summaryrefslogtreecommitdiffstats
path: root/src/model.rs
blob: 9092239576d66b48ae9a0e5a0dd2904c705f0e55 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
//! 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 <https://cep.is/01001000>
//! 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<Geo> {
        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<Faixa> {
        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<Geo>,
    /// 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<f64> {
    match value {
        Value::Number(n) => n.as_f64(),
        Value::String(s) => s.parse().ok(),
        _ => None,
    }
}