summaryrefslogtreecommitdiffstats
path: root/include/cepis.h
blob: a00982abe4ca32db876e4eccfdcd12ee85cbc4c9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
/*
 * 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.2.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

/** Size of cepis_faixa::localidade, including the terminating NUL. The
 *  longest municipality name in the IBGE list is 32 characters; this leaves
 *  room for it in UTF-8 twice over. A longer name is truncated, never
 *  overflowed. */
#define CEPIS_FAIXA_LOCALIDADE_MAX 72

/**
 * 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;
};

/**
 * The municipality a CEP that does not exist would have belonged to.
 *
 * A CEP that fails is rarely a random number: it is usually someone who knows
 * the city and guessed the last digits, and the classic guess is the `-000`
 * that 957 municipalities never received. So the API answers a 404 with the
 * band the number falls in, when a municipality owns that band. This is
 * context for an error and **not an address**: the lookup still failed, and
 * cepis_lookup() still returns CEPIS_ERR_NOT_FOUND.
 *
 * Everything here is inline storage inside cepis_error - nothing to free,
 * nothing that can be NULL. Read it only when `present` is non-zero; when it
 * is zero the strings are `""` and `ceps` is 0.
 *
 * `cep_ini` and `cep_fim` are the **smallest and largest CEP that actually
 * exist** in that municipality, not the band the Correios publish. For Areal
 * the DNE's own band starts at 25845000 - exactly the CEP that just failed -
 * while `cep_ini` is 25845001.
 *
 * The band is sparse. Areal's 118 CEPs are scattered over the ~4900 numbers
 * between 25845001 and 25849899, so `ceps` is a count and the pair is a pair
 * of endpoints. Never present the two as a range of valid CEPs, and never
 * iterate it.
 */
typedef struct cepis_faixa {
    /** Non-zero when the API named a municipality for this CEP. Zero means it
     *  did not - the field is omitted, never null, for a number in a band no
     *  municipality owns. */
    int present;
    /** Municipality name, e.g. "Areal". Truncated at
     *  CEPIS_FAIXA_LOCALIDADE_MAX - 1 bytes. */
    char localidade[CEPIS_FAIXA_LOCALIDADE_MAX];
    /** Two-letter state, e.g. "RJ". */
    char uf[3];
    /** Smallest CEP that exists there, 8 digits unmasked. */
    char cep_ini[9];
    /** Largest CEP that exists there, 8 digits unmasked. */
    char cep_fim[9];
    /** How many CEPs exist there. Far smaller than cep_fim - cep_ini. */
    long long ceps;
} cepis_faixa;

/**
 * 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, `faixa`
 * included, so a band left over from a previous 404 cannot be mistaken for
 * this one's.
 */
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. On a 404
     *  with a `faixa` it names the municipality too. */
    char message[CEPIS_MESSAGE_MAX];
    /** Only ever filled by CEPIS_ERR_NOT_FOUND, and only when the API sent a
     *  band. See cepis_faixa. */
    cepis_faixa faixa;
} 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.2.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.2.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, and fills cepis_error::faixa when the API says which
 * municipality owns that band. 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 */