summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 14:40:05 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 14:40:05 -0300
commit0bc578d6d334c7f1ccaf329cd61bb8bce76b7f59 (patch)
tree86f73108f0432fd8330edcaab8b8ae7018322dee /README.md
downloadperl-6661cb35162c38099b86f248b938f5e4d4d11515.tar.gz
Initial importv0.1.0
Diffstat (limited to 'README.md')
-rw-r--r--README.md293
1 files changed, 293 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b799f45
--- /dev/null
+++ b/README.md
@@ -0,0 +1,293 @@
+# Cepis
+
+Perl client for the [public CEP API at cep.is](https://cep.is). Official
+Correios data (DNE), refreshed every two weeks. No non-core dependencies:
+`HTTP::Tiny`, `JSON::PP` and `Encode` ship with Perl, and the CSV reader is part
+of the distribution.
+
+```perl
+use Cepis;
+
+my $address = Cepis::lookup('01001-000');
+printf "%s %s %s\n", $address->logradouro, $address->localidade, $address->uf;
+# Praça da Sé São Paulo SP
+```
+
+## Installation
+
+```sh
+perl Makefile.PL
+make
+make test
+make install
+```
+
+Requires Perl 5.14 or newer — that is when `HTTP::Tiny` and `JSON::PP` entered
+the core.
+
+### HTTPS needs two modules that Perl does not ship
+
+The library itself needs nothing from CPAN, but **reaching `https://cep.is`
+does**. `HTTP::Tiny` speaks TLS only when both of these are installed:
+
+| module | minimum version |
+| --- | --- |
+| `IO::Socket::SSL` | 1.968 |
+| `Net::SSLeay` | 1.49 |
+
+Neither is a core module. Without them every call against the default
+`base_url` fails, so install them alongside this distribution:
+
+```sh
+cpanm IO::Socket::SSL Net::SSLeay
+# or, from your distribution's packages:
+# Debian/Ubuntu apt install libio-socket-ssl-perl libnet-ssleay-perl
+# Arch pacman -S perl-io-socket-ssl perl-net-ssleay
+# Alpine apk add perl-io-socket-ssl perl-net-ssleay
+# OpenBSD pkg_add p5-IO-Socket-SSL p5-Net-SSLeay
+```
+
+The client checks for them before it opens a socket and, when they are missing,
+throws `Cepis::Error::Network` immediately with a message naming both — rather
+than letting a bare `HTTP::Tiny` internal error surface:
+
+```
+cannot reach https://cep.is/01001000: HTTP::Tiny has no HTTPS support on this
+Perl. It needs IO::Socket::SSL 1.968 or later and Net::SSLeay 1.49 or later,
+and neither of those ships with Perl. Install both (...), or point base_url at
+an http:// instance. HTTP::Tiny reports: ...
+```
+
+To check without making a request:
+
+```perl
+my ( $ok, $why ) = Cepis::Client::tls_available();
+```
+
+Plain HTTP — a local instance, a test server — works on a stock Perl with
+nothing installed.
+
+## Usage
+
+### A single lookup
+
+`lookup` accepts the CEP masked or unmasked and returns a `Cepis::Address`:
+
+```perl
+my $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
+`undef`. `$a->field('name')` reads any field of the raw response, including one
+the API grew after this release.
+
+Perl does not distinguish a number from a string, so keep CEPs in strings: the
+literal `01001000` loses its leading zero before this library ever sees it.
+
+### Coordinates
+
+About 57% of CEPs have a coordinate. When they do not, `geo` is `undef`:
+
+```perl
+my $a = Cepis::lookup('01310100');
+
+if ( my $geo = $a->geo ) {
+ printf "%s %s\n", $geo->lat, $geo->lon; # -23.567202 -46.649659
+ print $geo->raio_m, "\n"; # 1084
+ print $geo->precisao, "\n"; # "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 |
+
+`$geo->is_precise` is the shorthand for `precisao eq 'logradouro'`:
+
+```perl
+my $cost
+ = $a->geo && $a->geo->is_precise
+ ? shipping_from( $a->geo->lat, $a->geo->lon )
+ : shipping_from_municipality( $a->ibge );
+```
+
+### Many CEPs at once
+
+Do not loop over `lookup`. Use `lookup_many`, which resolves up to 500 CEPs per
+request:
+
+```perl
+my @ceps = qw(01001000 99999999 20040020);
+my $result = Cepis::lookup_many( \@ceps );
+
+for my $i ( 0 .. $#ceps ) {
+ print defined $result->[$i]
+ ? "$ceps[$i]: " . $result->[$i]->localidade . "\n"
+ : "$ceps[$i]: not found\n";
+}
+```
+
+The returned arrayref has **the same length and the same order** as the input. A
+CEP that does not exist, or is malformed, is `undef` at its position — it does
+not vanish, and it does not throw.
+
+A Perl array holds `undef` at any position without shortening, so nothing is
+dropped and nothing shifts: `$result->[$i]` always corresponds to `$ceps[$i]`.
+That is worth stating because it is not true everywhere — in Lua, for instance,
+a nil in a table truncates it. Here `scalar @$result == scalar @ceps` always
+holds, and `grep { defined } @$result` is how you discard the misses when the
+pairing no longer matters.
+
+Lists longer than 500 are chunked automatically.
+
+> **Batches carry no coordinate.** The endpoint behind them is CSV, and the CSV
+> has no `geo` column. Every `Cepis::Address` from `lookup_many` has `geo` set
+> to `undef`, 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:
+
+```perl
+my $h = Cepis::health();
+
+$h->ok; # 1
+$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 throw — that arrives in `$h->status`.
+
+## Configuration
+
+For anything beyond the defaults, build a `Cepis::Client` and reuse it. It keeps
+no state between calls:
+
+```perl
+use Cepis::Client;
+
+my $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 => undef, # cap on the whole call, attempts included
+ user_agent => undef, # defaults to cepis-perl/<version>
+);
+
+my $address = $client->lookup('01001000');
+```
+
+`timeout` bounds each socket operation, not the whole call — that cap is
+`total_timeout`, and without it the worst case is `max_attempts * timeout` plus
+the sum of the backoffs.
+
+Retries cover 429, 5xx and network failures, with exponential backoff and full
+jitter; 400 and 404 are never retried.
+
+The module-level functions (`Cepis::lookup`, and so on) share one default
+client, reachable as `Cepis::default_client()`. It applies process-wide,
+including to third-party libraries that also use this module. Need a different
+configuration? Build your own.
+
+Each client holds one `HTTP::Tiny`, which is not shared between threads. Give
+each thread its own client.
+
+### Pointing at a local instance
+
+`base_url` is the only setting that usually changes between environments. The
+API repo brings a local instance up with `just up`:
+
+```perl
+my $client = Cepis::Client->new( base_url => 'http://localhost:8080' );
+```
+
+## Errors
+
+Every failure is thrown as an object under `Cepis::Error`, so one test catches
+the lot. 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::Error::InvalidCEP` | not 8 digits | no |
+| `Cepis::Error::CEPNotFound` | well-formed, not in the database | no |
+| `Cepis::Error::RateLimited` | 429 | yes, later |
+| `Cepis::Error::ServerError` | 5xx | yes, later |
+| `Cepis::Error::Network` | DNS, connection, timeout, TLS | yes, later |
+| `Cepis::Error::InvalidResponse` | unintelligible response | probably not |
+
+```perl
+my $address = eval { Cepis::lookup($cep_from_form) };
+if ( !$address ) {
+ my $err = $@;
+ if ( ref $err && $err->isa('Cepis::Error::CEPNotFound') ) {
+ # ask the user for another CEP
+ }
+ elsif ( ref $err && $err->isa('Cepis::Error') ) {
+ # transient failure: reschedule
+ }
+ else {
+ die $err;
+ }
+}
+```
+
+Errors stringify to their message, so `warn $err` and `"$err"` do the obvious
+thing. `$err->retry_after` carries what the server asked for, in seconds, and
+`$err->status` the HTTP status of the last attempt.
+
+`Cepis::Error::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.
+
+A bad argument to `Cepis::Client->new` is a `croak`, not a `Cepis::Error`: that
+is a programming error, not a failed lookup.
+
+## Development
+
+Everything runs through `make` and the `perl` on your `PATH`. There is nothing
+to install: the distribution and its suite use only core modules, and the tests
+bring up a real HTTP server on loopback.
+
+```sh
+make # list the targets
+make test # run the suite with prove
+make lint # perl -c over lib, t and Makefile.PL
+make build # build Cepis-0.1.0.tar.gz
+make check # lint, then test, then build
+```
+
+`prove -l t/` works too.
+
+Perl ships no linter or formatter in its core, and this distribution depends on
+neither. `make lint` is therefore a `perl -c` compile check; if `perlcritic`
+happens to be installed it is run as well, and `make format` applies `perltidy`
+when that is installed. Neither is installed here, so `make lint` reports the
+syntax checks only.
+
+The `Makefile` in the repository is for development only and is not shipped.
+The tarball carries `Makefile.PL`, so installing from it is the usual
+`perl Makefile.PL && make && make test && make install`.
+
+Documentation lives in POD on the public API: `perldoc lib/Cepis.pm`,
+`perldoc lib/Cepis/Client.pm`.
+
+## Licence
+
+ISC. See [LICENSE](LICENSE).