diff options
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 219 |
1 files changed, 219 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..63f0e96 --- /dev/null +++ b/README.md @@ -0,0 +1,219 @@ +# cepis + +Go client for the [public CEP API at cep.is](https://cep.is). Official Correios +data (DNE), refreshed every two weeks. No dependencies beyond the standard +library. + +```go +addr, err := cepis.Lookup(ctx, "01001-000") +fmt.Println(addr.Logradouro, addr.Localidade, addr.UF) +// Praça da Sé São Paulo SP +``` + +## Installation + +```sh +go get cep.is/go +``` + +Requires Go 1.21 or newer. + +```go +import cepis "cep.is/go" +``` + +> `cep.is/go` is a vanity import path and needs a `go-import` meta tag served by +> cep.is; that route does not exist yet, so `go get` will not resolve until it +> does. + +## Usage + +### A single lookup + +`Lookup` accepts the CEP masked or unmasked and returns an `*Address`: + +```go +a, err := cepis.Lookup(ctx, "01310-100") + +a.Logradouro // "Avenida Paulista" +a.Localidade // "São Paulo" +a.UF // "SP" +a.IBGE // "3550308" +a.CEP // "01310100" (always unmasked) +a.FormattedCEP() // "01310-100" +``` + +Fields the database does not have for that CEP come back as empty strings, +never null. `a.Field("name")` reads any field of the raw response, including +one the API grew after this release. + +### Coordinates + +About 57% of CEPs have a coordinate. When they do not, `Geo` is nil: + +```go +if a.Geo != nil { + fmt.Println(a.Geo.Lat, a.Geo.Lon) // -23.567202 -46.649659 + fmt.Println(a.Geo.RaioM) // 1084 + fmt.Println(a.Geo.Precisao) // "logradouro" +} +``` + +**Check `Precisao` before computing distance.** There are two kinds of point, +and the difference between them is tens of kilometres: + +| `Precisao` | typical radius | what the point is | +| --- | --- | --- | +| `logradouro` | 110–140 m | the stretch of street — good for maps and distance | +| `localidade` | 7–40 km | the municipality centroid: the CEP *is* the whole city | + +`a.Geo.IsPrecise()` is the shorthand for `Precisao == "logradouro"`: + +```go +if a.Geo != nil && a.Geo.IsPrecise() { + cost = shippingFrom(a.Geo.Lat, a.Geo.Lon) +} else { + cost = shippingFromMunicipality(a.IBGE) +} +``` + +### Many CEPs at once + +Do not loop over `Lookup`. Use `LookupMany`, which resolves up to 500 CEPs per +request: + +```go +ceps := []string{"01001000", "99999999", "20040020"} +result, err := cepis.LookupMany(ctx, ceps) + +for i, addr := range result { + if addr == nil { + fmt.Printf("%s: not found\n", ceps[i]) + } else { + fmt.Printf("%s: %s\n", ceps[i], addr.Localidade) + } +} +``` + +The result has **the same length and the same order** as the input. A CEP that +does not exist, or is malformed, is nil at its position — it does not vanish, +and it is not an error. + +Slices longer than 500 are chunked automatically. + +> **Batches carry no coordinate.** The endpoint behind them is CSV, and the CSV +> has no geo column. Every `Address` from `LookupMany` has a nil `Geo`, +> including for CEPs that do have one. Need `Geo`? Use `Lookup`, one at a time. + +### API health + +Useful for two things: checking the API answers, and knowing **which version of +the database** is live: + +```go +h, err := cepis.CheckHealth(ctx) + +h.OK() // true +h.DNE // "26071" identifier of the fortnightly database +h.CEPs // 1596168 +h.BaseGeradaEm // "2026-07-24T03:48:28Z" +``` + +An API reporting itself degraded is not an error — that arrives in `h.Status`. + +## Configuration + +For anything beyond the defaults, build a `Client` and reuse it. It keeps no +state between calls and is safe to share across goroutines: + +```go +client, err := cepis.New( + cepis.WithBaseURL("https://cep.is"), // point at another instance + cepis.WithTimeout(10*time.Second), // cap on one attempt + cepis.WithMaxAttempts(3), // counting the first; 1 disables retrying + cepis.WithBackoff(500*time.Millisecond, 8*time.Second), + cepis.WithTotalTimeout(15*time.Second), // cap on the whole call + cepis.WithUserAgent("myapp/1.0"), + cepis.WithHTTPClient(&http.Client{}), +) + +addr, err := client.Lookup(ctx, "01001000") +``` + +`WithTimeout` bounds a single attempt — connection, response and body read +together. A call may make several attempts with waits between them; the cap on +all of that is `WithTotalTimeout`. A deadline on the context passed to a call is +honoured too, and the earlier of the two wins. + +Retries cover 429, 5xx and network failures, with exponential backoff and full +jitter; 400 and 404 are never retried. See the [package +documentation](https://pkg.go.dev/cep.is/go) for each option. + +The package-level functions (`cepis.Lookup`, etc.) use a shared default client, +reachable as `cepis.Default()`. It applies process-wide, third-party libraries +included. Need a different configuration? Build your own. + +### Pointing at a local instance + +`WithBaseURL` is the only setting that usually changes between environments. The +API repo brings a local instance up with `just up`: + +```go +client, err := cepis.New(cepis.WithBaseURL("http://localhost:8080")) +``` + +## Errors + +Every error matches `cepis.Err`. Below that, the split is by **the decision you +have to make**, not by the HTTP status that caused it: + +| error | when | does retrying help? | +| --- | --- | --- | +| `ErrInvalidCEP` | not 8 digits | no | +| `ErrCEPNotFound` | well-formed, not in the database | no | +| `ErrRateLimited` | 429 | yes, later | +| `ErrServer` | 5xx | yes, later | +| `ErrNetwork` | DNS, connection, timeout, TLS | yes, later | +| `ErrInvalidResponse` | unintelligible response | probably not | +| `ErrInvalidConfig` | a bad option given to `New` | no | + +```go +addr, err := cepis.Lookup(ctx, cepFromForm) +switch { +case errors.Is(err, cepis.ErrCEPNotFound): + // ask the user for another CEP +case errors.Is(err, cepis.Err): + // transient failure: reschedule +} +``` + +The concrete type behind every one of them is `*cepis.Error`, which carries the +CEP, the HTTP status and the server's `Retry-After`: + +```go +var cerr *cepis.Error +if errors.As(err, &cerr) && errors.Is(cerr, cepis.ErrRateLimited) { + reschedule(cerr.RetryAfter) +} +``` + +`ErrInvalidResponse` almost always means something between you and the API is +answering instead of it — a captive Wi-Fi portal, a corporate proxy, an HTML +error page. Useful as a diagnosis: the problem is not in your code. + +## Development + +Everything runs through `make`: + +```sh +make # list the targets +make test # run the suite +make test-race # run it under the race detector +make lint # gofmt -l + go vet, touches nothing +make build # compile every package +make check # lint, then test, then build +``` + +## Licence + +ISC. See [LICENSE](LICENSE). |