diff options
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 208 |
1 files changed, 208 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d75a9c --- /dev/null +++ b/README.md @@ -0,0 +1,208 @@ +# Cepis + +Swift client for the [public CEP API at cep.is](https://cep.is). Official +Correios data (DNE), refreshed every two weeks. + +```swift +import Cepis + +let address = try await Client.shared.lookup("01001-000") +print(address.logradouro, address.localidade, address.uf) +// Praça da Sé São Paulo SP +``` + +No dependencies: `URLSession` and `Codable` from the standard library and +Foundation. + +## Installation + +Add the package to `Package.swift`: + +```swift +dependencies: [ + .package(url: "https://git.cep.is/swift.git", from: "0.1.0") +] +``` + +and the product to your target: + +```swift +.target(name: "MyTarget", dependencies: [.product(name: "Cepis", package: "swift")]) +``` + +Requires Swift 5.9 or newer. Runs on macOS 10.15+, iOS 13+ and Linux. + +## Usage + +### A single lookup + +`lookup` accepts the CEP masked or unmasked and returns an `Address`: + +```swift +let a = try await Client.shared.lookup("01310-100") + +a.logradouro // "Avenida Paulista" +a.localidade // "São Paulo" +a.uf // "SP" +a.ibge // "3550308" +a.cep // "01310100" (always unmasked) +a.formattedCEP // "01310-100" +``` + +The data fields keep the API's Portuguese names; only the casing follows Swift +convention. Fields the database does not have for that CEP come back as empty +strings, never `nil`. + +### Coordinates + +About 57% of CEPs have a coordinate. When they do not, `geo` is `nil`: + +```swift +if let geo = a.geo { + print(geo.lat, geo.lon) // -23.567202 -46.649659 + print(geo.raioM) // 1084 (JSON `raio_m`) + print(geo.precisao) // "logradouro" +} +``` + +**Check `precisao` before computing distance.** There are two kinds of point, +and the difference between them is tens of kilometres: + +| `precisao` | typical radius | what the point is | +| --- | --- | --- | +| `logradouro` | 110–140 m | the stretch of street — good for maps and distance | +| `localidade` | 7–40 km | the municipality centroid: the CEP *is* the whole city | + +`geo.isPrecise` is the shorthand for `precisao == "logradouro"`: + +```swift +if let geo = a.geo, geo.isPrecise { + cost = shipping(from: geo.lat, geo.lon) +} else { + cost = shipping(fromMunicipality: a.ibge) +} +``` + +### Many CEPs at once + +Do not loop over `lookup`. Use `lookupMany`, which resolves up to 500 CEPs per +request: + +```swift +let ceps = ["01001000", "99999999", "20040020"] +let result = try await Client.shared.lookupMany(ceps) + +for (requested, address) in zip(ceps, result) { + if let address { + print("\(requested): \(address.localidade)") + } else { + print("\(requested): not found") + } +} +``` + +The returned array has **the same length and the same order** as the input. A +CEP that does not exist, or is malformed, becomes `nil` at its position — it +does not vanish, and it does not throw. + +Arrays longer than 500 are chunked automatically. + +> **Batches carry no coordinate.** The endpoint behind them is CSV, and the CSV +> has no `geo` column. Every `Address` from `lookupMany` has `geo == nil`, +> including for CEPs that do have one. Need `geo`? Use `lookup`, one at a time. + +### API health + +Useful for two things: checking the API answers, and knowing **which version of +the database** is live: + +```swift +let h = try await Client.shared.health() +h.ok // true +h.dne // "26071" identifier of the fortnightly database +h.ceps // 1596168 +h.baseGeradaEm // "2026-07-24T03:48:28Z" +``` + +An API reporting itself degraded does not throw — that arrives in `h.status`. + +## Configuration + +For anything beyond the defaults, build a `Client` and reuse it. It is an +immutable value and safe to share across tasks and threads: + +```swift +let client = Client( + baseURL: URL(string: "https://cep.is")!, // point at another instance + timeout: 10, // seconds per request + maxAttempts: 3, // counting the first; 1 disables retrying + backoffBase: 0.5, // base of the exponential wait + backoffMax: 8, // cap on each wait, Retry-After included + totalTimeout: nil // cap on the whole call, attempts included +) + +let address = try await client.lookup("01001000") +``` + +`timeout` is `URLRequest.timeoutInterval`, which measures inactivity rather +than total elapsed time; `totalTimeout` is the hard cap on the whole call. + +Retries cover 429, 5xx and network failures, with exponential backoff and +jitter; 400 and 404 are never retried. + +`Client.shared` is a ready-made instance with the defaults. + +### Pointing at a local instance + +`baseURL` is the only parameter that usually changes between environments. The +API repo brings a local instance up with `just up`: + +```swift +let client = Client(baseURL: URL(string: "http://localhost:8080")!) +``` + +## Errors + +Everything throws `CepisError`. Its cases split by **the decision you have to +make**, not by the HTTP status that caused them: + +| case | when | does retrying help? | +| --- | --- | --- | +| `invalidCEP` | not 8 digits | no | +| `cepNotFound` | well-formed, not in the database | no | +| `rateLimited` | 429; carries `retryAfter` | yes, later | +| `serverError` | 5xx; carries `status` | yes, later | +| `networkError` | DNS, connection, timeout, TLS | yes, later | +| `invalidResponse` | unintelligible response | probably not | + +```swift +do { + let address = try await Client.shared.lookup(cepFromForm) +} catch CepisError.cepNotFound { + // ask the user for another CEP +} catch let error as CepisError { + // network, server, rate limit: reschedule + print(error.message, error.isRetriable) +} +``` + +`invalidResponse` almost always means something between you and the API is +answering instead of it — a captive Wi-Fi portal, a corporate proxy, an HTML +error page. Useful as a diagnosis: the problem is not in your code. + +## Development + +```sh +make # list the targets +make test # run the suite +make lint # swift-format lint, touches nothing +make build # compile the library +make check # lint, then test, then build +``` + +`lint` and `format` need `swift format` (Swift 6 toolchains) or `swift-format` +on `PATH`; they report and skip when it is missing. + +## Licence + +ISC. See [LICENSE](LICENSE). |