summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:32:15 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:32:15 -0300
commit36a0ddb01a4fcb8fb2fefd7c407ecca0e3e56e7c (patch)
treebfd0a067edb5e802725bab48ebc7a0a09838feb1 /README.md
downloadruby-36a0ddb01a4fcb8fb2fefd7c407ecca0e3e56e7c.tar.gz
Initial importv0.1.0
Diffstat (limited to 'README.md')
-rw-r--r--README.md207
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).