diff options
Diffstat (limited to 'include/cepis')
| -rw-r--r-- | include/cepis/cepis.hpp | 6 | ||||
| -rw-r--r-- | include/cepis/client.hpp | 7 | ||||
| -rw-r--r-- | include/cepis/errors.hpp | 30 | ||||
| -rw-r--r-- | include/cepis/models.hpp | 39 |
4 files changed, 78 insertions, 4 deletions
diff --git a/include/cepis/cepis.hpp b/include/cepis/cepis.hpp index 4ac3f1f..1861d88 100644 --- a/include/cepis/cepis.hpp +++ b/include/cepis/cepis.hpp @@ -28,8 +28,10 @@ /// ```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::CepNotFound& error) { +/// // the CEP does not exist: ask the user for another. `error.faixa()` +/// // often names the municipality it would have belonged to — context for +/// // the message, never an address. /// } catch (const cepis::Error&) { /// // network, server, rate limit: retry later /// } diff --git a/include/cepis/client.hpp b/include/cepis/client.hpp index 205ba85..3a0d863 100644 --- a/include/cepis/client.hpp +++ b/include/cepis/client.hpp @@ -15,7 +15,7 @@ namespace cepis { /// The library version, as it appears in the default `User-Agent`. -inline constexpr std::string_view kVersion = "0.1.0"; +inline constexpr std::string_view kVersion = "0.2.0"; /// Where the public API lives. inline constexpr std::string_view kDefaultBaseUrl = "https://cep.is"; @@ -33,7 +33,7 @@ inline constexpr std::size_t kMaxBatch = 500; /// 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)`. +/// The default `User-Agent`, e.g. `cepis-cpp/0.2.0 (+https://cep.is)`. std::string default_user_agent(); /// How a `Client` behaves. Every field has a working default; in practice only @@ -108,6 +108,9 @@ class Client { /// 5xx after every attempt; `NetworkError` when the request never /// completed; `InvalidResponse` when a success status carries a body that /// is not a JSON object. + /// + /// A `CepNotFound` often carries `faixa()`, the municipality the CEP would + /// have belonged to. It is context for the error, not an address. [[nodiscard]] Address lookup(std::string_view cep) const; /// Looks up many CEPs in one batch. diff --git a/include/cepis/errors.hpp b/include/cepis/errors.hpp index 0420ecd..fa928f0 100644 --- a/include/cepis/errors.hpp +++ b/include/cepis/errors.hpp @@ -4,6 +4,9 @@ #include <optional> #include <stdexcept> #include <string> +#include <utility> + +#include "cepis/models.hpp" namespace cepis { @@ -38,9 +41,36 @@ class InvalidCep : public Error { }; /// The CEP is well-formed but does not exist in the DNE database. +/// +/// The API often says **where it would have been** — see `faixa()`. That is +/// still a not-found: the extra context does not make the CEP deliverable, and +/// this remains the exception thrown for it. class CepNotFound : public Error { public: using Error::Error; + + CepNotFound(const std::string& message, std::optional<Faixa> faixa) + : Error(message), faixa_(std::move(faixa)) {} + + /// The municipality the CEP would have belonged to, when the API names one. + /// + /// `std::nullopt` when the CEP falls in a band no municipality owns — the + /// API **omits** the block there, it never sends null — and also whenever + /// the block cannot be read. Read `Faixa` before showing any of it: the + /// range it describes is sparse, and the CEP does not exist either way. + /// + /// ```cpp + /// } catch (const cepis::CepNotFound& error) { + /// if (error.faixa()) { + /// std::cerr << "not found; did you mean somewhere in " + /// << error.faixa()->localidade << '/' << error.faixa()->uf << "?\n"; + /// } + /// } + /// ``` + [[nodiscard]] const std::optional<Faixa>& faixa() const noexcept { return faixa_; } + + private: + std::optional<Faixa> faixa_; }; /// The request never completed: DNS, refused connection, timeout, TLS. diff --git a/include/cepis/models.hpp b/include/cepis/models.hpp index faa2dde..6252001 100644 --- a/include/cepis/models.hpp +++ b/include/cepis/models.hpp @@ -96,6 +96,45 @@ struct Address { static Address from_json(const Json& data); }; +/// The municipality a CEP that does **not** exist would have belonged to. +/// +/// It arrives on a `404`, attached to `CepNotFound`. It is context for an +/// error, never an address: the CEP still does not exist, and nothing in this +/// block makes it deliverable. Treat it as a hint for the message you show — +/// "did you mean somewhere in Areal/RJ?" — not as a result. +/// +/// It exists because a CEP that fails is rarely random. It is usually someone +/// who knows the city and guessed the number, and the classic guess is the +/// `-000` that 957 municipalities never received. +/// +/// `cep_ini` and `cep_fim` are the smallest and the largest CEP that +/// **actually exist** in that municipality — not the bounds the Correios +/// publish. For Areal the DNE's own start is `25845000`, which is precisely +/// the CEP that just failed; `cep_ini` is `25845001`. +/// +/// The range is also **sparse**: Areal's 118 CEPs are scattered across the +/// ~4,900 numbers between `25845001` and `25849899`. That is what `ceps` is +/// for. The pair is a span, not a list — walking from `cep_ini` to `cep_fim` +/// produces thousands of CEPs that do not exist either. +struct Faixa { + std::string localidade; + std::string uf; + std::string estado; + std::string regiao; + std::string ibge; + std::string cep_ini; + std::string cep_fim; + std::int64_t ceps = 0; + + /// Reads the API's `faixa` block, or `std::nullopt` when it cannot be read. + /// + /// Anything unexpected — a missing block, a non-object, an empty one, a + /// municipality without a name — yields `std::nullopt` rather than an + /// error. The 404 is already the answer; failing to read the hint attached + /// to it must not turn a clean not-found into a parse failure. + static std::optional<Faixa> from_json(const Json& data); +}; + /// The response of `GET /health`. /// /// Good for two things: checking that the API answers, and knowing **which |