summaryrefslogtreecommitdiffstats
path: root/include
diff options
context:
space:
mode:
Diffstat (limited to 'include')
-rw-r--r--include/cepis/cepis.hpp6
-rw-r--r--include/cepis/client.hpp7
-rw-r--r--include/cepis/errors.hpp30
-rw-r--r--include/cepis/models.hpp39
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