diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:56:51 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:56:59 -0300 |
| commit | b7b08a7d29d84de187dd8a55e0c94de66b07a572 (patch) | |
| tree | 7e55661d3eca3cf3b16aada6e87eabee68802fb9 /include/cepis.h | |
| download | asm-b7b08a7d29d84de187dd8a55e0c94de66b07a572.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'include/cepis.h')
| -rw-r--r-- | include/cepis.h | 334 |
1 files changed, 334 insertions, 0 deletions
diff --git a/include/cepis.h b/include/cepis.h new file mode 100644 index 0000000..ea8d378 --- /dev/null +++ b/include/cepis.h @@ -0,0 +1,334 @@ +/* + * cepis - client for the public CEP API at https://cep.is + * + * Copyright (c) 2026 Murilo Ijanc <[email protected]> + * SPDX-License-Identifier: ISC + * + * The whole implementation is hand-written x86-64 assembly (GNU as, AT&T + * syntax); this header is the only C in the library. Every entry point below + * follows the System V AMD64 ABI. + */ + +#ifndef CEPIS_H +#define CEPIS_H + +#include <stddef.h> + +#ifdef __cplusplus +extern "C" { +#endif + +#define CEPIS_VERSION "0.1.0" + +/** Largest number of CEPs the API accepts in one /csv request. */ +#define CEPIS_MAX_BATCH 500 + +/** Size of cepis_error::message, including the terminating NUL. */ +#define CEPIS_MESSAGE_MAX 256 + +/** + * Outcome of a call. Split by the decision the caller has to make, not by the + * HTTP status that caused it: CEPIS_ERR_INVALID_CEP and CEPIS_ERR_NOT_FOUND + * are problems with the request and retrying is pointless, while + * CEPIS_ERR_RATE_LIMITED, CEPIS_ERR_SERVER and CEPIS_ERR_NETWORK are problems + * with transport that may succeed later. The client has already retried those + * three internally before returning them. + */ +typedef enum { + CEPIS_OK = 0, + /** The CEP is not 8 digits. Detected locally; no request was made. */ + CEPIS_ERR_INVALID_CEP = 1, + /** Well-formed, but absent from the DNE database (HTTP 404). */ + CEPIS_ERR_NOT_FOUND = 2, + /** HTTP 429 on every attempt. See cepis_error::retry_after. */ + CEPIS_ERR_RATE_LIMITED = 3, + /** HTTP 5xx on every attempt. See cepis_error::http_status. */ + CEPIS_ERR_SERVER = 4, + /** The request never completed: DNS, connection, timeout, TLS. */ + CEPIS_ERR_NETWORK = 5, + /** A success status came back with a body that cannot be parsed. */ + CEPIS_ERR_INVALID_RESPONSE = 6, + /** An allocation failed. */ + CEPIS_ERR_NOMEM = 7, + /** A required out-parameter was NULL, or a configuration value was + * nonsensical (a negative max_attempts). */ + CEPIS_ERR_USAGE = 8 +} cepis_status; + +/** + * The CEP's coordinate, when the database has one. + * + * The point is the median of the coordinates of that CEP's addresses, not the + * mean: one badly geocoded address 40 km away shifts a mean and does not move + * a median. + * + * Always check `precisao` before using lat/lon for distance: + * + * - "logradouro": the point is a stretch of street, `raio_m` around + * 100-150 m. Good for maps and for distance. + * - "localidade": the CEP *is* a whole municipality and the point is its + * centroid, `raio_m` from 7 km to 40 km. Computing shipping from this is + * wrong by tens of kilometres. + * + * `raio_m` is the radius containing 90% of that CEP's addresses. It exists + * precisely so that error cannot pass unnoticed. + */ +typedef struct cepis_geo { + double lat; + double lon; + long raio_m; + /** "logradouro" or "localidade". Never NULL. */ + char *precisao; +} cepis_geo; + +/** + * A CEP and everything the database knows about it. + * + * Field names are the API's own, in Portuguese, so that what you read in the + * JSON at https://cep.is/01001000 is exactly what you access here. + * + * Every char* field is non-NULL. A field the database lacks for that CEP is + * the empty string, never NULL, matching what the API returns. + * + * `geo` is the exception: NULL when there is no coordinate (about 43% of + * CEPs), because the API omits the whole object in that case rather than + * sending null. It is also always NULL for addresses from + * cepis_lookup_many() - see that function. + * + * `raw` holds the response body exactly as it arrived, so a field the API + * grows before this library is updated is not lost. It is NULL for addresses + * produced by cepis_lookup_many(). + * + * Free with cepis_address_free(). + */ +typedef struct cepis_address { + char *cep; + char *logradouro; + char *complemento; + char *unidade; + char *bairro; + char *localidade; + char *uf; + char *estado; + char *regiao; + char *ibge; + char *gia; + char *ddd; + char *siafi; + cepis_geo *geo; + char *raw; +} cepis_address; + +/** + * The response of GET /health. + * + * This one is a bare struct tag, not a typedef, because cepis_health() is the + * function that fills it - the same split POSIX makes between struct stat and + * stat(). Write struct cepis_health. + * + * Good for two things: checking that the API answers, and knowing which + * version of the database is live. The DNE database changes every two weeks; + * `dne` identifies it. An API reporting itself degraded is not an error - that + * arrives in `status`. + * + * Free with cepis_health_free(). + */ +struct cepis_health { + /** "ok" when the API reports itself healthy. Never NULL. */ + char *status; + char *dne; + long long ceps; + long long geo; + char *base_gerada_em; + /** The response body as it arrived. Never NULL. */ + char *raw; +}; + +/** + * Detail about a failure. Optional: pass NULL to any call that takes one. + * + * Caller-allocated, no ownership: declare one on the stack and reuse it. It is + * fully overwritten at the start of every call that receives it. + */ +typedef struct cepis_error { + /** Same value the call returned. */ + int status; + /** The HTTP status for CEPIS_ERR_SERVER and CEPIS_ERR_RATE_LIMITED, + * else 0. */ + int http_status; + /** Seconds the server asked for via Retry-After, or 0 if it did not say. + * Only meaningful for CEPIS_ERR_RATE_LIMITED. */ + double retry_after; + /** Human-readable description, NUL-terminated, empty on success. */ + char message[CEPIS_MESSAGE_MAX]; +} cepis_error; + +/** + * Client configuration. Zero-initialise it and set only what you need: every + * field left at 0 (or NULL) takes its default. + */ +typedef struct cepis_config { + /** Where the API lives. Default "https://cep.is". A trailing slash is + * stripped. This is the only field that usually changes between + * environments - point it at a local instance to test. */ + const char *base_url; + /** Seconds allowed for one attempt, connect and transfer together. + * Default 10.0. libcurl has no per-socket-operation timeout, so unlike + * the Python client this is a per-attempt cap; `total_timeout` caps the + * whole call including retries and waits. */ + double timeout; + /** Attempts in total, counting the first. Default 3; 1 disables retrying. + * Negative is rejected with CEPIS_ERR_USAGE. */ + int max_attempts; + /** Base of the exponential wait, in seconds. Default 0.5. The actual wait + * is drawn from [0, base * 2^(n-1)] - full jitter. Without the + * randomness, N clients that fail together come back together. */ + double backoff_base; + /** Cap on each individual wait, in seconds. Default 8.0. It applies to a + * server-sent Retry-After too, so a misconfigured proxy answering + * "Retry-After: 3600" cannot park the caller for an hour. */ + double backoff_max; + /** Cap on the whole call, attempts and waits included, in seconds. + * 0 leaves max_attempts as the only bound. */ + double total_timeout; + /** Overrides the User-Agent. Default + * "cepis-asm/0.1.0 (+https://cep.is)". */ + const char *user_agent; +} cepis_config; + +/** Opaque client. Create with cepis_client_new(), destroy with + * cepis_client_free(). The configuration is immutable and no call keeps state + * between requests, so one instance is safe to share across threads. */ +typedef struct cepis_client cepis_client; + +/** Library version, "0.1.0". Static storage; do not free. */ +const char *cepis_version(void); + +/** One-line description of a cepis_status. Static storage; do not free. */ +const char *cepis_strerror(int status); + +/** + * Build a client. `cfg` may be NULL for an all-defaults client. + * + * On CEPIS_OK, *out owns a client the caller must release with + * cepis_client_free(). On failure *out is NULL. + */ +cepis_status cepis_client_new(const cepis_config *cfg, cepis_client **out); + +/** Release a client. NULL is accepted. */ +void cepis_client_free(cepis_client *client); + +/** + * The process-wide default client, created on first use with all defaults. + * This is what the lookup calls use when passed a NULL client. Returns NULL + * only if it could not be allocated. Owned by the library; do not free it - + * cepis_global_cleanup() releases it. + */ +cepis_client *cepis_default_client(void); + +/** + * Initialise libcurl's global state. Optional: the first request does it + * anyway. Call it once from the main thread before starting other threads if + * your program is multi-threaded, since the lazy path races. + */ +void cepis_global_init(void); + +/** + * Release libcurl's global state and the default client. Call once at exit, + * after the last cepis call and with no other thread in the library. Not + * required for correctness, but it makes leak checkers quiet. + */ +void cepis_global_cleanup(void); + +/** + * Return the CEP's 8 digits, accepting the mask. + * + * "01001-000", "01001000" and " 01001-000 " all become "01001000"; the + * separators '-', '.' and whitespace are ignored wherever they appear. + * + * `out` must have room for 9 bytes and receives a NUL-terminated string, or + * "" on failure. Returns CEPIS_ERR_INVALID_CEP unless exactly 8 digits + * remain, or CEPIS_ERR_USAGE if `out` is NULL. + */ +cepis_status cepis_normalize_cep(const char *value, char *out); + +/** + * Look up a single CEP. `cep` may be masked or unmasked. + * + * `client` may be NULL to use the default client. `err` may be NULL. + * + * On CEPIS_OK, *out owns an address the caller must release with + * cepis_address_free(). On failure *out is NULL. + * + * The CEP is validated locally first: a 7-digit CEP returns + * CEPIS_ERR_INVALID_CEP without a request being made. A 400 from the API is + * also CEPIS_ERR_INVALID_CEP, carrying the API's own message. A 404 is + * CEPIS_ERR_NOT_FOUND. Neither is retried; 429, 5xx and network failures are. + */ +cepis_status cepis_lookup(cepis_client *client, const char *cep, + cepis_address **out, cepis_error *err); + +/** + * Look up many CEPs, up to CEPIS_MAX_BATCH per request. Do not loop over + * cepis_lookup(): 500 calls pay 500 TLS handshakes, the batch pays one. + * + * On CEPIS_OK, *out owns an array of exactly `n` pointers, in the same order + * as `ceps`. A CEP that does not exist, or that is malformed, is NULL at its + * position - it does not vanish from the array and it is not an error. If you + * need to know which one was malformed, validate first with + * cepis_normalize_cep(). Release the array with + * cepis_address_list_free(arr, n). + * + * Input longer than CEPIS_MAX_BATCH is chunked automatically. + * + * WARNING: the batch carries no coordinate. The endpoint behind it is CSV and + * the CSV has no geo column, so every address returned here has geo == NULL, + * including for CEPs that do have one. Need geo? Use cepis_lookup(), one at a + * time. `raw` is NULL here for the same reason. + * + * `n` may be 0, in which case *out is a valid empty array. + */ +cepis_status cepis_lookup_many(cepis_client *client, const char *const *ceps, + size_t n, cepis_address ***out, + cepis_error *err); + +/** + * Query GET /health. + * + * On CEPIS_OK, *out owns a cepis_health the caller must release with + * cepis_health_free(). An API reporting itself degraded (HTTP 503 with a body) + * is still CEPIS_OK - the diagnosis arrives in cepis_health::status, which is + * exactly what you want when the thing you are doing is checking health. + * Network failures still fail. + */ +cepis_status cepis_health(cepis_client *client, struct cepis_health **out, + cepis_error *err); + +/** Release an address. NULL is accepted. */ +void cepis_address_free(cepis_address *address); + +/** Release an array of `n` addresses and the array itself, skipping NULL + * entries. NULL is accepted. */ +void cepis_address_list_free(cepis_address **addresses, size_t n); + +/** Release a cepis_health. NULL is accepted. */ +void cepis_health_free(struct cepis_health *health); + +/** + * Write the CEP with its mask, "01001-000", into `out`, which must have room + * for 10 bytes. The API always returns it unmasked; this is presentation. + */ +void cepis_address_formatted_cep(const cepis_address *address, char *out); + +/** + * Non-zero when the coordinate points at a street rather than a municipality, + * i.e. strcmp(geo->precisao, "logradouro") == 0. This is the check to make + * before using the point to measure distance. NULL returns 0. + */ +int cepis_geo_is_precise(const cepis_geo *geo); + +#ifdef __cplusplus +} +#endif + +#endif /* CEPIS_H */ |