diff options
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 264 |
1 files changed, 264 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..021448c --- /dev/null +++ b/README.md @@ -0,0 +1,264 @@ +# cepis + +COBOL client for the [public CEP API at cep.is](https://cep.is). Official +Correios data (DNE), refreshed every two weeks. + +```cobol +WORKING-STORAGE SECTION. +COPY "cepis.cpy". + +PROCEDURE DIVISION. + CALL "CEPIS-DEFAULT-CONFIG" USING CEPIS-CONFIG + MOVE "01001-000" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + IF CEPIS-OK + DISPLAY ADDR-LOGRADOURO " " ADDR-LOCALIDADE " " ADDR-UF + END-IF +``` + +Targets **GnuCOBOL 3.x**, default dialect, **free source format** +(`cobc -free`). HTTP goes through **libcurl**, reached from a small C shim +in `src/cepis-http.c` rather than from COBOL directly, because +`curl_easy_setopt` is variadic and a COBOL `CALL` cannot express that +portably. + +## Installation + +```sh +git clone https://git.cep.is/cobol.git cepis-cobol +cd cepis-cobol +make build +``` + +That produces `build/libcepis.a`. Link it into your program and add the +copybook directory to the search path: + +```sh +cobc -x -free -I /path/to/cepis-cobol/copy \ + -o myprog myprog.cob /path/to/cepis-cobol/build/libcepis.a -lcurl +``` + +Requires GnuCOBOL 3.x, a C compiler and libcurl with its development +headers. `python3` is needed only to run the test suite. + +## Usage + +Everything is a `CALL` to a subprogram, and everything shares two records +from `cepis.cpy`: `CEPIS-CONFIG` going in and `CEPIS-STATUS` coming back. +`CALL "CEPIS-DEFAULT-CONFIG" USING CEPIS-CONFIG` fills the configuration +with its defaults; override afterwards what you need. + +### A single lookup + +`CEPIS-LOOKUP` accepts the CEP masked or unmasked: + +```cobol +MOVE "01310-100" TO CEPIS-CEP-INPUT +CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + +ADDR-LOGRADOURO *> 'Avenida Paulista' +ADDR-LOCALIDADE *> 'São Paulo' +ADDR-UF *> 'SP' +ADDR-IBGE *> '3550308' +ADDR-CEP *> '01310100' always unmasked +ADDR-CEP-MASKED *> '01310-100' +``` + +Fields the database does not have for that CEP come back as spaces. +`CEPIS-ADDRESS` is cleared before every call, so a failed lookup never +leaves the previous CEP's street behind. + +### Coordinates + +About 57% of CEPs have a coordinate. There is an explicit flag rather than +a marker value: + +```cobol +IF ADDR-GEO-KNOWN + DISPLAY ADDR-GEO-LAT " " ADDR-GEO-LON + DISPLAY ADDR-GEO-RAIO-M " " ADDR-GEO-PRECISAO +END-IF +``` + +**Check `ADDR-GEO-PRECISAO` before computing distance.** There are two +kinds of point, and the difference between them is tens of kilometres: + +| `precisao` | condition name | typical radius | what the point is | +| --- | --- | --- | --- | +| `logradouro` | `ADDR-GEO-PRECISE` | 110–140 m | the stretch of street — good for maps and distance | +| `localidade` | `ADDR-GEO-CITY-WIDE` | 7–40 km | the municipality centroid: the CEP *is* the whole city | + +```cobol +IF ADDR-GEO-KNOWN AND ADDR-GEO-PRECISE + PERFORM SHIPPING-FROM-POINT +ELSE + PERFORM SHIPPING-FROM-MUNICIPALITY +END-IF +``` + +`ADDR-GEO-LAT` and `ADDR-GEO-LON` are fixed-point `S9(3)V9(6)`, six +decimal places — about 11 cm, well past what the data supports. + +### Many CEPs at once + +Do not loop over `CEPIS-LOOKUP`. `CEPIS-LOOKUP-MANY` resolves up to 500 +CEPs per request: + +```cobol +MOVE 3 TO BATCH-COUNT +MOVE "01001000" TO BATCH-REQUESTED (1) +MOVE "99999999" TO BATCH-REQUESTED (2) +MOVE "20040020" TO BATCH-REQUESTED (3) + +CALL "CEPIS-LOOKUP-MANY" USING CEPIS-CONFIG CEPIS-BATCH CEPIS-STATUS + +PERFORM VARYING I FROM 1 BY 1 UNTIL I > BATCH-COUNT + IF BATCH-ENTRY-FOUND (I) + DISPLAY BATCH-REQUESTED (I) ": " BAT-LOCALIDADE (I) + ELSE + DISPLAY BATCH-REQUESTED (I) ": not found" + END-IF +END-PERFORM +``` + +Every slot comes back in place. A CEP that does not exist, or that is not +eight digits, leaves its slot `BATCH-ENTRY-MISSING` — it is never removed, +never shifted, and never fails the call. + +The table holds 1000 slots and requests are chunked at 500 automatically. +A `BATCH-COUNT` above 1000 is refused with `CEPIS-INVALID-CEP` rather than +silently truncated. + +> **Batches carry no coordinate.** The endpoint behind them is CSV, and the +> CSV has no `geo` column. Every slot has `BAT-GEO-ABSENT` set, including +> CEPs that do have a coordinate. Need `geo`? Use `CEPIS-LOOKUP`, one at a +> time. + +### API health + +```cobol +CALL "CEPIS-HEALTH" USING CEPIS-CONFIG CEPIS-HEALTH CEPIS-STATUS + +HEALTH-OK *> condition name, true when status is "ok" +HEALTH-DNE *> '26071' identifier of the fortnightly database +HEALTH-CEPS *> 1596168 +HEALTH-BASE-GERADA-EM *> '2026-07-24T03:48:28Z' +``` + +An API reporting itself degraded does not fail the call — that arrives in +`HEALTH-STATUS` with `CEPIS-OK` set. + +### Validating a CEP on its own + +```cobol +MOVE "01001-000" TO CEPIS-CEP-INPUT +CALL "CEPIS-NORMALIZE-CEP" USING CEPIS-CEP-INPUT CEPIS-CEP CEPIS-STATUS +*> CEPIS-CEP is now '01001000' +``` + +## Configuration + +| field | default | meaning | +| --- | --- | --- | +| `CFG-BASE-URL` | `https://cep.is` | where the API lives; point it at a local instance | +| `CFG-TIMEOUT-MS` | `10000` | per request: bounds the connect and the transfer | +| `CFG-MAX-ATTEMPTS` | `3` | attempts in total, counting the first; `1` disables retrying | +| `CFG-BACKOFF-BASE-MS` | `500` | base of the exponential wait | +| `CFG-BACKOFF-MAX-MS` | `8000` | cap on each wait, `Retry-After` included | +| `CFG-TOTAL-TIMEOUT-MS` | `0` | cap on the whole call, attempts and waits included; `0` means none | +| `CFG-USER-AGENT` | `cepis-cobol/0.1.0 (+https://cep.is)` | identifies the library and version | + +Retries cover 429, 5xx and transport failures, with exponential backoff and +full jitter; 400 and 404 are never retried. + +`CFG-BASE-URL` is the only field that usually changes between environments. +The API repo brings a local instance up with `just up`: + +```cobol +MOVE "http://localhost:8080" TO CFG-BASE-URL +``` + +## Errors + +`CEPIS-STATUS` carries the outcome. Test `CEPIS-OK` first; `CEPIS-FAILED` +covers every failure at once, and `CEPIS-RETRIABLE` the three the client +already retried on its own before giving up. + +| condition | when | does retrying help? | +| --- | --- | --- | +| `CEPIS-INVALID-CEP` | not 8 digits | no | +| `CEPIS-NOT-FOUND` | well-formed, not in the database | no | +| `CEPIS-RATE-LIMITED` | 429; see `CEPIS-STATUS-RETRY-AFTER-MS` | yes, later | +| `CEPIS-SERVER-ERROR` | 5xx; see `CEPIS-STATUS-HTTP` | yes, later | +| `CEPIS-NETWORK-ERROR` | DNS, connection, timeout, TLS | yes, later | +| `CEPIS-INVALID-RESPONSE` | success status, unintelligible body | probably not | + +```cobol +EVALUATE TRUE + WHEN CEPIS-OK PERFORM USE-THE-ADDRESS + WHEN CEPIS-NOT-FOUND PERFORM ASK-FOR-ANOTHER-CEP + WHEN CEPIS-RETRIABLE PERFORM RESCHEDULE + WHEN OTHER DISPLAY CEPIS-STATUS-MESSAGE +END-EVALUATE +``` + +`CEPIS-STATUS-MESSAGE` holds a description, `CEPIS-STATUS-HTTP` the last +status seen and `CEPIS-STATUS-ATTEMPTS` how many attempts were made. + +`CEPIS-INVALID-RESPONSE` 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. + +## Field widths + +COBOL records are fixed width. A value longer than its field is truncated +on the right and `ADDR-TRUNCATED` (or `BAT-TRUNCATED`, per slot) is set, so +truncation is visible rather than silent. Lengths are in bytes and the data +is UTF-8, so an accented character costs two. + +| field | bytes | | field | bytes | +| --- | --- | --- | --- | --- | +| `cep` | 8 | | `uf` | 2 | +| `cep-masked` | 9 | | `estado` | 60 | +| `logradouro` | 120 | | `regiao` | 30 | +| `complemento` | 120 | | `ibge` | 10 | +| `unidade` | 80 | | `gia` | 10 | +| `bairro` | 100 | | `ddd` | 4 | +| `localidade` | 100 | | `siafi` | 10 | + +A response body larger than 256 KiB is reported as +`CEPIS-INVALID-RESPONSE`; 500 CSV rows come to roughly 60 KiB. + +## Limitations + +- **One call at a time per process.** A GnuCOBOL subprogram's + `WORKING-STORAGE` is static, so the subprograms here are not reentrant. +- The raw response body is not kept. Fields the client does not know are + ignored without error, but they are not readable afterwards. +- `CFG-TIMEOUT-MS` maps onto libcurl's connect and transfer timeouts, not + onto each individual socket read. + +## Development + +```sh +make # list the targets +make build # compile the library into build/libcepis.a +make test # run the suite against a local fake API +make lint # syntax and warning check, writes nothing +make check # lint, then test, then build +make clean # remove build artefacts +``` + +`make test` stands a fake cep.is up on a loopback port with +`tests/server.py`, points `CFG-BASE-URL` at it and runs the driver in +`tests/cepis-tests.cob`. No test touches the network. The run exits +non-zero if any check fails. + +COBOL has no standard formatter, so there is no `format` target; `lint` +compiles every unit with `-Wall -fsyntax-only`. + +## Licence + +ISC. See [LICENSE](LICENSE). |