summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:52:43 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:52:43 -0300
commit42d8c12d2bd88462df8c3aac9449abbf98400084 (patch)
tree564b07e68aed22c3aa4a066b822d731328fb4e97 /README.md
downloadfortran-42d8c12d2bd88462df8c3aac9449abbf98400084.tar.gz
Initial import
Diffstat (limited to 'README.md')
-rw-r--r--README.md259
1 files changed, 259 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6ccea02
--- /dev/null
+++ b/README.md
@@ -0,0 +1,259 @@
+# cepis
+
+Fortran client for the [public CEP API at cep.is](https://cep.is). Official
+Correios data (DNE), refreshed every two weeks.
+
+```fortran
+program demo
+ use cepis
+ implicit none
+
+ type(cepis_address) :: address
+ character(len=:), allocatable :: message
+ integer :: status
+
+ call cepis_lookup('01001-000', address, status, message)
+ if (status == CEPIS_OK) then
+ print *, address%logradouro, address%localidade, address%uf
+ end if
+end program demo
+```
+
+## Installation
+
+There is no package manager here. Build the static library and link it:
+
+```sh
+git clone https://git.cep.is/fortran.git cepis-fortran
+cd cepis-fortran
+make build
+sudo make install # modules under $PREFIX/include/cepis, libcepis.a under $PREFIX/lib
+```
+
+```sh
+gfortran -I/usr/local/include/cepis demo.f90 -L/usr/local/lib -lcepis -lcurl -o demo
+```
+
+Requires **Fortran 2018** (gfortran 9 or newer), a C compiler, and **libcurl
+with its development headers** — `libcurl4-openssl-dev` on Debian and Ubuntu,
+`curl-devel` on Fedora, `curl` on Alpine and OpenBSD. Fortran has no HTTP
+client and no sockets, so the transport is libcurl bound through
+`iso_c_binding`; the interface blocks are written by hand in
+`src/cepis_http.f90` rather than vendoring a third-party Fortran-curl wrapper,
+because the twelve entry points this library needs are a fraction of what such
+a wrapper brings, and one file with no dependency is easier to audit than one
+more dependency.
+
+## Usage
+
+`use cepis` brings in everything. Every fallible call returns an `integer`
+status and an allocatable `message`; nothing stops the program.
+
+### A single lookup
+
+`lookup` accepts the CEP masked or unmasked:
+
+```fortran
+type(cepis_address) :: a
+character(len=:), allocatable :: message
+integer :: status
+
+call cepis_lookup('01310100', a, status, message)
+
+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.
+Every character component is allocated after a successful call, so none of them
+needs an `allocated` check.
+
+### Coordinates
+
+About 57% of CEPs have a coordinate. `has_geo` says whether this one does:
+
+```fortran
+if (a%has_geo) then
+ print *, a%geo%lat, a%geo%lon ! -23.567202 -46.649659
+ print *, a%geo%raio_m ! 1084
+ print *, a%geo%precisao ! 'logradouro'
+end if
+```
+
+**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 |
+
+`a%geo%is_precise()` is the shorthand for `precisao == 'logradouro'`:
+
+```fortran
+if (a%has_geo .and. a%geo%is_precise()) then
+ call shipping_from(a%geo%lat, a%geo%lon)
+else
+ call shipping_from_municipality(a%ibge)
+end if
+```
+
+### Many CEPs at once
+
+Do not loop over `lookup`. Use `lookup_many`, which resolves up to 500 CEPs per
+request:
+
+```fortran
+type(cepis_address), allocatable :: found(:)
+character(len=8) :: ceps(3) = ['01001000', '99999999', '20040020']
+integer :: i
+
+call cepis_lookup_many(ceps, found, status, message)
+
+do i = 1, size(found)
+ if (found(i)%found) then
+ print *, ceps(i), found(i)%localidade
+ else
+ print *, ceps(i), 'not found'
+ end if
+end do
+```
+
+The result has **the same length and the same order** as the input. A CEP that
+does not exist, or is malformed, has `found = .false.` at its position — it does
+not vanish, and it is not an error. Trailing blanks in each element are ignored,
+so a fixed-length array literal works as written.
+
+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 address from `lookup_many` has `has_geo = .false.`,
+> 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:
+
+```fortran
+type(cepis_health) :: h
+
+call cepis_check_health(h, status, message)
+
+h%ok() ! .true.
+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 is not an error — that arrives in `h%status`.
+
+### From the command line
+
+`make build` also produces `build/cepis`:
+
+```sh
+build/cepis 01001000
+build/cepis --base-url http://localhost:8080 01001000 20040020
+build/cepis --health
+```
+
+## Configuration
+
+For anything beyond the defaults, build a `cepis_client` and reuse it. It keeps
+no state between calls:
+
+```fortran
+type(cepis_client) :: client
+
+call cepis_client_init(client, status, message, &
+ base_url='https://cep.is', & ! point at another instance
+ timeout=10.0d0, & ! seconds per socket operation
+ max_attempts=3, & ! counting the first; 1 disables retrying
+ backoff_base=0.5d0, & ! base of the exponential wait
+ backoff_max=8.0d0, & ! cap on each wait, Retry-After included
+ total_timeout=0.0d0) ! cap on the whole call; 0 means none
+
+call client%lookup('01001000', address, status, message)
+```
+
+Every argument after `message` is optional. Retries cover 429, 5xx and network
+failures, with exponential backoff and jitter; 400 and 404 are never retried.
+
+`lookup`, `lookup_many` and `health` also accept two optional `intent(out)`
+arguments, `http_status` and `retry_after`, for callers that want to reschedule
+rate-limited work.
+
+The module-level `cepis_lookup`, `cepis_lookup_many` and `cepis_check_health`
+use one shared default client pointed at `https://cep.is`. It is not safe to use
+from two threads at once; build your own client per thread if you need that.
+
+### 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`:
+
+```fortran
+call cepis_client_init(client, status, message, base_url='http://localhost:8080')
+```
+
+## Errors
+
+`status` is one of these. The split is by **the decision you have to make**, not
+by the HTTP status that caused it:
+
+| status | when | does retrying help? |
+| --- | --- | --- |
+| `CEPIS_OK` | the call succeeded | — |
+| `CEPIS_ERR_INVALID_CEP` | not 8 digits | no |
+| `CEPIS_ERR_NOT_FOUND` | well-formed, not in the database | no |
+| `CEPIS_ERR_RATE_LIMITED` | 429 | yes, later |
+| `CEPIS_ERR_SERVER` | 5xx | yes, later |
+| `CEPIS_ERR_NETWORK` | DNS, connection, timeout, TLS | yes, later |
+| `CEPIS_ERR_INVALID_RESPONSE` | unintelligible response | probably not |
+| `CEPIS_ERR_CONFIG` | a configuration value is out of range | no |
+
+```fortran
+call cepis_lookup(cep_from_form, address, status, message)
+select case (status)
+case (CEPIS_OK)
+ call use_it(address)
+case (CEPIS_ERR_NOT_FOUND, CEPIS_ERR_INVALID_CEP)
+ call ask_again()
+case default
+ call reschedule() ! transient failure
+end select
+```
+
+`cepis_status_name(status)` gives a short, stable English name for logging.
+
+`CEPIS_ERR_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.
+
+## Development
+
+```sh
+make # list the targets
+make test # build and run the suite against a local HTTP server
+make lint # warning-free rebuild + fprettify --diff, touches nothing
+make format # apply fprettify (writes)
+make build # libcepis.a, the modules and the cepis CLI
+make check # lint, then test, then build
+make sanitize # rebuild and run the suite under ASan + UBSan
+```
+
+The suite starts a real HTTP server on a loopback port (`test/http_server.c`,
+compiled with the tests) and points `base_url` at it, so nothing is mocked.
+`make lint` and `make format` need [fprettify]; `lint` skips the style check
+when it is not installed.
+
+[fprettify]: https://github.com/fortran-lang/fprettify
+
+## Licence
+
+ISC. See [LICENSE](LICENSE).