summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:45:04 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:45:04 -0300
commitcf5597871327dbff2c7c743672d696e7ff340c42 (patch)
tree2502272b9042d9bc980b20a8a3f308b7c5e7fe87 /README.md
downloadpascal-cf5597871327dbff2c7c743672d696e7ff340c42.tar.gz
Initial importv0.1.0
Diffstat (limited to 'README.md')
-rw-r--r--README.md266
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).