diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:32:15 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:32:15 -0300 |
| commit | 36a0ddb01a4fcb8fb2fefd7c407ecca0e3e56e7c (patch) | |
| tree | bfd0a067edb5e802725bab48ebc7a0a09838feb1 /README.md | |
| download | ruby-36a0ddb01a4fcb8fb2fefd7c407ecca0e3e56e7c.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 207 |
1 files changed, 207 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..017fe89 --- /dev/null +++ b/README.md @@ -0,0 +1,207 @@ +# cepis + +Ruby client for the [public CEP API at cep.is](https://cep.is). Official +Correios data (DNE), refreshed every two weeks. No runtime dependencies: +`net/http`, `json` and `csv` come with Ruby. + +```ruby +require "cepis" + +address = Cepis.lookup("01001-000") +puts "#{address.logradouro} #{address.localidade} #{address.uf}" +# Praça da Sé São Paulo SP +``` + +## Installation + +```sh +gem install cepis +``` + +Or, in a `Gemfile`: + +```ruby +gem "cepis", git: "https://git.cep.is/ruby.git" +``` + +Requires Ruby 3.0 or newer. + +## Usage + +### A single lookup + +`lookup` accepts the CEP masked or unmasked and returns a `Cepis::Address`: + +```ruby +a = Cepis.lookup("01310-100") + +a.logradouro # "Avenida Paulista" +a.localidade # "São Paulo" +a.uf # "SP" +a.ibge # "3550308" +a.cep # "01310100" (always unmasked) +a.formatted_cep # "01310-100" +``` + +Fields the database does not have for that CEP come back as empty strings, +never `nil`. + +### Coordinates + +About 57% of CEPs have a coordinate. When they do not, `geo` is `nil`: + +```ruby +a = Cepis.lookup("01310100") + +if a.geo + puts "#{a.geo.lat} #{a.geo.lon}" # -23.567202 -46.649659 + puts a.geo.raio_m # 1084 + puts a.geo.precisao # "logradouro" +end +``` + +**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.precise?` is the shorthand for `precisao == "logradouro"`: + +```ruby +cost = if a.geo&.precise? + shipping_from(a.geo.lat, a.geo.lon) + else + shipping_from_municipality(a.ibge) + end +``` + +### Many CEPs at once + +Do not loop over `lookup`. Use `lookup_many`, which resolves up to 500 CEPs +per request: + +```ruby +ceps = %w[01001000 99999999 20040020] + +ceps.zip(Cepis.lookup_many(ceps)).each do |requested, address| + puts address.nil? ? "#{requested}: not found" : "#{requested}: #{address.localidade}" +end +``` + +The returned array has **the same length and the same order** as the input. A +CEP that does not exist, or is malformed, becomes `nil` at its position — it +does not vanish, and it does not raise. + +Arrays 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 `lookup_many` has `geo` set to +> `nil`, 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: + +```ruby +h = Cepis.health + +h.ok? # true +h.dne # "26071" identifier of the fortnightly database +h.ceps # 1596168 +h.base_gerada_em # "2026-07-24T03:48:28Z" +``` + +An API reporting itself degraded does not raise — that arrives in `h.status`. + +## Configuration + +For anything beyond the defaults, build a `Cepis::Client` and reuse it. It +keeps no state between calls and is safe to share across threads: + +```ruby +client = Cepis::Client.new( + base_url: "https://cep.is", # point at another instance + timeout: 10.0, # seconds per socket operation + max_attempts: 3, # counting the first; 1 disables retrying + backoff_base: 0.5, # base of the exponential wait + backoff_max: 8.0, # cap on each wait + total_timeout: nil # cap on the whole call, attempts included +) + +address = client.lookup("01001000") +``` + +Retries cover 429, 5xx and network failures, with exponential backoff and +jitter; 400 and 404 are never retried. + +The module-level methods (`Cepis.lookup`, etc.) use a shared default client. +Do not mutate it: it applies process-wide, including to third-party libraries +that also use this gem. Need a different configuration? Build your own. + +### Pointing at a local instance + +`base_url` is the only parameter that usually changes between environments. +The API repo brings a local instance up with `just up`: + +```ruby +client = Cepis::Client.new(base_url: "http://localhost:8080") +``` + +## Errors + +Everything derives from `Cepis::Error`, itself a `StandardError`. 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? | +| --- | --- | --- | +| `Cepis::InvalidCEP` | not 8 digits | no | +| `Cepis::CEPNotFound` | well-formed, not in the database | no | +| `Cepis::RateLimited` | 429 | yes, later | +| `Cepis::ServerError` | 5xx | yes, later | +| `Cepis::NetworkError` | DNS, connection, timeout, TLS | yes, later | +| `Cepis::InvalidResponse` | unintelligible response | probably not | + +```ruby +begin + address = Cepis.lookup(cep_from_form) +rescue Cepis::CEPNotFound + # ask the user for another CEP +rescue Cepis::Error + # transient failure: reschedule +end +``` + +`RateLimited#retry_after` carries what the server asked for, in seconds, and +`ServerError#status` the HTTP status of the last attempt. + +`InvalidResponse` 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` and the Ruby on your `PATH`. There is nothing +to install: the gem and its suite use only the standard library, and the tests +bring up a real HTTP server on loopback. + +```sh +make # list the targets +make test # run the suite (minitest) +make lint # ruby -w -c over the tree, plus a warning-clean load +make build # build the gem into pkg/ +make check # lint, then test, then build +``` + +Ruby ships no linter or formatter, and this gem does not depend on one. `make +lint` is therefore a syntax and warning check; if `rubocop` happens to be +installed it is run as well, and `make format` applies it. + +## Licence + +ISC. See [LICENSE](LICENSE). |