summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:18:32 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:18:32 -0300
commit72a4b451c9d06f7a97cac2f43f1b7f52fafcd3b7 (patch)
tree3e4d33a614b673bc8cd9de1e688245f340cd3e1e /README.md
downloadswift-72a4b451c9d06f7a97cac2f43f1b7f52fafcd3b7.tar.gz
Initial importv0.1.0
Diffstat (limited to 'README.md')
-rw-r--r--README.md208
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).