summaryrefslogtreecommitdiffstats
path: root/Sources/Cepis/Models.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Sources/Cepis/Models.swift')
-rw-r--r--Sources/Cepis/Models.swift241
1 files changed, 241 insertions, 0 deletions
diff --git a/Sources/Cepis/Models.swift b/Sources/Cepis/Models.swift
new file mode 100644
index 0000000..8d8df35
--- /dev/null
+++ b/Sources/Cepis/Models.swift
@@ -0,0 +1,241 @@
+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 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" }
+}