//! The data the API returns, as types. //! //! **Field names are the API's own, in Portuguese** — `logradouro`, `bairro`, //! `localidade`. Everything else in this crate is English; the fields are the //! one deliberate exception. //! //! The reason is fidelity: what you read in the JSON at //! is exactly what you access on the value. Translating to `street`/`city` //! would mean maintaining a field-by-field mapping and forcing anyone reading //! the API docs alongside this crate to translate back in their head. The //! values are Portuguese either way — they come from the Correios database. use serde_json::Value; /// 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`](Geo::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. #[derive(Debug, Clone, PartialEq)] pub struct Geo { /// Latitude in decimal degrees, WGS 84. pub lat: f64, /// Longitude in decimal degrees, WGS 84. pub lon: f64, /// Radius, in metres, containing 90% of the CEP's addresses. pub raio_m: i64, /// `"logradouro"` (a street) or `"localidade"` (a municipality centroid). pub precisao: String, } impl Geo { /// `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. pub fn is_precise(&self) -> bool { self.precisao == "logradouro" } fn from_json(value: &Value) -> Option { let object = value.as_object()?; Some(Geo { lat: number(object.get("lat")?)?, lon: number(object.get("lon")?)?, raio_m: number(object.get("raio_m")?)? as i64, precisao: match object.get("precisao")? { Value::String(s) => s.clone(), _ => return None, }, }) } } /// The municipality a CEP that does not exist would have belonged to. /// /// This is **context for a not-found**, never an address. It rides on /// [`Error::NotFound`](crate::Error::NotFound), the response is still a 404, and /// nothing here says the CEP that was looked up exists — it does not. /// /// It is there because a CEP that fails is rarely a random number: it is /// usually someone who knows the city and guessed the digits, and the classic /// guess is the `-000` that 957 municipalities never received. /// /// # The range is not the Correios range /// /// [`cep_ini`](Faixa::cep_ini) and [`cep_fim`](Faixa::cep_fim) are the smallest /// and the largest CEP that **actually exist** in that municipality, not the /// bounds the Correios publish for it. For Areal/RJ the published start is /// `25845000` — which is precisely the CEP that just failed. Showing the /// official bounds would therefore suggest the CEP is valid. /// /// # The range is sparse /// /// The two ends are not a span of usable numbers. Areal's 118 CEPs are spread /// over the roughly 4,900 numbers between `25845001` and `25849899`; the vast /// majority of that interval does not exist either. That is what /// [`ceps`](Faixa::ceps) is for — never present the pair as a list, or as a /// count, of valid CEPs. #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub struct Faixa { /// Municipality. pub localidade: String, /// Federative unit, two letters: `"RJ"`. pub uf: String, /// Federative unit spelled out: `"Rio de Janeiro"`. pub estado: String, /// Region: `"Sudeste"`, `"Nordeste"`, and so on. pub regiao: String, /// IBGE municipality code — the key to join with public datasets. pub ibge: String, /// Smallest CEP that exists in the municipality, unmasked. pub cep_ini: String, /// Largest CEP that exists in the municipality, unmasked. pub cep_fim: String, /// How many CEPs exist between those two ends, the ends included. /// /// Read next to [`cep_ini`](Faixa::cep_ini) and [`cep_fim`](Faixa::cep_fim) /// it is the measure of how sparse the interval is: 118 across ~4,900 /// numbers for Areal/RJ. pub ceps: u64, } impl Faixa { /// `"Areal/RJ"` — the municipality in the shortest form worth showing. pub fn municipality(&self) -> String { format!("{}/{}", self.localidade, self.uf) } /// Build from the `faixa` block of a 404 body, or `None`. /// /// Anything short of an object naming a municipality yields `None`: /// absent, null, empty, or malformed. The caller is already returning /// [`Error::NotFound`](crate::Error::NotFound) — turning a clean not-found /// into a parse failure over decoration would be a bad trade. pub(crate) fn from_json(value: &Value) -> Option { value.as_object()?; let localidade = text(value, "localidade"); if localidade.is_empty() { return None; } Some(Faixa { localidade, uf: text(value, "uf"), estado: text(value, "estado"), regiao: text(value, "regiao"), ibge: text(value, "ibge"), cep_ini: text(value, "cep_ini"), cep_fim: text(value, "cep_fim"), ceps: count(value, "ceps"), }) } } /// 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 crate. /// /// [`geo`](Address::geo) is the exception: it is `None` 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. /// /// [`raw`](Address::raw) holds the response exactly as it arrived, /// uninterpreted, so a field the API grows before this crate is updated is not /// lost. #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub struct Address { /// The CEP, always unmasked: `"01001000"`. pub cep: String, /// Street, avenue, square — the public way. pub logradouro: String, /// Range or side of the street the CEP covers, when it covers only part. pub complemento: String, /// Name of the large user or building the CEP belongs to, when it does. pub unidade: String, /// Neighbourhood. pub bairro: String, /// Municipality. pub localidade: String, /// Federative unit, two letters: `"SP"`. pub uf: String, /// Federative unit spelled out: `"São Paulo"`. pub estado: String, /// Region: `"Sudeste"`, `"Nordeste"`, and so on. pub regiao: String, /// IBGE municipality code — the key to join with public datasets. pub ibge: String, /// GIA code, São Paulo state tax administration only. pub gia: String, /// Area code for telephony: `"11"`. pub ddd: String, /// SIAFI municipality code, federal administration. pub siafi: String, /// The coordinate, or `None` when the database has none for this CEP. /// /// Always `None` for an address that came from /// [`lookup_many`](crate::Client::lookup_many) — batches carry no /// coordinate at all. pub geo: Option, /// The response exactly as it arrived, so a new API field is never lost. pub raw: Value, } impl Address { /// 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 /// `format!` in every project that uses this. pub fn formatted_cep(&self) -> String { if self.cep.len() == 8 { format!("{}-{}", &self.cep[..5], &self.cep[5..]) } else { self.cep.clone() } } /// Build from the JSON object the API returned. /// /// Unknown fields are ignored during construction but preserved in /// [`raw`](Address::raw). A new field in the API therefore cannot break /// this crate. /// /// A malformed `geo` block yields `geo: None` rather than failing the whole /// value: it is not worth losing the address over, since the rest of the /// response is still useful. pub fn from_json(value: Value) -> Address { let geo = value.get("geo").and_then(Geo::from_json); Address { cep: text(&value, "cep"), logradouro: text(&value, "logradouro"), complemento: text(&value, "complemento"), unidade: text(&value, "unidade"), bairro: text(&value, "bairro"), localidade: text(&value, "localidade"), uf: text(&value, "uf"), estado: text(&value, "estado"), regiao: text(&value, "regiao"), ibge: text(&value, "ibge"), gia: text(&value, "gia"), ddd: text(&value, "ddd"), siafi: text(&value, "siafi"), geo, raw: value, } } } /// 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`](Health::dne) identifies it. #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub struct Health { /// `"ok"` when the API considers itself healthy. pub status: String, /// Identifier of the fortnightly DNE database currently loaded. pub dne: String, /// How many CEPs the database holds. pub ceps: u64, /// How many of those carry a coordinate. pub geo: u64, /// When the database was generated, RFC 3339 in UTC. pub base_gerada_em: String, /// The response exactly as it arrived. pub raw: Value, } impl Health { /// `true` when the API reports itself healthy. pub fn ok(&self) -> bool { self.status == "ok" } /// Build from the JSON object the API returned. pub fn from_json(value: Value) -> Health { Health { status: text(&value, "status"), dne: text(&value, "dne"), ceps: count(&value, "ceps"), geo: count(&value, "geo"), base_gerada_em: text(&value, "base_gerada_em"), raw: value, } } } fn text(value: &Value, key: &str) -> String { match value.get(key) { Some(Value::String(s)) => s.clone(), Some(Value::Null) | None => String::new(), Some(other) => other.to_string(), } } fn count(value: &Value, key: &str) -> u64 { match value.get(key) { Some(Value::Number(n)) => n.as_u64().unwrap_or(0), Some(Value::String(s)) => s.parse().unwrap_or(0), _ => 0, } } fn number(value: &Value) -> Option { match value { Value::Number(n) => n.as_f64(), Value::String(s) => s.parse().ok(), _ => None, } }