diff options
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 266 |
1 files changed, 266 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..3129bd8 --- /dev/null +++ b/README.md @@ -0,0 +1,266 @@ +# cepis + +Object Pascal client for the [public CEP API at cep.is](https://cep.is). +Official Correios data (DNE), refreshed every two weeks. + +```pascal +uses Cepis; + +var + Address: TCepisAddress; +begin + Address := CepisLookup('01001-000'); + try + WriteLn(Address.Logradouro, ' ', Address.Localidade, ' ', Address.UF); + // Praça da Sé São Paulo SP + finally + Address.Free; + end; +end. +``` + +## Installation + +There is nothing to build and no package manager involved. Copy the repository +somewhere and put `src/` on the unit search path: + +```sh +git clone https://git.cep.is/pascal.git +fpc -Fu/path/to/cepis-pascal/src yourprogram.pas +``` + +Requires Free Pascal 3.2 or newer. Only units that ship with the compiler are +used: `fphttpclient`, `opensslsockets`, `fpjson`, `jsonparser` and +`csvdocument`. + +### HTTPS needs OpenSSL + +`fphttpclient` loads OpenSSL at run time, so an `https://` base URL — which is +the default — needs `libssl` and `libcrypto` reachable by the dynamic loader +under a name FPC looks for. On Debian and Ubuntu that means `libssl-dev` +installed, which provides the unversioned `libssl.so` and `libcrypto.so` +symlinks; on other systems install the equivalent development package. + +Without them every request fails with `ECepisNetworkError: Could not +initialize OpenSSL library`. Plain `http://` needs nothing. + +### Delphi + +Developed and tested with Free Pascal. The HTTP call is isolated in +`CepisHttp.pas`, which has a `System.Net.HttpClient` branch for Delphi; the +JSON and CSV parsing in `Cepis.pas` uses the FCL units above and would need the +`System.JSON` equivalents. The Delphi branch has never been compiled. + +## Ownership + +Every object a method returns belongs to the caller, who frees it, with two +exceptions: + +| object | freed by | +| --- | --- | +| `TCepisAddress` from `Lookup` | you | +| `TCepisAddressList` from `LookupMany` | you — and freeing it frees the addresses inside | +| `TCepisHealth` from `Health` | you | +| `TCepisGeo` on an address | its address | +| the client behind `CepisLookup` and friends | the `Cepis` unit, at finalisation | + +`TCepisAddressList.Extract` and `TCepisAddress.ExtractGeo` hand ownership over +when you need one item to outlive its container. + +## Usage + +### A single lookup + +`Lookup` accepts the CEP masked or unmasked and returns a `TCepisAddress`: + +```pascal +Address := CepisLookup('01310-100'); + +Address.Logradouro; // 'Avenida Paulista' +Address.Localidade; // 'São Paulo' +Address.UF; // 'SP' +Address.Ibge; // '3550308' +Address.Cep; // '01310100' (always unmasked) +Address.FormattedCEP; // '01310-100' +``` + +Fields the database does not have for that CEP come back as empty strings, +never `nil`. Strings are UTF-8. + +### Coordinates + +About 57% of CEPs have a coordinate. When they do not, `Geo` is `nil`: + +```pascal +if Address.Geo <> nil then +begin + WriteLn(Address.Geo.Lat:0:6, ' ', Address.Geo.Lon:0:6); // -23.567202 -46.649659 + WriteLn(Address.Geo.RaioM); // 1084 + WriteLn(Address.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 | + +`Address.Geo.IsPrecise` is the shorthand for `Precisao = 'logradouro'`: + +```pascal +if (Address.Geo <> nil) and Address.Geo.IsPrecise then + Cost := ShippingFrom(Address.Geo.Lat, Address.Geo.Lon) +else + Cost := ShippingFromMunicipality(Address.Ibge); +``` + +### Many CEPs at once + +Do not loop over `Lookup`. Use `LookupMany`, which resolves up to 500 CEPs per +request: + +```pascal +var + Ceps: array[0..2] of string = ('01001000', '99999999', '20040020'); + Result: TCepisAddressList; + I: Integer; +begin + Result := CepisLookupMany(Ceps); + try + for I := 0 to Result.Count - 1 do + if Result[I] = nil then + WriteLn(Ceps[I], ': not found') + else + WriteLn(Ceps[I], ': ', Result[I].Localidade); + finally + Result.Free; + end; +end; +``` + +The list 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 does not raise. + +Input longer than 500 is chunked automatically. + +> **Batches carry no coordinate.** The endpoint behind them is CSV, and the CSV +> has no `geo` column. Every address from `LookupMany` has `Geo = 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: + +```pascal +Health := CepisHealth; +try + Health.Ok; // True + Health.Dne; // '26071' identifier of the fortnightly database + Health.Ceps; // 1596168 + Health.BaseGeradaEm; // '2026-07-24T03:48:28Z' +finally + Health.Free; +end; +``` + +An API reporting itself degraded does not raise — that arrives in +`Health.Status`. + +## Configuration + +For anything beyond the defaults, build a `TCepisClient` and reuse it. It keeps +no state between calls: + +```pascal +Client := TCepisClient.Create; +try + Client.BaseURL := 'https://cep.is'; // point at another instance + Client.Timeout := 10.0; // seconds per socket operation + Client.MaxAttempts := 3; // counting the first; 1 disables retrying + Client.BackoffBase := 0.5; // base of the exponential wait + Client.BackoffMax := 8.0; // cap on each wait + Client.TotalTimeout := 0; // cap on the whole call; 0 means none + Client.UserAgent := 'myapp/1.0'; + + Address := Client.Lookup('01001000'); + ... +finally + Client.Free; +end; +``` + +Retries cover 429, 5xx and network failures, with exponential backoff and +jitter; 400 and 404 are never retried. + +`CepisLookup`, `CepisLookupMany` and `CepisHealth` share one default client. Do +not reconfigure it: it applies process-wide, including to other code in the same +program that uses this unit. Need different settings? Build your own. + +### Pointing at a local instance + +`BaseURL` is the only property that usually changes between environments. The +API repo brings a local instance up with `just up`: + +```pascal +Client.BaseURL := 'http://localhost:8080'; +``` + +## Errors + +Everything derives from `ECepis`. 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? | +| --- | --- | --- | +| `ECepisInvalidCEP` | not 8 digits | no | +| `ECepisNotFound` | well-formed, not in the database | no | +| `ECepisRateLimited` | 429; carries `RetryAfter` | yes, later | +| `ECepisServerError` | 5xx; carries `Status` | yes, later | +| `ECepisNetworkError` | DNS, connection, timeout, TLS | yes, later | +| `ECepisInvalidResponse` | unintelligible response | probably not | + +```pascal +try + Address := CepisLookup(CepFromForm); +except + on E: ECepisNotFound do + ; // ask the user for another CEP + on E: ECepis do + ; // transient failure: reschedule +end; +``` + +`CepisNormalizeCEP` strips the mask and validates the 8 digits on its own, for +code that wants to check before it asks: + +```pascal +Number := CepisNormalizeCEP(' 01001-000 '); // '01001000' +``` + +`ECepisInvalidResponse` 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`, which shells out to `fpc`: + +```sh +make # list the targets +make test # compile and run the suite against a local HTTP server +make lint # compile with warnings as errors, touches nothing +make build # compile the units into build/units +make check # lint, then test, then build +``` + +The suite stands up a real HTTP server on loopback and points `BaseURL` at it. +It needs no network. + +## Licence + +ISC. See [LICENSE](LICENSE). |