#ifndef CEPIS_CLIENT_HPP #define CEPIS_CLIENT_HPP #include #include #include #include #include #include #include "cepis/errors.hpp" #include "cepis/json.hpp" #include "cepis/models.hpp" namespace cepis { /// The library version, as it appears in the default `User-Agent`. inline constexpr std::string_view kVersion = "0.1.0"; /// Where the public API lives. inline constexpr std::string_view kDefaultBaseUrl = "https://cep.is"; /// The API's ceiling on one batch request. `Client::lookup_many` chunks at /// this size on its own; you do not need to. inline constexpr std::size_t kMaxBatch = 500; /// Returns the CEP's eight digits, accepting the mask. /// /// `"01001-000"`, `"01001000"` and `" 01001-000 "` all become `"01001000"`. /// /// Throws `InvalidCep` unless exactly eight digits remain. This runs **before** /// any request: a seven-digit CEP is the caller's bug, and spending a round /// trip for the server to say so wastes both sides' time. std::string normalize_cep(std::string_view value); /// The default `User-Agent`, e.g. `cepis-cpp/0.1.0 (+https://cep.is)`. std::string default_user_agent(); /// How a `Client` behaves. Every field has a working default; in practice only /// `base_url` changes between environments. struct Config { /// Where the API lives. Point it at a local instance (the API repo brings /// one up on `http://localhost:8080` with `just up`) or at a test server. A /// trailing slash is stripped, so it cannot produce doubled separators that /// some proxies normalise and others 404. std::string base_url{kDefaultBaseUrl}; /// Cap on one attempt — connection plus transfer. libcurl works on whole /// transfers rather than per socket operation, so this is not a per-read /// timeout; the cap on the whole call, retries and waits included, is /// `total_timeout`. std::chrono::milliseconds timeout{10000}; /// How many attempts in total, counting the first. `1` disables retrying. /// The default of `3` covers the common case (one transient failure) /// without turning a real outage into a long wait. int max_attempts = 3; /// Base of the exponential wait. The actual wait is drawn from /// `[0, base * 2^(n-1)]` — *full jitter*. The randomness is not decoration: /// without it, N clients that fail together come back together and reproduce /// the spike that took the service down. std::chrono::milliseconds backoff_base{500}; /// Cap on each individual wait. It applies to a server-sent `Retry-After` /// too, or a `Retry-After: 3600` from a misconfigured proxy would park the /// thread for an hour. std::chrono::milliseconds backoff_max{8000}; /// Cap on the whole call, attempts and waits included. Unset leaves /// `max_attempts` as the only bound. Set it in code serving a web request: /// without it the worst case is `max_attempts * timeout` plus the sum of the /// backoffs. std::optional total_timeout; /// Overrides the `User-Agent`. Empty means `default_user_agent()`, which /// identifies the library and its version — that is what lets the server /// diagnose an old client misbehaving. std::string user_agent; }; /// Client for the cep.is API. /// /// Reuse one instance: the configuration is fixed at construction and no method /// keeps state between calls, so a `const Client&` is safe to share across /// threads. Each request owns its own connection. /// /// ```cpp /// cepis::Config config; /// config.base_url = "http://localhost:8080"; /// const cepis::Client client(config); /// const cepis::Address a = client.lookup("01001-000"); /// ``` class Client { public: /// Builds a client. Throws `std::invalid_argument` for a `max_attempts` /// below 1 or a non-positive `timeout`. explicit Client(Config config = {}); /// The configuration in use, with `base_url` and `user_agent` normalised. [[nodiscard]] const Config& config() const noexcept { return config_; } /// Looks up a single CEP. /// /// Throws `InvalidCep` when the CEP is not eight digits (no request is /// made) or when the API answers 400; `CepNotFound` when it is well-formed /// but absent; `RateLimited` on 429 after every attempt; `ServerError` on /// 5xx after every attempt; `NetworkError` when the request never /// completed; `InvalidResponse` when a success status carries a body that /// is not a JSON object. [[nodiscard]] Address lookup(std::string_view cep) const; /// Looks up many CEPs in one batch. /// /// Returns a vector of **the same size and in the same order** as the input. /// A CEP that does not exist is empty at its position — it does not vanish /// from the vector, and it does not throw. That is the right difference from /// `lookup`: in a batch of a thousand, one bad CEP must not cost the other /// nine hundred and ninety-nine. /// /// A malformed CEP is likewise empty rather than throwing `InvalidCep`. If /// you need to know *which* one was malformed, validate first with /// `normalize_cep`. /// /// Input is chunked into blocks of `kMaxBatch` automatically. /// /// **Warning: the batch carries no coordinate.** The endpoint behind it is /// CSV, and the CSV has no `geo` column. Every `Address` returned here has /// an empty `geo`, including for CEPs that do have one. Need `geo`? Use /// `lookup`, one at a time. [[nodiscard]] std::vector> lookup_many( const std::vector& ceps) const; /// Queries `GET /health`: does the API answer, and with which database. /// /// Does not throw for an API reporting itself degraded — that arrives in /// `Health::status`, and checking health is exactly when you want the /// diagnosis rather than an exception. Network failures still throw. [[nodiscard]] Health health() const; private: struct Response { int status = 0; std::string body; std::optional retry_after; }; [[nodiscard]] Response request(std::string_view path, const std::vector& accepted_status) const; [[nodiscard]] std::vector> lookup_chunk(const std::string* first, std::size_t count) const; [[nodiscard]] std::chrono::milliseconds backoff(int attempt, std::optional retry_after) const; Config config_; }; /// Looks a CEP up with the default configuration. /// /// The free functions share one process-wide `Client`. It is built once /// because `Client` keeps no state between calls and is safe to share. Anyone /// needing different settings should build their own. Address lookup(std::string_view cep); /// Looks many CEPs up with the default configuration. Read `Client::lookup_many` /// first, especially the warning that batches carry no coordinate. std::vector> lookup_many(const std::vector& ceps); /// Queries the API's health with the default configuration. Health health(); } // namespace cepis #endif // CEPIS_CLIENT_HPP