summaryrefslogtreecommitdiffstats
path: root/include
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:56:51 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:56:59 -0300
commitb7b08a7d29d84de187dd8a55e0c94de66b07a572 (patch)
tree7e55661d3eca3cf3b16aada6e87eabee68802fb9 /include
downloadasm-0.1.0.tar.gz
Initial importv0.1.0
Diffstat (limited to 'include')
-rw-r--r--include/cepis.h334
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 */