summaryrefslogtreecommitdiffstats
path: root/src/client.c
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:25:48 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:25:48 -0300
commite2c6ae13af335b311da2429c903032124f926df2 (patch)
tree73776fab7f3e52124a217e2768968fc9a8a76c34 /src/client.c
downloadc-e2c6ae13af335b311da2429c903032124f926df2.tar.gz
Initial importv0.1.0
Diffstat (limited to 'src/client.c')
-rw-r--r--src/client.c822
1 files changed, 822 insertions, 0 deletions
diff --git a/src/client.c b/src/client.c
new file mode 100644
index 0000000..58c55b3
--- /dev/null
+++ b/src/client.c
@@ -0,0 +1,822 @@
+#include "internal.h"
+
+#include <curl/curl.h>
+#include <errno.h>
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <time.h>
+
+#define CEPIS_ACCEPT_HEADER \
+ "Accept: application/json, text/csv;q=0.9, */*;q=0.1"
+
+static const long RETRIABLE_STATUS[] = {429, 500, 502, 503, 504};
+
+#define RETRIABLE_COUNT (sizeof(RETRIABLE_STATUS) / sizeof(RETRIABLE_STATUS[0]))
+
+struct cepis_client {
+ char *base_url;
+ char *user_agent;
+ double timeout;
+ double backoff_base;
+ double backoff_max;
+ double total_timeout;
+ int max_attempts;
+ uint64_t rng;
+ CURL *curl;
+ struct curl_slist *headers;
+};
+
+typedef struct {
+ long status;
+ double retry_after;
+ cepis_buf_t body;
+} cepis_response_t;
+
+const char *cepis_status_str(cepis_status_t status)
+{
+ switch (status) {
+ case CEPIS_OK:
+ return "ok";
+ case CEPIS_ERR_INVALID_CEP:
+ return "invalid CEP";
+ case CEPIS_ERR_NOT_FOUND:
+ return "CEP not found";
+ case CEPIS_ERR_RATE_LIMITED:
+ return "rate limited";
+ case CEPIS_ERR_SERVER:
+ return "server error";
+ case CEPIS_ERR_NETWORK:
+ return "network error";
+ case CEPIS_ERR_INVALID_RESPONSE:
+ return "invalid response";
+ case CEPIS_ERR_CONFIG:
+ return "invalid configuration";
+ case CEPIS_ERR_NOMEM:
+ return "out of memory";
+ }
+ return "unknown error";
+}
+
+cepis_status_t cepis_global_init(void)
+{
+ if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK)
+ return CEPIS_ERR_NETWORK;
+ return CEPIS_OK;
+}
+
+void cepis_global_cleanup(void)
+{
+ curl_global_cleanup();
+}
+
+cepis_status_t cepis_normalize_cep(const char *value,
+ char out[CEPIS_CEP_LEN + 1])
+{
+ char digits[CEPIS_CEP_LEN + 1];
+ size_t n = 0;
+ size_t i;
+
+ if (value == NULL || out == NULL)
+ return CEPIS_ERR_INVALID_CEP;
+
+ for (i = 0; value[i] != '\0'; i++) {
+ char c = value[i];
+ if (c == ' ' || c == '\t' || c == '\n' || c == '\r' ||
+ c == '-' || c == '.')
+ continue;
+ if (c < '0' || c > '9')
+ return CEPIS_ERR_INVALID_CEP;
+ if (n >= CEPIS_CEP_LEN)
+ return CEPIS_ERR_INVALID_CEP;
+ digits[n++] = c;
+ }
+ if (n != CEPIS_CEP_LEN)
+ return CEPIS_ERR_INVALID_CEP;
+
+ digits[CEPIS_CEP_LEN] = '\0';
+ memcpy(out, digits, CEPIS_CEP_LEN + 1);
+ return CEPIS_OK;
+}
+
+cepis_status_t cepis_format_cep(const char *cep, char out[CEPIS_CEP_LEN + 2])
+{
+ char digits[CEPIS_CEP_LEN + 1];
+ cepis_status_t status = cepis_normalize_cep(cep, digits);
+
+ if (status != CEPIS_OK)
+ return status;
+ memcpy(out, digits, 5);
+ out[5] = '-';
+ memcpy(out + 6, digits + 5, 3);
+ out[CEPIS_CEP_LEN + 1] = '\0';
+ return CEPIS_OK;
+}
+
+void cepis_config_init(cepis_config_t *config)
+{
+ if (config == NULL)
+ return;
+ config->base_url = CEPIS_DEFAULT_BASE_URL;
+ config->user_agent = NULL;
+ config->timeout = 10.0;
+ config->max_attempts = 3;
+ config->backoff_base = 0.5;
+ config->backoff_max = 8.0;
+ config->total_timeout = 0.0;
+}
+
+static char *trim_base_url(const char *base_url)
+{
+ size_t len = strlen(base_url);
+ char *copy;
+
+ while (len > 0 && base_url[len - 1] == '/')
+ len--;
+ copy = malloc(len + 1);
+ if (copy == NULL)
+ return NULL;
+ memcpy(copy, base_url, len);
+ copy[len] = '\0';
+ return copy;
+}
+
+cepis_status_t cepis_client_new(const cepis_config_t *config,
+ cepis_client_t **out, cepis_error_t *err)
+{
+ cepis_config_t defaults;
+ cepis_client_t *client;
+
+ if (out == NULL)
+ return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0,
+ "out must not be NULL");
+ if (config == NULL) {
+ cepis_config_init(&defaults);
+ config = &defaults;
+ }
+ if (config->base_url == NULL || config->base_url[0] == '\0')
+ return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0,
+ "base_url must not be empty");
+ if (config->max_attempts < 1)
+ return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0,
+ "max_attempts must be >= 1");
+ if (!(config->timeout > 0.0))
+ return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0,
+ "timeout must be > 0");
+ if (config->backoff_base < 0.0 || config->backoff_max < 0.0)
+ return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0,
+ "backoff values must be >= 0");
+ if (config->total_timeout < 0.0)
+ return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0,
+ "total_timeout must be >= 0");
+
+ if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK)
+ return cepis_set_error(err, CEPIS_ERR_NETWORK, 0, -1.0,
+ "could not initialise libcurl");
+
+ client = calloc(1, sizeof(*client));
+ if (client == NULL) {
+ curl_global_cleanup();
+ return cepis_set_error(err, CEPIS_ERR_NOMEM, 0, -1.0,
+ "out of memory");
+ }
+
+ client->timeout = config->timeout;
+ client->max_attempts = config->max_attempts;
+ client->backoff_base = config->backoff_base;
+ client->backoff_max = config->backoff_max;
+ client->total_timeout = config->total_timeout;
+ client->rng =
+ (uint64_t)time(NULL) ^ ((uint64_t)(uintptr_t)client << 16);
+ if (client->rng == 0)
+ client->rng = 0x9E3779B97F4A7C15ULL;
+
+ client->base_url = trim_base_url(config->base_url);
+ if (config->user_agent != NULL && config->user_agent[0] != '\0') {
+ client->user_agent = cepis_strdup(config->user_agent);
+ } else {
+ client->user_agent = cepis_strdup("cepis-c/" CEPIS_VERSION
+ " (+https://cep.is)");
+ }
+ client->curl = curl_easy_init();
+ client->headers = curl_slist_append(NULL, CEPIS_ACCEPT_HEADER);
+
+ if (client->base_url == NULL || client->user_agent == NULL ||
+ client->curl == NULL || client->headers == NULL) {
+ cepis_client_free(client);
+ return cepis_set_error(err, CEPIS_ERR_NOMEM, 0, -1.0,
+ "out of memory");
+ }
+
+ *out = client;
+ return CEPIS_OK;
+}
+
+void cepis_client_free(cepis_client_t *client)
+{
+ if (client == NULL)
+ return;
+ if (client->curl != NULL)
+ curl_easy_cleanup(client->curl);
+ if (client->headers != NULL)
+ curl_slist_free_all(client->headers);
+ free(client->base_url);
+ free(client->user_agent);
+ free(client);
+ curl_global_cleanup();
+}
+
+const char *cepis_client_base_url(const cepis_client_t *client)
+{
+ return client == NULL ? NULL : client->base_url;
+}
+
+const char *cepis_client_user_agent(const cepis_client_t *client)
+{
+ return client == NULL ? NULL : client->user_agent;
+}
+
+static double monotonic_now(void)
+{
+ struct timespec ts;
+
+ if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
+ return 0.0;
+ return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
+}
+
+static void sleep_seconds(double seconds)
+{
+ struct timespec request;
+
+ if (seconds <= 0.0)
+ return;
+ if (seconds > 3600.0)
+ seconds = 3600.0;
+ request.tv_sec = (time_t)seconds;
+ request.tv_nsec = (long)((seconds - (double)request.tv_sec) * 1e9);
+ if (request.tv_nsec < 0)
+ request.tv_nsec = 0;
+ if (request.tv_nsec > 999999999L)
+ request.tv_nsec = 999999999L;
+ while (nanosleep(&request, &request) == -1 && errno == EINTR)
+ ;
+}
+
+static double next_uniform(cepis_client_t *client)
+{
+ uint64_t x = client->rng;
+
+ x ^= x >> 12;
+ x ^= x << 25;
+ x ^= x >> 27;
+ client->rng = x;
+ x *= 0x2545F4914F6CDD1DULL;
+ return (double)(x >> 11) / 9007199254740992.0;
+}
+
+static double backoff_delay(cepis_client_t *client, int attempt,
+ double retry_after)
+{
+ double ceiling;
+
+ if (retry_after >= 0.0)
+ return retry_after < client->backoff_max ? retry_after
+ : client->backoff_max;
+ ceiling = client->backoff_base * pow(2.0, (double)(attempt - 1));
+ if (ceiling > client->backoff_max)
+ ceiling = client->backoff_max;
+ return next_uniform(client) * ceiling;
+}
+
+static double parse_retry_after(const char *value, size_t len)
+{
+ char text[128];
+ const char *formats[] = {"%a, %d %b %Y %H:%M:%S",
+ "%A, %d-%b-%y %H:%M:%S",
+ "%a %b %d %H:%M:%S %Y"};
+ char *stop = NULL;
+ double seconds;
+ size_t i;
+
+ while (len > 0 && (*value == ' ' || *value == '\t')) {
+ value++;
+ len--;
+ }
+ while (len > 0 && (value[len - 1] == ' ' || value[len - 1] == '\t' ||
+ value[len - 1] == '\r' || value[len - 1] == '\n'))
+ len--;
+ if (len == 0 || len >= sizeof(text))
+ return -1.0;
+ memcpy(text, value, len);
+ text[len] = '\0';
+
+ seconds = strtod(text, &stop);
+ if (stop != NULL && *stop == '\0' && stop != text)
+ return seconds < 0.0 ? 0.0 : seconds;
+
+ for (i = 0; i < sizeof(formats) / sizeof(formats[0]); i++) {
+ struct tm parsed;
+ time_t target;
+
+ memset(&parsed, 0, sizeof(parsed));
+ if (strptime(text, formats[i], &parsed) == NULL)
+ continue;
+ target = timegm(&parsed);
+ if (target == (time_t)-1)
+ continue;
+ seconds = difftime(target, time(NULL));
+ return seconds < 0.0 ? 0.0 : seconds;
+ }
+ return -1.0;
+}
+
+static size_t write_body(char *ptr, size_t size, size_t nmemb, void *userdata)
+{
+ cepis_buf_t *body = userdata;
+ size_t total = size * nmemb;
+
+ if (!cepis_buf_append(body, ptr, total))
+ return 0;
+ return total;
+}
+
+static size_t read_header(char *ptr, size_t size, size_t nmemb, void *userdata)
+{
+ cepis_response_t *response = userdata;
+ size_t total = size * nmemb;
+ static const char name[] = "Retry-After:";
+ size_t name_len = sizeof(name) - 1;
+
+ if (total > name_len && strncasecmp(ptr, name, name_len) == 0)
+ response->retry_after =
+ parse_retry_after(ptr + name_len, total - name_len);
+ return total;
+}
+
+static bool is_retriable(long status)
+{
+ size_t i;
+
+ for (i = 0; i < RETRIABLE_COUNT; i++)
+ if (RETRIABLE_STATUS[i] == status)
+ return true;
+ return false;
+}
+
+static bool is_accepted(long status, const long *accepted, size_t count)
+{
+ size_t i;
+
+ for (i = 0; i < count; i++)
+ if (accepted[i] == status)
+ return true;
+ return false;
+}
+
+static void apply_options(cepis_client_t *client, const char *url,
+ cepis_response_t *response, char *error_buffer,
+ double budget)
+{
+ CURL *curl = client->curl;
+ long low_speed_time = (long)ceil(client->timeout);
+
+ if (low_speed_time < 1)
+ low_speed_time = 1;
+
+ curl_easy_reset(curl);
+ curl_easy_setopt(curl, CURLOPT_URL, url);
+ curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
+ curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
+ curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
+ curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L);
+ curl_easy_setopt(curl, CURLOPT_USERAGENT, client->user_agent);
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, client->headers);
+ curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "identity");
+ curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS,
+ (long)(client->timeout * 1000.0));
+ curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1L);
+ curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, low_speed_time);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_body);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response->body);
+ curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, read_header);
+ curl_easy_setopt(curl, CURLOPT_HEADERDATA, response);
+ curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error_buffer);
+ if (budget > 0.0) {
+ long ms = (long)(budget * 1000.0);
+ curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, ms > 1 ? ms : 1L);
+ }
+}
+
+static cepis_status_t client_request(cepis_client_t *client, const char *url,
+ const long *accepted,
+ size_t accepted_count,
+ cepis_response_t *response,
+ cepis_error_t *err)
+{
+ char error_buffer[CURL_ERROR_SIZE];
+ double deadline = client->total_timeout > 0.0
+ ? monotonic_now() + client->total_timeout
+ : 0.0;
+ long last_status = 0;
+ double last_retry_after = -1.0;
+ int attempt;
+
+ error_buffer[0] = '\0';
+ response->status = 0;
+ response->retry_after = -1.0;
+ cepis_buf_init(&response->body);
+
+ for (attempt = 1; attempt <= client->max_attempts; attempt++) {
+ double budget = 0.0;
+ CURLcode code;
+ double wait;
+
+ if (deadline > 0.0) {
+ budget = deadline - monotonic_now();
+ if (budget <= 0.0)
+ break;
+ }
+
+ response->body.len = 0;
+ if (response->body.data != NULL)
+ response->body.data[0] = '\0';
+ response->retry_after = -1.0;
+ error_buffer[0] = '\0';
+
+ apply_options(client, url, response, error_buffer, budget);
+ code = curl_easy_perform(client->curl);
+
+ if (code == CURLE_OK) {
+ long status = 0;
+ curl_easy_getinfo(client->curl, CURLINFO_RESPONSE_CODE,
+ &status);
+ if (is_accepted(status, accepted, accepted_count) ||
+ !is_retriable(status)) {
+ response->status = status;
+ return CEPIS_OK;
+ }
+ last_status = status;
+ last_retry_after = response->retry_after;
+ } else {
+ last_status = 0;
+ last_retry_after = -1.0;
+ if (error_buffer[0] == '\0')
+ snprintf(error_buffer, sizeof(error_buffer),
+ "%s", curl_easy_strerror(code));
+ }
+
+ if (attempt == client->max_attempts)
+ break;
+
+ wait = backoff_delay(client, attempt, last_retry_after);
+ if (deadline > 0.0) {
+ double remaining = deadline - monotonic_now();
+ if (remaining <= 0.0)
+ break;
+ if (wait > remaining)
+ wait = remaining;
+ }
+ sleep_seconds(wait);
+ }
+
+ cepis_buf_free(&response->body);
+
+ if (last_status == 429)
+ return cepis_set_error(err, CEPIS_ERR_RATE_LIMITED, last_status,
+ last_retry_after,
+ "rate limit reached at %s (after %d "
+ "attempt(s))",
+ url, client->max_attempts);
+ if (last_status != 0)
+ return cepis_set_error(err, CEPIS_ERR_SERVER, last_status, -1.0,
+ "the API answered %ld at %s (after %d "
+ "attempt(s))",
+ last_status, url, client->max_attempts);
+ return cepis_set_error(err, CEPIS_ERR_NETWORK, 0, -1.0,
+ "could not reach %s (after %d attempt(s)): %s",
+ url, client->max_attempts,
+ error_buffer[0] != '\0' ? error_buffer
+ : "request aborted");
+}
+
+static void api_message(const cepis_buf_t *body, char *out, size_t out_size,
+ const char *fallback)
+{
+ json_value_t *root = json_parse(body->data, body->len);
+ const char *message = json_as_string(json_object_get(root, "mensagem"));
+
+ snprintf(out, out_size, "%s", message != NULL ? message : fallback);
+ json_free(root);
+}
+
+static cepis_status_t build_url(const cepis_client_t *client, const char *path,
+ cepis_buf_t *out)
+{
+ cepis_buf_init(out);
+ if (!cepis_buf_append_str(out, client->base_url) ||
+ !cepis_buf_append_str(out, path)) {
+ cepis_buf_free(out);
+ return CEPIS_ERR_NOMEM;
+ }
+ return CEPIS_OK;
+}
+
+cepis_status_t cepis_lookup(cepis_client_t *client, const char *cep,
+ cepis_address_t **out, cepis_error_t *err)
+{
+ char number[CEPIS_CEP_LEN + 1];
+ char path[CEPIS_CEP_LEN + 2];
+ char message[CEPIS_ERROR_MESSAGE_MAX];
+ char fallback[CEPIS_ERROR_MESSAGE_MAX];
+ static const long accepted[] = {200};
+ cepis_response_t response;
+ cepis_address_t *address;
+ cepis_buf_t url;
+ json_value_t *root;
+ cepis_status_t status;
+
+ if (client == NULL || out == NULL)
+ return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0,
+ "client and out must not be NULL");
+ if (cepis_normalize_cep(cep, number) != CEPIS_OK)
+ return cepis_set_error(err, CEPIS_ERR_INVALID_CEP, 0, -1.0,
+ "CEP must be 8 digits, got \"%s\"",
+ cep != NULL ? cep : "(null)");
+
+ snprintf(path, sizeof(path), "/%s", number);
+ status = build_url(client, path, &url);
+ if (status != CEPIS_OK)
+ return cepis_set_error(err, status, 0, -1.0, "out of memory");
+
+ status = client_request(client, url.data, accepted, 1, &response, err);
+ cepis_buf_free(&url);
+ if (status != CEPIS_OK)
+ return status;
+
+ if (response.status == 404) {
+ cepis_buf_free(&response.body);
+ return cepis_set_error(
+ err, CEPIS_ERR_NOT_FOUND, 404, -1.0,
+ "CEP %s does not exist in the DNE database", number);
+ }
+ if (response.status == 400) {
+ snprintf(fallback, sizeof(fallback), "CEP %s rejected", number);
+ api_message(&response.body, message, sizeof(message), fallback);
+ cepis_buf_free(&response.body);
+ return cepis_set_error(err, CEPIS_ERR_INVALID_CEP, 400, -1.0,
+ "%s", message);
+ }
+ if (response.status != 200) {
+ long seen = response.status;
+ cepis_buf_free(&response.body);
+ return cepis_set_error(err, CEPIS_ERR_INVALID_RESPONSE, seen,
+ -1.0, "the API answered %ld", seen);
+ }
+
+ root = json_parse(response.body.data, response.body.len);
+ if (root == NULL || root->type != JSON_OBJECT) {
+ json_free(root);
+ status = cepis_set_error(err, CEPIS_ERR_INVALID_RESPONSE, 200,
+ -1.0,
+ "response is not a JSON object - "
+ "something between you and "
+ "the API is answering instead of it");
+ cepis_buf_free(&response.body);
+ return status;
+ }
+
+ address = cepis_address_from_json(root, response.body.data,
+ response.body.len);
+ json_free(root);
+ cepis_buf_free(&response.body);
+ if (address == NULL)
+ return cepis_set_error(err, CEPIS_ERR_NOMEM, 0, -1.0,
+ "out of memory");
+
+ *out = address;
+ return CEPIS_OK;
+}
+
+cepis_status_t cepis_health(cepis_client_t *client, cepis_health_t **out,
+ cepis_error_t *err)
+{
+ static const long accepted[] = {200, 503};
+ cepis_response_t response;
+ cepis_health_t *health;
+ cepis_buf_t url;
+ json_value_t *root;
+ cepis_status_t status;
+
+ if (client == NULL || out == NULL)
+ return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0,
+ "client and out must not be NULL");
+
+ status = build_url(client, "/health", &url);
+ if (status != CEPIS_OK)
+ return cepis_set_error(err, status, 0, -1.0, "out of memory");
+
+ status = client_request(client, url.data, accepted, 2, &response, err);
+ cepis_buf_free(&url);
+ if (status != CEPIS_OK)
+ return status;
+
+ root = json_parse(response.body.data, response.body.len);
+ health = cepis_health_from_json(root);
+ json_free(root);
+ cepis_buf_free(&response.body);
+
+ if (health == NULL)
+ return cepis_set_error(err, CEPIS_ERR_INVALID_RESPONSE,
+ response.status, -1.0,
+ "health response is not a JSON object");
+
+ *out = health;
+ return CEPIS_OK;
+}
+
+static bool blank(const char *text)
+{
+ size_t i;
+
+ if (text == NULL)
+ return true;
+ for (i = 0; text[i] != '\0'; i++)
+ if (text[i] != ' ' && text[i] != '\t' && text[i] != '\r' &&
+ text[i] != '\n')
+ return false;
+ return true;
+}
+
+static size_t column_index(const csv_row_t *header, const char *name)
+{
+ size_t i;
+
+ for (i = 0; i < header->count; i++)
+ if (strcmp(header->fields[i], name) == 0)
+ return i;
+ return (size_t)-1;
+}
+
+static cepis_status_t chunk_url(cepis_client_t *client, const char *const *ceps,
+ size_t count, cepis_buf_t *out)
+{
+ cepis_buf_t joined;
+ char *escaped;
+ size_t i;
+
+ cepis_buf_init(&joined);
+ for (i = 0; i < count; i++) {
+ const char *value = ceps[i] != NULL ? ceps[i] : "";
+ if ((i > 0 && !cepis_buf_append_str(&joined, ",")) ||
+ !cepis_buf_append_str(&joined, value)) {
+ cepis_buf_free(&joined);
+ return CEPIS_ERR_NOMEM;
+ }
+ }
+
+ escaped = curl_easy_escape(client->curl, joined.data ? joined.data : "",
+ (int)joined.len);
+ cepis_buf_free(&joined);
+ if (escaped == NULL)
+ return CEPIS_ERR_NOMEM;
+
+ cepis_buf_init(out);
+ if (!cepis_buf_append_str(out, client->base_url) ||
+ !cepis_buf_append_str(out, "/csv?ceps=") ||
+ !cepis_buf_append_str(out, escaped)) {
+ cepis_buf_free(out);
+ curl_free(escaped);
+ return CEPIS_ERR_NOMEM;
+ }
+ curl_free(escaped);
+ return CEPIS_OK;
+}
+
+static cepis_status_t fill_chunk(cepis_client_t *client,
+ const char *const *ceps, size_t count,
+ cepis_address_t **slots, cepis_error_t *err)
+{
+ static const long accepted[] = {200};
+ char message[CEPIS_ERROR_MESSAGE_MAX];
+ cepis_response_t response;
+ csv_reader_t *reader;
+ csv_row_t header;
+ cepis_buf_t url;
+ size_t localidade;
+ size_t index;
+ cepis_status_t status;
+
+ status = chunk_url(client, ceps, count, &url);
+ if (status != CEPIS_OK)
+ return cepis_set_error(err, status, 0, -1.0, "out of memory");
+
+ status = client_request(client, url.data, accepted, 1, &response, err);
+ cepis_buf_free(&url);
+ if (status != CEPIS_OK)
+ return status;
+
+ if (response.status == 400) {
+ api_message(&response.body, message, sizeof(message),
+ "the batch was rejected");
+ cepis_buf_free(&response.body);
+ return cepis_set_error(err, CEPIS_ERR_INVALID_CEP, 400, -1.0,
+ "%s", message);
+ }
+ if (response.status != 200) {
+ long seen = response.status;
+ cepis_buf_free(&response.body);
+ return cepis_set_error(err, CEPIS_ERR_INVALID_RESPONSE, seen,
+ -1.0, "the API answered %ld", seen);
+ }
+
+ reader = csv_reader_new(response.body.data ? response.body.data : "",
+ response.body.len);
+ if (reader == NULL) {
+ cepis_buf_free(&response.body);
+ return cepis_set_error(err, CEPIS_ERR_NOMEM, 0, -1.0,
+ "out of memory");
+ }
+
+ if (csv_reader_next(reader, &header) != 1) {
+ csv_reader_free(reader);
+ cepis_buf_free(&response.body);
+ return cepis_set_error(err, CEPIS_ERR_INVALID_RESPONSE, 200,
+ -1.0,
+ "the batch response has no header");
+ }
+
+ localidade = column_index(&header, "localidade");
+ status = CEPIS_OK;
+
+ for (index = 0; index < count; index++) {
+ csv_row_t record;
+ int read = csv_reader_next(reader, &record);
+
+ if (read != 1)
+ break;
+ if (localidade != (size_t)-1 && localidade < record.count &&
+ !blank(record.fields[localidade])) {
+ slots[index] = cepis_address_from_csv(&header, &record);
+ if (slots[index] == NULL) {
+ csv_row_free(&record);
+ status =
+ cepis_set_error(err, CEPIS_ERR_NOMEM, 0,
+ -1.0, "out of memory");
+ break;
+ }
+ }
+ csv_row_free(&record);
+ }
+
+ csv_row_free(&header);
+ csv_reader_free(reader);
+ cepis_buf_free(&response.body);
+ return status;
+}
+
+cepis_status_t cepis_lookup_many(cepis_client_t *client,
+ const char *const *ceps, size_t count,
+ cepis_address_list_t *out, cepis_error_t *err)
+{
+ cepis_address_list_t list = {NULL, 0};
+ size_t start;
+
+ if (client == NULL || out == NULL)
+ return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0,
+ "client and out must not be NULL");
+ if (count == 0) {
+ out->items = NULL;
+ out->count = 0;
+ return CEPIS_OK;
+ }
+ if (ceps == NULL)
+ return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0,
+ "ceps must not be NULL");
+
+ list.items = calloc(count, sizeof(*list.items));
+ if (list.items == NULL)
+ return cepis_set_error(err, CEPIS_ERR_NOMEM, 0, -1.0,
+ "out of memory");
+ list.count = count;
+
+ for (start = 0; start < count; start += CEPIS_MAX_BATCH) {
+ size_t size = count - start;
+ cepis_status_t status;
+
+ if (size > CEPIS_MAX_BATCH)
+ size = CEPIS_MAX_BATCH;
+ status = fill_chunk(client, ceps + start, size,
+ list.items + start, err);
+ if (status != CEPIS_OK) {
+ cepis_address_list_free(&list);
+ return status;
+ }
+ }
+
+ *out = list;
+ return CEPIS_OK;
+}