diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-27 21:12:05 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-27 21:12:05 -0300 |
| commit | 97b49ae7081b989274b313532ff17c71694097a9 (patch) | |
| tree | 6595e0d5a39d04971a71e886bc3f8cc589741f5d /internal/lookup/lookup.go | |
| parent | 83ce95cf10fd9e9d581362453c558239d37ae06e (diff) | |
| download | telegram-0.2.1.tar.gz | |
cep.is/go v0.2.0 exposes the municipality as Error.Faixa, which is what
internal/lookup existed to work around: v0.1.0 discarded the 404 body, so
the bot re-issued the request just to read the block back.
The extra GET on the 404 path is gone, and with it the second timeout,
the body size cap and the "is it still a 404?" check that guarded it.
The replies are byte for byte what they were -- the formatting tests did
not change a single assertion, only the type they build.
Diffstat (limited to 'internal/lookup/lookup.go')
| -rw-r--r-- | internal/lookup/lookup.go | 248 |
1 files changed, 0 insertions, 248 deletions
diff --git a/internal/lookup/lookup.go b/internal/lookup/lookup.go deleted file mode 100644 index 956a731..0000000 --- a/internal/lookup/lookup.go +++ /dev/null @@ -1,248 +0,0 @@ -// Package lookup resolves a CEP through the cep.is API. -// -// It is a thin layer over the official client, and it exists for one thing the -// client does not do: a 404 from GET /{cep} may carry a `faixa` block naming -// the municipality that CEP would belong to, and cep.is/go v0.1.0 turns the -// response into [cepis.ErrCEPNotFound] without keeping anything from the body. -// This package puts that block back, as [NotFoundError.Faixa]. -// -// Everything about it is best-effort. The municipality is context on top of an -// error that is already complete without it, so every failure along the way — -// no body, malformed JSON, a slow second request — ends as a plain -// ErrCEPNotFound, exactly as if the API had never sent the block. -package lookup - -import ( - "context" - "encoding/json" - "errors" - "io" - "net/http" - "strings" - "time" - - cepis "cep.is/go" -) - -// defaultFaixaTimeout caps the extra request. It is deliberately short and -// deliberately not the timeout of the lookup itself: the CEP is already known -// not to exist, the answer is already writable, and nobody should wait longer -// for the footnote than for the answer. -const defaultFaixaTimeout = 2 * time.Second - -// maxFaixaBody is how much of the 404 body is read. The real one is around 200 -// bytes; anything past this is not the API answering. -const maxFaixaBody = 64 << 10 - -// Faixa is the municipality a CEP that does not exist would fall in, together -// with the range of CEPs that municipality actually has. -// -// A CEP that fails is rarely a random number: it is usually someone who knows -// the city and guessed the rest, and the classic guess is the "-000" that 957 -// municipalities never received. Areal/RJ answers from 25845-001 up and never -// at 25845-000, so the 404 for 25845000 names Areal. -// -// CEPIni and CEPFim are the smallest and the largest CEP that EXIST there, not -// the bounds the Correios publish — the DNE's own start for Areal is the -// 25845000 that just failed. The range is sparse, which is what CEPs is for: -// Areal's 118 CEPs are scattered over nearly five thousand numbers. Never -// present the pair as a list of valid CEPs. -type Faixa struct { - Localidade string `json:"localidade"` - UF string `json:"uf"` - Estado string `json:"estado"` - Regiao string `json:"regiao"` - IBGE string `json:"ibge"` - CEPIni string `json:"cep_ini"` - CEPFim string `json:"cep_fim"` - - // CEPs is how many CEPs the municipality has inside the range. - CEPs int `json:"ceps"` -} - -// Place is the municipality as it is written to a user: "Areal/RJ", or just -// "Areal" when the API sent no UF. -func (f *Faixa) Place() string { - if f == nil { - return "" - } - if f.UF == "" { - return f.Localidade - } - return f.Localidade + "/" + f.UF -} - -// NotFoundError is [cepis.ErrCEPNotFound] with the municipality attached. It -// matches the sentinel through errors.Is, so a caller that does not know about -// this type keeps behaving exactly as before: -// -// if errors.Is(err, cepis.ErrCEPNotFound) { ... } -type NotFoundError struct { - // Faixa is the municipality the CEP would belong to. It is never nil in - // an error this package builds: without a faixa there is nothing to add - // and the underlying error is returned untouched. - Faixa *Faixa - - // Err is the error from the client library, wrapped unchanged. - Err error -} - -// Error implements the error interface, reporting the wrapped failure verbatim: -// the faixa is for the user, not for the log line. -func (e *NotFoundError) Error() string { - if e.Err != nil { - return e.Err.Error() - } - return cepis.ErrCEPNotFound.Error() -} - -// Unwrap keeps errors.Is(err, cepis.ErrCEPNotFound) true. -func (e *NotFoundError) Unwrap() error { - if e.Err != nil { - return e.Err - } - return cepis.ErrCEPNotFound -} - -// FaixaOf returns the municipality carried by err, or nil when there is none. -// Any error is safe to pass, including nil. -func FaixaOf(err error) *Faixa { - var notFound *NotFoundError - if errors.As(err, ¬Found) { - return notFound.Faixa - } - return nil -} - -// Client resolves CEPs. It is safe for concurrent use, as the client it wraps -// is. -type Client struct { - api *cepis.Client - httpClient *http.Client - timeout time.Duration -} - -// Option configures a [Client]. -type Option func(*Client) - -// WithTimeout caps the extra request that reads the faixa off a 404. -func WithTimeout(timeout time.Duration) Option { - return func(c *Client) { - if timeout > 0 { - c.timeout = timeout - } - } -} - -// WithHTTPClient supplies the [net/http.Client] used for that request. -func WithHTTPClient(httpClient *http.Client) Option { - return func(c *Client) { - if httpClient != nil { - c.httpClient = httpClient - } - } -} - -// New wraps an already configured cep.is client. -func New(api *cepis.Client, opts ...Option) *Client { - c := &Client{ - api: api, - httpClient: &http.Client{}, - timeout: defaultFaixaTimeout, - } - for _, opt := range opts { - if opt != nil { - opt(c) - } - } - return c -} - -// Lookup resolves a single CEP. -// -// It behaves exactly like [cepis.Client.Lookup], except that a CEP absent from -// the DNE may come back as a [*NotFoundError] naming the municipality. That is -// still a failure and still matches cepis.ErrCEPNotFound; nothing about it says -// an address was found. -func (c *Client) Lookup(ctx context.Context, cep string) (*cepis.Address, error) { - address, err := c.api.Lookup(ctx, cep) - if err == nil || !errors.Is(err, cepis.ErrCEPNotFound) { - return address, err - } - - faixa := c.faixa(ctx, cep) - if faixa == nil { - return nil, err - } - return nil, &NotFoundError{Faixa: faixa, Err: err} -} - -// faixa asks for the CEP once more, only to read the block the library dropped. -// -// One extra GET is the price of not forking the client. It is bounded to the -// 404 path, which is definitive and never retried, so the request cannot land -// in a retry loop; and it returns nil for every problem it meets, because the -// caller already has a complete answer to send without it. -// -// Delete this the day cep.is/go exposes the faixa itself. -func (c *Client) faixa(ctx context.Context, cep string) *Faixa { - number, err := cepis.NormalizeCEP(cep) - if err != nil { - return nil - } - - ctx, cancel := context.WithTimeout(ctx, c.timeout) - defer cancel() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.api.BaseURL()+"/"+number, nil) - if err != nil { - return nil - } - req.Header.Set("User-Agent", c.api.UserAgent()) - req.Header.Set("Accept", "application/json") - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil - } - defer resp.Body.Close() - - // Anything but a second 404 means the answer changed under us — a CEP - // that now exists, an outage, a captive portal. None of those is a - // municipality. - if resp.StatusCode != http.StatusNotFound { - return nil - } - - body, err := io.ReadAll(io.LimitReader(resp.Body, maxFaixaBody)) - if err != nil { - return nil - } - return decodeFaixa(body) -} - -// decodeFaixa reads the `faixa` block of a 404 body. Every failure yields nil: -// an absent block (the API omits it, never sends null), a body that is not -// JSON, a block that names no municipality. A field the API grows later is -// ignored rather than fatal, which is what encoding/json does by default. -func decodeFaixa(body []byte) *Faixa { - if len(body) == 0 { - return nil - } - - var envelope struct { - Faixa *Faixa `json:"faixa"` - } - if err := json.Unmarshal(body, &envelope); err != nil { - return nil - } - - faixa := envelope.Faixa - if faixa == nil || strings.TrimSpace(faixa.Localidade) == "" { - return nil - } - if faixa.CEPs < 0 { - faixa.CEPs = 0 - } - return faixa -} |