summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md264
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).