summaryrefslogtreecommitdiffstats
path: root/include/cepis/client.hpp
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:28:10 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:28:10 -0300
commitb71faa6e95a0736f1ff171e006b171467b65db90 (patch)
tree7580c3ab515e8f50726dc68dbc240995ff4a1a4c /include/cepis/client.hpp
downloadcpp-b71faa6e95a0736f1ff171e006b171467b65db90.tar.gz
Initial importv0.1.0
Diffstat (limited to 'include/cepis/client.hpp')
-rw-r--r--include/cepis/client.hpp174
1 files changed, 174 insertions, 0 deletions
diff --git a/include/cepis/client.hpp b/include/cepis/client.hpp
new file mode 100644
index 0000000..205ba85
--- /dev/null
+++ b/include/cepis/client.hpp
@@ -0,0 +1,174 @@
+#ifndef CEPIS_CLIENT_HPP
+#define CEPIS_CLIENT_HPP
+
+#include <chrono>
+#include <cstddef>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#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<std::chrono::milliseconds> 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<std::optional<Address>> lookup_many(
+ const std::vector<std::string>& 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<double> retry_after;
+ };
+
+ [[nodiscard]] Response request(std::string_view path,
+ const std::vector<int>& accepted_status) const;
+ [[nodiscard]] std::vector<std::optional<Address>> lookup_chunk(const std::string* first,
+ std::size_t count) const;
+ [[nodiscard]] std::chrono::milliseconds backoff(int attempt,
+ std::optional<double> 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<std::optional<Address>> lookup_many(const std::vector<std::string>& ceps);
+
+/// Queries the API's health with the default configuration.
+Health health();
+
+} // namespace cepis
+
+#endif // CEPIS_CLIENT_HPP