diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:18:32 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:18:32 -0300 |
| commit | 72a4b451c9d06f7a97cac2f43f1b7f52fafcd3b7 (patch) | |
| tree | 3e4d33a614b673bc8cd9de1e688245f340cd3e1e /Sources/Cepis/Models.swift | |
| download | swift-72a4b451c9d06f7a97cac2f43f1b7f52fafcd3b7.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'Sources/Cepis/Models.swift')
| -rw-r--r-- | Sources/Cepis/Models.swift | 241 |
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" } +} |