summaryrefslogtreecommitdiffstats
path: root/internal/lookup/lookup.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/lookup/lookup.go')
-rw-r--r--internal/lookup/lookup.go248
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, &notFound) {
- 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
-}