summaryrefslogtreecommitdiffstats
path: root/source/cepis/model.d
diff options
context:
space:
mode:
Diffstat (limited to 'source/cepis/model.d')
-rw-r--r--source/cepis/model.d129
1 files changed, 129 insertions, 0 deletions
diff --git a/source/cepis/model.d b/source/cepis/model.d
index 9ec3ffb..01692dd 100644
--- a/source/cepis/model.d
+++ b/source/cepis/model.d
@@ -201,6 +201,92 @@ struct Address
}
/**
+ * The municipality a CEP that does $(B not) exist would have belonged to.
+ *
+ * This arrives with a $(B 404), on $(D CepNotFoundException.faixa). It is
+ * $(B context for an error, never an address): the CEP asked for does not
+ * exist, and nothing here says otherwise. Rendering it as though the lookup had
+ * succeeded turns a clean not-found into a wrong answer.
+ *
+ * It is worth having because a CEP that fails is rarely a random number — it is
+ * someone who knows the city and guessed the digits, and the classic guess is
+ * the `-000` that 957 municipalities never received.
+ *
+ * $(D cep_ini) and $(D cep_fim) are the smallest and the largest CEP that
+ * $(B actually exist) in that municipality, $(B not) the bounds the Correios
+ * publish for it. The DNE's own start for Areal is `25845000` — precisely the
+ * CEP that just failed; the smallest one that exists is `25845001`.
+ *
+ * The range is also $(B sparse). Areal's 118 CEPs are scattered across the
+ * ~4,900 numbers between `25845001` and `25849899`. That is what $(D ceps) is
+ * for: never present the pair as the list of valid CEPs for the city, and never
+ * walk it expecting the numbers in between to resolve.
+ */
+struct Faixa
+{
+ /// Municipality that owns the range: `"Areal"`.
+ string localidade;
+ /// Federative unit, two letters: `"RJ"`.
+ string uf;
+ /// Federative unit spelled out: `"Rio de Janeiro"`.
+ string estado;
+ /// Region: `"Sudeste"`, `"Nordeste"`, and so on.
+ string regiao;
+ /// IBGE municipality code — the key to join with public datasets.
+ string ibge;
+ /// The smallest CEP that exists in the municipality, unmasked.
+ string cep_ini;
+ /// The largest CEP that exists in the municipality, unmasked.
+ string cep_fim;
+ /// How many CEPs actually exist between $(D cep_ini) and $(D cep_fim).
+ ulong ceps;
+
+ /**
+ * The municipality the way one says it out loud: `"Areal/RJ"`.
+ *
+ * For a message to a human — it is what the not-found error puts in its
+ * own. Falls back to whichever of the two the API sent.
+ */
+ string localidadeUf() const pure nothrow
+ {
+ if (localidade.length == 0)
+ return uf;
+ if (uf.length == 0)
+ return localidade;
+ return localidade ~ "/" ~ uf;
+ }
+
+ /**
+ * Build from the `faixa` block a 404 carried.
+ *
+ * Returns a null $(D Nullable) for anything that does not name a
+ * municipality: absent, not an object, or with an empty $(D localidade).
+ * The API $(B omits) the block — it never sends null — when the CEP falls
+ * in a band no municipality owns, and that is an ordinary not-found, not a
+ * failure. Nothing read here may turn a 404 into a parse error.
+ */
+ static Nullable!Faixa fromJSON(const JSONValue value)
+ {
+ if (value.type != JSONType.object)
+ return Nullable!Faixa.init;
+
+ Faixa faixa;
+ faixa.localidade = jsonText(value, "localidade");
+ if (faixa.localidade.length == 0)
+ return Nullable!Faixa.init;
+
+ faixa.uf = jsonText(value, "uf");
+ faixa.estado = jsonText(value, "estado");
+ faixa.regiao = jsonText(value, "regiao");
+ faixa.ibge = jsonText(value, "ibge");
+ faixa.cep_ini = jsonText(value, "cep_ini");
+ faixa.cep_fim = jsonText(value, "cep_fim");
+ faixa.ceps = jsonCount(value, "ceps");
+ return nullable(faixa);
+ }
+}
+
+/**
* The response of `GET /health`.
*
* Good for two things: checking that the API answers, and knowing $(B which
@@ -365,6 +451,49 @@ unittest
assert(address.raw["campo_do_futuro"].str == "value");
}
+@("a faixa names the municipality the missing cep would belong to")
+unittest
+{
+ import std.json : parseJSON;
+
+ const faixa = Faixa.fromJSON(parseJSON(`{"localidade":"Areal","uf":"RJ",`
+ ~ `"estado":"Rio de Janeiro","regiao":"Sudeste","ibge":"3300225",`
+ ~ `"cep_ini":"25845001","cep_fim":"25849899","ceps":118}`));
+
+ assert(!faixa.isNull);
+ assert(faixa.get.localidade == "Areal");
+ assert(faixa.get.ibge == "3300225");
+ assert(faixa.get.cep_ini == "25845001");
+ assert(faixa.get.cep_fim == "25849899");
+ assert(faixa.get.ceps == 118);
+ assert(faixa.get.localidadeUf == "Areal/RJ");
+}
+
+@("a faixa that names no municipality is absent, not an error")
+unittest
+{
+ import std.json : parseJSON;
+
+ assert(Faixa.fromJSON(parseJSON(`{}`)).isNull);
+ assert(Faixa.fromJSON(parseJSON(`{"localidade":"","uf":"RJ"}`)).isNull);
+ assert(Faixa.fromJSON(parseJSON(`"Areal"`)).isNull);
+ assert(Faixa.fromJSON(parseJSON(`null`)).isNull);
+ assert(Faixa.fromJSON(JSONValue.init).isNull);
+}
+
+@("a partial faixa keeps what it has")
+unittest
+{
+ import std.json : parseJSON;
+
+ const faixa = Faixa.fromJSON(parseJSON(`{"localidade":"Areal","ceps":"118"}`));
+
+ assert(!faixa.isNull);
+ assert(faixa.get.ceps == 118);
+ assert(faixa.get.cep_ini == "");
+ assert(faixa.get.localidadeUf == "Areal");
+}
+
@("health reads counts as numbers")
unittest
{