summaryrefslogtreecommitdiffstats
path: root/Sources/Cepis/Models.swift
blob: f4c2291c6f6704bd91d9ea40c507f37a95944f19 (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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
import Foundation

/// 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`` before using ``lat``/``lon`` for distance:
///
/// - `"logradouro"`: the point is a stretch of street. ``raioM`` 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. ``raioM`` runs from 7 km to 40 km.
///   Computing shipping from this is wrong by tens of kilometres.
///
/// ``raioM`` is the radius containing 90% of that CEP's addresses. It exists
/// precisely so that error cannot pass unnoticed.
///
/// The property names are the API's own, in Portuguese; only the casing is
/// adapted, and ``raioM`` maps to the JSON's `raio_m`.
public struct Geo: Codable, Sendable, Equatable, Hashable {
    /// Latitude in decimal degrees, WGS 84.
    public let lat: Double
    /// Longitude in decimal degrees, WGS 84.
    public let lon: Double
    /// Radius in metres containing 90% of the CEP's addresses. JSON `raio_m`.
    public let raioM: Int
    /// `"logradouro"` (a street) or `"localidade"` (a municipality centroid).
    public let precisao: String

    enum CodingKeys: String, CodingKey {
        case lat
        case lon
        case raioM = "raio_m"
        case precisao
    }

    public init(lat: Double, lon: Double, raioM: Int, precisao: String) {
        self.lat = lat
        self.lon = lon
        self.raioM = raioM
        self.precisao = precisao
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.lat = try container.decode(Double.self, forKey: .lat)
        self.lon = try container.decode(Double.self, forKey: .lon)
        if let exact = try? container.decode(Int.self, forKey: .raioM) {
            self.raioM = exact
        } else {
            self.raioM = Int(try container.decode(Double.self, forKey: .raioM))
        }
        self.precisao = try container.decode(String.self, forKey: .precisao)
    }

    /// `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.
    public var isPrecise: Bool { precisao == "logradouro" }
}

/// A CEP and everything the database knows about it.
///
/// Fields the database does not have for that CEP come back as empty strings,
/// never `nil` — that is what the API returns, and changing it here would
/// create a difference between reading the JSON and reading this library.
///
/// ``geo`` is the exception: it is `nil` 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.
///
/// The property names are the API's own, in Portuguese. Everything else in
/// this library is English; the fields are the one deliberate exception, so
/// that what you read in the JSON at <https://cep.is/01001000> is exactly what
/// you access on the value.
public struct Address: Codable, Sendable, Equatable, Hashable {
    /// The CEP, always unmasked: `"01001000"`. See ``formattedCEP``.
    public let cep: String
    public let logradouro: String
    public let complemento: String
    public let unidade: String
    public let bairro: String
    public let localidade: String
    /// The two-letter state code: `"SP"`.
    public let uf: String
    public let estado: String
    public let regiao: String
    /// The IBGE municipality code, the key to join against census data.
    public let ibge: String
    public let gia: String
    public let ddd: String
    public let siafi: String
    /// The coordinate, or `nil` when the database has none for this CEP.
    ///
    /// Always `nil` for an ``Address`` obtained from
    /// ``Client/lookupMany(_:)`` — batches carry no coordinate at all.
    public let geo: Geo?

    enum CodingKeys: String, CodingKey {
        case cep, logradouro, complemento, unidade, bairro, localidade
        case uf, estado, regiao, ibge, gia, ddd, siafi, geo
    }

    public init(
        cep: String,
        logradouro: String = "",
        complemento: String = "",
        unidade: String = "",
        bairro: String = "",
        localidade: String = "",
        uf: String = "",
        estado: String = "",
        regiao: String = "",
        ibge: String = "",
        gia: String = "",
        ddd: String = "",
        siafi: String = "",
        geo: Geo? = nil
    ) {
        self.cep = cep
        self.logradouro = logradouro
        self.complemento = complemento
        self.unidade = unidade
        self.bairro = bairro
        self.localidade = localidade
        self.uf = uf
        self.estado = estado
        self.regiao = regiao
        self.ibge = ibge
        self.gia = gia
        self.ddd = ddd
        self.siafi = siafi
        self.geo = geo
    }

    /// Decodes the API's JSON object.
    ///
    /// Unknown fields are ignored, so a field the API grows before this library
    /// is updated cannot break an existing client. A malformed `geo` block
    /// yields `geo == nil` rather than failing the whole decode: it is not
    /// worth losing the address over, since the rest of the response is still
    /// useful.
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        func string(_ key: CodingKeys) -> String {
            (try? container.decode(String.self, forKey: key)) ?? ""
        }

        self.cep = string(.cep)
        self.logradouro = string(.logradouro)
        self.complemento = string(.complemento)
        self.unidade = string(.unidade)
        self.bairro = string(.bairro)
        self.localidade = string(.localidade)
        self.uf = string(.uf)
        self.estado = string(.estado)
        self.regiao = string(.regiao)
        self.ibge = string(.ibge)
        self.gia = string(.gia)
        self.ddd = string(.ddd)
        self.siafi = string(.siafi)
        self.geo = (try? container.decodeIfPresent(Geo.self, forKey: .geo)) ?? nil
    }

    /// The CEP with its mask: `"01001-000"`.
    ///
    /// The API always returns it unmasked; formatting is presentation, and it
    /// lives here so it does not become a repeated interpolation in every
    /// project that uses this library.
    public var formattedCEP: String {
        guard cep.count == 8 else { return cep }
        let split = cep.index(cep.startIndex, offsetBy: 5)
        return "\(cep[cep.startIndex..<split])-\(cep[split...])"
    }

    init(csvFields fields: [String: String]) {
        self.init(
            cep: fields["cep"] ?? "",
            logradouro: fields["logradouro"] ?? "",
            complemento: fields["complemento"] ?? "",
            unidade: fields["unidade"] ?? "",
            bairro: fields["bairro"] ?? "",
            localidade: fields["localidade"] ?? "",
            uf: fields["uf"] ?? "",
            estado: fields["estado"] ?? "",
            regiao: fields["regiao"] ?? "",
            ibge: fields["ibge"] ?? "",
            gia: fields["gia"] ?? "",
            ddd: fields["ddd"] ?? "",
            siafi: fields["siafi"] ?? "",
            geo: nil
        )
    }
}

/// The municipality a CEP that does not exist *would* belong to.
///
/// It arrives on the **404** of ``Client/lookup(_:)``, carried by
/// ``CepisError/cepNotFound(message:faixa:)``. It is context for an error, not
/// an address: the CEP asked for still does not exist, and a `Faixa` never
/// turns a not-found into a success. Use it to tell someone *"25845-000 is not
/// a CEP, but you are looking at Areal/RJ"* instead of *"not found"*.
///
/// A failing CEP is rarely a random number: it is usually someone who knows the
/// city and guessed the digits. The classic guess is the `-000`, which 957
/// municipalities never received.
///
/// ## The bounds are not the Correios' bounds
///
/// ``cepIni`` and ``cepFim`` are the smallest and the largest CEP that
/// **actually exist** in that municipality — not the range the Correios
/// publish. For Areal the DNE's own start is `25845000`, which is precisely the
/// CEP that just failed; ``cepIni`` is `25845001`.
///
/// ## The range is sparse
///
/// The pair is **not** a list of valid CEPs and iterating it is wrong. Areal's
/// ``ceps`` are 118 numbers scattered across the ~4,900 between `25845001` and
/// `25849899` — roughly one in forty. That is what ``ceps`` is for: it is the
/// count that stops the pair from being read as a contiguous block. For the
/// same reason this type deliberately offers no `contains` — only a lookup can
/// answer whether a CEP exists.
///
/// The field is **omitted, never null**, when the CEP falls in a band no
/// municipality owns, so absence is ordinary and must be handled.
///
/// The property names are the API's own, in Portuguese; only the casing is
/// adapted, and ``cepIni``/``cepFim`` map to the JSON's `cep_ini`/`cep_fim`.
public struct Faixa: Codable, Sendable, Equatable, Hashable {
    /// The municipality's name: `"Areal"`.
    public let localidade: String
    /// The two-letter state code: `"RJ"`.
    public let uf: String
    /// The state's full name: `"Rio de Janeiro"`.
    public let estado: String
    /// The region: `"Sudeste"`.
    public let regiao: String
    /// The IBGE municipality code, the key to join against census data.
    public let ibge: String
    /// The **smallest CEP that exists** there, unmasked. JSON `cep_ini`.
    public let cepIni: String
    /// The **largest CEP that exists** there, unmasked. JSON `cep_fim`.
    public let cepFim: String
    /// How many CEPs actually exist between ``cepIni`` and ``cepFim``.
    ///
    /// Almost always far fewer than the arithmetic distance between the two:
    /// the range is sparse, and this is the number that says by how much.
    public let ceps: Int

    enum CodingKeys: String, CodingKey {
        case localidade, uf, estado, regiao, ibge, ceps
        case cepIni = "cep_ini"
        case cepFim = "cep_fim"
    }

    public init(
        localidade: String,
        uf: String = "",
        estado: String = "",
        regiao: String = "",
        ibge: String = "",
        cepIni: String = "",
        cepFim: String = "",
        ceps: Int = 0
    ) {
        self.localidade = localidade
        self.uf = uf
        self.estado = estado
        self.regiao = regiao
        self.ibge = ibge
        self.cepIni = cepIni
        self.cepFim = cepFim
        self.ceps = ceps
    }

    /// Decodes the API's `faixa` object.
    ///
    /// Every field is tolerated except ``localidade``: a faixa that does not
    /// name a municipality says nothing, so an absent or empty one throws and
    /// the caller turns that into `nil`. Nothing here can escalate — the error
    /// being decorated is a not-found either way, and losing the decoration
    /// must never cost the error itself.
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        func string(_ key: CodingKeys) -> String {
            ((try? container.decode(String.self, forKey: key)) ?? "")
                .trimmingCharacters(in: .whitespaces)
        }

        let localidade = string(.localidade)
        guard !localidade.isEmpty else {
            throw DecodingError.dataCorruptedError(
                forKey: .localidade, in: container,
                debugDescription: "a faixa that names no municipality carries no information")
        }

        self.localidade = localidade
        self.uf = string(.uf)
        self.estado = string(.estado)
        self.regiao = string(.regiao)
        self.ibge = string(.ibge)
        self.cepIni = string(.cepIni)
        self.cepFim = string(.cepFim)
        if let exact = try? container.decode(Int.self, forKey: .ceps) {
            self.ceps = exact
        } else if let rounded = try? container.decode(Double.self, forKey: .ceps) {
            self.ceps = Int(rounded)
        } else {
            self.ceps = 0
        }
    }

    /// The municipality as `"Areal/RJ"`, or just its name when there is no
    /// state code.
    public var localidadeUF: String {
        uf.isEmpty ? localidade : "\(localidade)/\(uf)"
    }
}

/// 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`` identifies it.
public struct Health: Codable, Sendable, Equatable, Hashable {
    /// `"ok"` when healthy. See ``ok``.
    public let status: String
    /// Identifier of the fortnightly DNE database currently loaded.
    public let dne: String
    /// How many CEPs the database holds.
    public let ceps: Int
    /// How many of those carry a coordinate.
    public let geo: Int
    /// When the database was generated, as an RFC 3339 timestamp.
    public let baseGeradaEm: String

    enum CodingKeys: String, CodingKey {
        case status, dne, ceps, geo
        case baseGeradaEm = "base_gerada_em"
    }

    public init(status: String, dne: String, ceps: Int, geo: Int, baseGeradaEm: String) {
        self.status = status
        self.dne = dne
        self.ceps = ceps
        self.geo = geo
        self.baseGeradaEm = baseGeradaEm
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.status = (try? container.decode(String.self, forKey: .status)) ?? ""
        self.dne = (try? container.decode(String.self, forKey: .dne)) ?? ""
        self.ceps = (try? container.decode(Int.self, forKey: .ceps)) ?? 0
        self.geo = (try? container.decode(Int.self, forKey: .geo)) ?? 0
        self.baseGeradaEm = (try? container.decode(String.self, forKey: .baseGeradaEm)) ?? ""
    }

    /// `true` when the API reports itself healthy.
    public var ok: Bool { status == "ok" }
}