summaryrefslogtreecommitdiffstats
path: root/include
diff options
context:
space:
mode:
Diffstat (limited to 'include')
-rw-r--r--include/cepis/cepis.hpp47
-rw-r--r--include/cepis/client.hpp174
-rw-r--r--include/cepis/errors.hpp93
-rw-r--r--include/cepis/json.hpp91
-rw-r--r--include/cepis/models.hpp120
5 files changed, 525 insertions, 0 deletions
diff --git a/include/cepis/cepis.hpp b/include/cepis/cepis.hpp
new file mode 100644
index 0000000..4ac3f1f
--- /dev/null
+++ b/include/cepis/cepis.hpp
@@ -0,0 +1,47 @@
+#ifndef CEPIS_CEPIS_HPP
+#define CEPIS_CEPIS_HPP
+
+/// C++ client for the public CEP API at https://cep.is — official Correios
+/// data (DNE), refreshed every two weeks.
+///
+/// Quick use — for a script, the free functions are enough:
+///
+/// ```cpp
+/// #include <cepis/cepis.hpp>
+///
+/// const cepis::Address a = cepis::lookup("01001-000");
+/// std::cout << a.localidade << ' ' << a.uf << '\n'; // São Paulo SP
+/// ```
+///
+/// In an application, build a `cepis::Client` once and reuse it:
+///
+/// ```cpp
+/// cepis::Config config;
+/// config.base_url = "http://localhost:8080";
+/// config.timeout = std::chrono::seconds{5};
+/// const cepis::Client client(config);
+/// const cepis::Address a = client.lookup("01001000");
+/// ```
+///
+/// Errors are explicit and split by decision — whether retrying helps or not:
+///
+/// ```cpp
+/// try {
+/// const cepis::Address a = cepis::lookup(cep_from_form);
+/// } catch (const cepis::CepNotFound&) {
+/// // the CEP does not exist: ask the user for another
+/// } catch (const cepis::Error&) {
+/// // network, server, rate limit: retry later
+/// }
+/// ```
+///
+/// Note on field names: the API surface is English, but the **data fields keep
+/// the API's own Portuguese names** (`logradouro`, `bairro`, `localidade`).
+/// What you read in the JSON is what you access on the object.
+
+#include "cepis/client.hpp"
+#include "cepis/errors.hpp"
+#include "cepis/json.hpp"
+#include "cepis/models.hpp"
+
+#endif // CEPIS_CEPIS_HPP
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
diff --git a/include/cepis/errors.hpp b/include/cepis/errors.hpp
new file mode 100644
index 0000000..0420ecd
--- /dev/null
+++ b/include/cepis/errors.hpp
@@ -0,0 +1,93 @@
+#ifndef CEPIS_ERRORS_HPP
+#define CEPIS_ERRORS_HPP
+
+#include <optional>
+#include <stdexcept>
+#include <string>
+
+namespace cepis {
+
+/// Base of every error this library throws. Catch it to handle any failure.
+///
+/// It derives from `std::runtime_error`, so a `catch (const std::exception&)`
+/// already in place around your I/O keeps working without knowing this library
+/// exists, and `what()` carries the message.
+///
+/// Below this the split is by **the decision the caller has to make**, not by
+/// the HTTP status that caused it:
+///
+/// - `InvalidCep` and `CepNotFound` are problems with the *request*. Retrying
+/// is pointless — the same CEP fails again.
+/// - `NetworkError`, `ServerError` and `RateLimited` are problems with
+/// *transport*. Retrying may help, and the client already retried on its own
+/// before throwing any of them.
+class Error : public std::runtime_error {
+ public:
+ explicit Error(const std::string& message) : std::runtime_error(message) {}
+};
+
+/// The CEP is not eight digits.
+///
+/// Usually thrown **without** a request ever being made: the client checks the
+/// format before going to the network. It is also what a `400` from the API
+/// becomes, carrying the API's own message — the format already passed local
+/// validation there, so the server knows something this client does not.
+class InvalidCep : public Error {
+ public:
+ using Error::Error;
+};
+
+/// The CEP is well-formed but does not exist in the DNE database.
+class CepNotFound : public Error {
+ public:
+ using Error::Error;
+};
+
+/// The request never completed: DNS, refused connection, timeout, TLS.
+///
+/// Thrown only after every configured attempt has failed.
+class NetworkError : public Error {
+ public:
+ using Error::Error;
+};
+
+/// The API answered 5xx on every attempt.
+class ServerError : public Error {
+ public:
+ ServerError(const std::string& message, int status) : Error(message), status_(status) {}
+
+ /// The HTTP status seen on the last attempt.
+ [[nodiscard]] int status() const noexcept { return status_; }
+
+ private:
+ int status_;
+};
+
+/// The API (or the edge in front of it) answered 429 on every attempt.
+class RateLimited : public Error {
+ public:
+ RateLimited(const std::string& message, std::optional<double> retry_after)
+ : Error(message), retry_after_(retry_after) {}
+
+ /// What the server asked for, in seconds, or `std::nullopt` if it did not
+ /// say. The client already honoured that value across the attempts it made;
+ /// it is exposed here for callers who want to reschedule the work.
+ [[nodiscard]] std::optional<double> retry_after() const noexcept { return retry_after_; }
+
+ private:
+ std::optional<double> retry_after_;
+};
+
+/// A success status came back with a body that cannot be parsed.
+///
+/// In practice this means something between you and the API is answering
+/// instead of it — a captive Wi-Fi portal, a corporate proxy, an HTML error
+/// page. Useful as a diagnosis: the problem is not in your code.
+class InvalidResponse : public Error {
+ public:
+ using Error::Error;
+};
+
+} // namespace cepis
+
+#endif // CEPIS_ERRORS_HPP
diff --git a/include/cepis/json.hpp b/include/cepis/json.hpp
new file mode 100644
index 0000000..38bde79
--- /dev/null
+++ b/include/cepis/json.hpp
@@ -0,0 +1,91 @@
+#ifndef CEPIS_JSON_HPP
+#define CEPIS_JSON_HPP
+
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace cepis {
+
+/// An immutable-by-convention JSON value, sufficient for this API's responses.
+///
+/// The library ships its own parser rather than depending on a general-purpose
+/// one: the payloads are flat objects of strings and numbers, and a client
+/// whose only dependency is libcurl is easier to drop into an existing build.
+///
+/// Objects preserve **insertion order** — the API documents a fixed field
+/// order and round-tripping it through `dump()` keeps that visible. Lookup is
+/// a linear scan, which is the faster choice at this size anyway.
+///
+/// The parser accepts UTF-8 input, decodes `\uXXXX` escapes (surrogate pairs
+/// included) into UTF-8, and rejects trailing garbage after the top-level
+/// value.
+class Json {
+ public:
+ /// The six JSON types.
+ enum class Kind : std::uint8_t { Null, Bool, Number, String, Array, Object };
+
+ using Array = std::vector<Json>;
+ using Object = std::vector<std::pair<std::string, Json>>;
+
+ /// Constructs a null value.
+ Json() = default;
+
+ /// Constructs a value of the given kind, empty for the composite kinds.
+ static Json boolean(bool value);
+ static Json number(double value);
+ static Json string(std::string value);
+ static Json array(Array value = {});
+ static Json object(Object value = {});
+
+ /// Parses `text`, returning `std::nullopt` if it is not one well-formed
+ /// JSON value. Callers in this library turn that into `InvalidResponse`.
+ static std::optional<Json> parse(std::string_view text);
+
+ /// Serialises back to compact JSON. Non-ASCII is emitted as raw UTF-8.
+ [[nodiscard]] std::string dump() const;
+
+ [[nodiscard]] Kind kind() const noexcept { return kind_; }
+ [[nodiscard]] bool is_null() const noexcept { return kind_ == Kind::Null; }
+ [[nodiscard]] bool is_bool() const noexcept { return kind_ == Kind::Bool; }
+ [[nodiscard]] bool is_number() const noexcept { return kind_ == Kind::Number; }
+ [[nodiscard]] bool is_string() const noexcept { return kind_ == Kind::String; }
+ [[nodiscard]] bool is_array() const noexcept { return kind_ == Kind::Array; }
+ [[nodiscard]] bool is_object() const noexcept { return kind_ == Kind::Object; }
+
+ /// Typed accessors. Each returns a harmless default for a mismatched kind
+ /// rather than throwing: every field of this API is optional in practice,
+ /// and a wrong type on one field must not cost the whole response.
+ [[nodiscard]] bool as_bool(bool fallback = false) const noexcept;
+ [[nodiscard]] double as_number(double fallback = 0.0) const noexcept;
+ [[nodiscard]] const std::string& as_string() const noexcept;
+ [[nodiscard]] const Array& as_array() const noexcept;
+ [[nodiscard]] const Object& as_object() const noexcept;
+
+ /// Returns the member named `key`, or `nullptr` when this is not an object
+ /// or has no such member.
+ [[nodiscard]] const Json* find(std::string_view key) const noexcept;
+
+ /// Appends a member. Replaces the value if `key` is already present, so the
+ /// original position in the order is kept.
+ void set(std::string key, Json value);
+
+ /// Appends an element. Does nothing unless this is an array.
+ void push_back(Json value);
+
+ private:
+ Kind kind_ = Kind::Null;
+ bool bool_ = false;
+ double number_ = 0.0;
+ std::string string_;
+ Array array_;
+ Object object_;
+};
+
+} // namespace cepis
+
+#endif // CEPIS_JSON_HPP
diff --git a/include/cepis/models.hpp b/include/cepis/models.hpp
new file mode 100644
index 0000000..faa2dde
--- /dev/null
+++ b/include/cepis/models.hpp
@@ -0,0 +1,120 @@
+#ifndef CEPIS_MODELS_HPP
+#define CEPIS_MODELS_HPP
+
+#include <cstdint>
+#include <optional>
+#include <string>
+
+#include "cepis/json.hpp"
+
+/// The data the API returns, as values.
+///
+/// **Field names are the API's own, in Portuguese** — `logradouro`, `bairro`,
+/// `localidade`. Everything else in this library is English; the fields are the
+/// one deliberate exception.
+///
+/// The reason is fidelity: what you read in the JSON at https://cep.is/01001000
+/// is exactly what you access on the object. Translating to `street`/`city`
+/// would mean maintaining a field-by-field mapping and forcing anyone reading
+/// the API docs alongside this library to translate back in their head. The
+/// values are Portuguese either way — they come from the Correios database.
+namespace cepis {
+
+/// 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` lands around
+/// 100–150 m. Good for maps and for distance.
+/// - `"localidade"`: the CEP *is* the whole municipality (a city-wide general
+/// CEP), and the point is its centroid. `raio_m` runs 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.
+struct Geo {
+ double lat = 0.0;
+ double lon = 0.0;
+ std::int64_t raio_m = 0;
+ std::string precisao;
+
+ /// `true` when the coordinate points at a street, not a municipality.
+ ///
+ /// Shorthand for `precisao == "logradouro"`, which is the check to make
+ /// before using the point to measure distance.
+ [[nodiscard]] bool is_precise() const noexcept { return precisao == "logradouro"; }
+};
+
+/// A CEP and everything the database knows about it.
+///
+/// Fields the database does not have for that CEP come back as empty strings,
+/// never absent — that is what the API returns, and changing it here would
+/// create a difference between reading the JSON and reading this library.
+///
+/// `geo` is the exception: it is `std::nullopt` when there is no coordinate.
+/// There the difference is real, because the API **omits** the whole field in
+/// that case (about 43% of CEPs) rather than sending null.
+struct Address {
+ std::string cep;
+ std::string logradouro;
+ std::string complemento;
+ std::string unidade;
+ std::string bairro;
+ std::string localidade;
+ std::string uf;
+ std::string estado;
+ std::string regiao;
+ std::string ibge;
+ std::string gia;
+ std::string ddd;
+ std::string siafi;
+ std::optional<Geo> geo;
+
+ /// The response exactly as it arrived, uninterpreted, so a field the API
+ /// grows before this library is updated is not lost.
+ Json raw;
+
+ /// The CEP with its mask: `01001-000`.
+ ///
+ /// The native contract always returns it unmasked; formatting is
+ /// presentation, and it lives here so it does not become a repeated
+ /// concatenation in every project that uses this.
+ [[nodiscard]] std::string formatted_cep() const;
+
+ /// Builds from the object the API returned.
+ ///
+ /// Unknown fields are ignored during construction but preserved in `raw`. A
+ /// new field in the API therefore cannot break this library.
+ ///
+ /// A malformed `geo` block yields an empty `geo` rather than propagating the
+ /// error: it is not worth losing the whole address over, since the rest of
+ /// the response is still useful.
+ static Address from_json(const Json& data);
+};
+
+/// The response of `GET /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.
+struct Health {
+ std::string status;
+ std::string dne;
+ std::int64_t ceps = 0;
+ std::int64_t geo = 0;
+ std::string base_gerada_em;
+
+ /// `true` when the API reports itself healthy.
+ [[nodiscard]] bool ok() const noexcept { return status == "ok"; }
+
+ /// Builds from the object the API returned.
+ static Health from_json(const Json& data);
+};
+
+} // namespace cepis
+
+#endif // CEPIS_MODELS_HPP