//! The data the API returns, as types. //! //! **Field names are the API's own, in Portuguese** — `logradouro`, `bairro`, //! `localidade`. Everything else in this library is English; the fields are the //! one deliberate exception. //! //! The reason is fidelity: what you read in the JSON at //! is exactly what you access on the value. Translating to `street`/`city` //! would mean maintaining a field-by-field mapping and forcing anyone reading //! the API docs alongside this library to translate back in their head. The //! values are Portuguese either way — they come from the Correios database. //! //! ## Ownership //! //! `Address` and `Health` own their strings. Every one of their `[]const u8` //! fields points into memory the value allocated, and `deinit` releases it. //! Pass the same allocator the `Client` was built with, and treat every slice //! as invalid afterwards. const std = @import("std"); const Allocator = std.mem.Allocator; /// 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. `raio_m` 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. `raio_m` runs from 7 km to 40 km. /// Computing shipping from this is wrong by tens of kilometres. /// /// `raio_m` is the radius containing 90% of that CEP's addresses. It exists /// precisely so that error cannot pass unnoticed. pub const Geo = struct { /// Latitude in decimal degrees, WGS 84. lat: f64, /// Longitude in decimal degrees, WGS 84. lon: f64, /// Radius, in metres, containing 90% of the CEP's addresses. raio_m: i64, /// `"logradouro"` (a street) or `"localidade"` (a municipality centroid). /// Borrowed from the owning `Address`; invalid once that is deinitialised. precisao: []const u8, /// 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. pub fn isPrecise(g: Geo) bool { return std.mem.eql(u8, g.precisao, "logradouro"); } }; /// The string fields of an `Address`, in the API's own order. /// /// Used to keep the field list, the JSON keys and the CSV columns in step: all /// three are driven from this one declaration. pub const address_fields = [_][]const u8{ "cep", "logradouro", "complemento", "unidade", "bairro", "localidade", "uf", "estado", "regiao", "ibge", "gia", "ddd", "siafi", }; /// A CEP and everything the database knows about it. /// /// Fields the database does not have for that CEP come back as empty strings, /// never absent — 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 null 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. /// /// ## Ownership /// /// The value owns every byte its slices point at. Call `deinit` with the /// allocator the `Client` was built with exactly once; all slices, including /// `geo.precisao` and `raw`, are invalid from then on. pub const Address = struct { /// The CEP, always unmasked: `"01001000"`. cep: []const u8 = "", /// Street, avenue, square — the public way. logradouro: []const u8 = "", /// Range or side of the street the CEP covers, when it covers only part. complemento: []const u8 = "", /// Name of the large user or building the CEP belongs to, when it does. unidade: []const u8 = "", /// Neighbourhood. bairro: []const u8 = "", /// Municipality. localidade: []const u8 = "", /// Federative unit, two letters: `"SP"`. uf: []const u8 = "", /// Federative unit spelled out: `"São Paulo"`. estado: []const u8 = "", /// Region: `"Sudeste"`, `"Nordeste"`, and so on. regiao: []const u8 = "", /// IBGE municipality code — the key to join with public datasets. ibge: []const u8 = "", /// GIA code, São Paulo state tax administration only. gia: []const u8 = "", /// Area code for telephony: `"11"`. ddd: []const u8 = "", /// SIAFI municipality code, federal administration. siafi: []const u8 = "", /// The coordinate, or null when the database has none for this CEP. /// /// Always null for an address that came from `Client.lookupMany` — batches /// carry no coordinate at all. geo: ?Geo = null, /// The response body exactly as it arrived, uninterpreted, so a field the /// API grows before this library is updated is not lost. Parse it yourself /// with `std.json` when you need one. /// /// Empty for an address that came from `Client.lookupMany`: the batch /// endpoint answers in CSV, and a single row is not a document. raw: []const u8 = "", /// Backing storage for every string field above. Internal — do not read it /// and do not free it yourself; `deinit` does. bytes: []u8 = &.{}, /// Release everything the address owns. Pass the allocator the `Client` /// was built with. Every slice on the value is invalid afterwards. pub fn deinit(a: *Address, gpa: Allocator) void { gpa.free(a.bytes); gpa.free(a.raw); a.* = undefined; } /// Write the CEP with its mask — `01001-000` — into `buffer`. /// /// The native contract always returns it unmasked; formatting is /// presentation, and it lives here so it does not become a repeated /// `bufPrint` in every project that uses this. A CEP that is not 8 digits /// long is copied through unchanged. The result borrows `buffer`, which /// must hold at least 9 bytes. pub fn formattedCep(a: Address, buffer: []u8) []const u8 { if (a.cep.len != 8 or buffer.len < 9) { const n = @min(a.cep.len, buffer.len); @memcpy(buffer[0..n], a.cep[0..n]); return buffer[0..n]; } @memcpy(buffer[0..5], a.cep[0..5]); buffer[5] = '-'; @memcpy(buffer[6..9], a.cep[5..8]); return buffer[0..9]; } /// Build from the JSON object the API returned. /// /// Unknown fields are ignored during construction but preserved in `raw`. /// A new field in the API therefore cannot break this library. /// /// A malformed `geo` block yields `geo = null` rather than failing the /// whole value: it is not worth losing the address over, since the rest of /// the response is still useful. /// /// `body` is duplicated into `raw`; the caller keeps ownership of what it /// passed in. The returned `Address` owns its memory — call `deinit`. pub fn fromJson(gpa: Allocator, value: std.json.Value, body: []const u8) Allocator.Error!Address { var scratch: [address_fields.len][32]u8 = undefined; var values: [address_fields.len][]const u8 = undefined; inline for (address_fields, 0..) |name, i| { values[i] = jsonText(value, name, &scratch[i]); } const geo_source = geoFromJson(value); return build(gpa, &values, geo_source, body); } /// Build from one CSV record of the batch endpoint, given the header row. /// /// Columns the header does not mention are ignored; columns the header /// mentions but the record lacks come out empty. There is no `geo` column /// in the batch, so `geo` is always null and `raw` always empty. /// /// The returned `Address` owns its memory — call `deinit`. pub fn fromCsvRecord( gpa: Allocator, header: []const []const u8, record: []const []const u8, ) Allocator.Error!Address { var values: [address_fields.len][]const u8 = @splat(""); for (header, 0..) |column, i| { if (i >= record.len) break; inline for (address_fields, 0..) |name, slot| { if (std.mem.eql(u8, column, name)) values[slot] = record[i]; } } return build(gpa, &values, null, ""); } /// A `Geo` whose `precisao` still points at borrowed memory, as read out of /// a JSON block but before the strings are packed. Internal. const GeoSource = struct { lat: f64, lon: f64, raio_m: i64, precisao: []const u8 }; fn build( gpa: Allocator, values: *const [address_fields.len][]const u8, geo_source: ?GeoSource, body: []const u8, ) Allocator.Error!Address { var total: usize = 0; for (values) |v| total += v.len; if (geo_source) |g| total += g.precisao.len; const bytes = try gpa.alloc(u8, total); errdefer gpa.free(bytes); const raw = try gpa.dupe(u8, body); errdefer gpa.free(raw); var address: Address = .{ .bytes = bytes, .raw = raw }; var cursor: usize = 0; inline for (address_fields, 0..) |name, i| { const v = values[i]; @memcpy(bytes[cursor..][0..v.len], v); @field(address, name) = bytes[cursor..][0..v.len]; cursor += v.len; } if (geo_source) |g| { @memcpy(bytes[cursor..][0..g.precisao.len], g.precisao); address.geo = .{ .lat = g.lat, .lon = g.lon, .raio_m = g.raio_m, .precisao = bytes[cursor..][0..g.precisao.len], }; cursor += g.precisao.len; } return address; } fn geoFromJson(value: std.json.Value) ?GeoSource { const object = switch (value) { .object => |o| o, else => return null, }; const block = switch (object.get("geo") orelse return null) { .object => |o| o, else => return null, }; const precisao = switch (block.get("precisao") orelse return null) { .string, .number_string => |s| s, else => return null, }; return .{ .lat = jsonFloat(block.get("lat")) orelse return null, .lon = jsonFloat(block.get("lon")) orelse return null, .raio_m = jsonInt(block.get("raio_m")) orelse return null, .precisao = precisao, }; } }; /// 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. /// /// ## Ownership /// /// Owns its strings. Call `deinit` with the allocator the `Client` was built /// with exactly once. pub const Health = struct { /// `"ok"` when the API considers itself healthy. status: []const u8 = "", /// Identifier of the fortnightly DNE database currently loaded. dne: []const u8 = "", /// How many CEPs the database holds. ceps: u64 = 0, /// How many of those carry a coordinate. geo: u64 = 0, /// When the database was generated, RFC 3339 in UTC. base_gerada_em: []const u8 = "", /// The response body exactly as it arrived. raw: []const u8 = "", /// Backing storage for the string fields. Internal. bytes: []u8 = &.{}, /// Release everything the value owns. Pass the allocator the `Client` was /// built with. pub fn deinit(h: *Health, gpa: Allocator) void { gpa.free(h.bytes); gpa.free(h.raw); h.* = undefined; } /// True when the API reports itself healthy. pub fn ok(h: Health) bool { return std.mem.eql(u8, h.status, "ok"); } /// Build from the JSON object the API returned. `body` is duplicated into /// `raw`; the returned value owns its memory — call `deinit`. pub fn fromJson(gpa: Allocator, value: std.json.Value, body: []const u8) Allocator.Error!Health { var scratch: [3][32]u8 = undefined; const status = jsonText(value, "status", &scratch[0]); const dne = jsonText(value, "dne", &scratch[1]); const base_gerada_em = jsonText(value, "base_gerada_em", &scratch[2]); const bytes = try gpa.alloc(u8, status.len + dne.len + base_gerada_em.len); errdefer gpa.free(bytes); const raw = try gpa.dupe(u8, body); errdefer gpa.free(raw); @memcpy(bytes[0..status.len], status); @memcpy(bytes[status.len..][0..dne.len], dne); @memcpy(bytes[status.len + dne.len ..][0..base_gerada_em.len], base_gerada_em); const maybe_object: ?std.json.ObjectMap = switch (value) { .object => |o| o, else => null, }; return .{ .status = bytes[0..status.len], .dne = bytes[status.len..][0..dne.len], .base_gerada_em = bytes[status.len + dne.len ..][0..base_gerada_em.len], .ceps = if (maybe_object) |o| jsonCount(o.get("ceps")) else 0, .geo = if (maybe_object) |o| jsonCount(o.get("geo")) else 0, .raw = raw, .bytes = bytes, }; } }; /// Read `key` off a JSON object as text. /// /// A string comes through as-is; a number is rendered into `scratch`; anything /// else — including a missing key and an explicit null — is the empty string, /// which is what the API sends for a field the database lacks. fn jsonText(value: std.json.Value, key: []const u8, scratch: []u8) []const u8 { const object = switch (value) { .object => |o| o, else => return "", }; return switch (object.get(key) orelse return "") { .string, .number_string => |s| s, .integer => |n| std.fmt.bufPrint(scratch, "{d}", .{n}) catch "", .float => |n| std.fmt.bufPrint(scratch, "{d}", .{n}) catch "", .bool => |b| if (b) "true" else "false", else => "", }; } fn jsonFloat(maybe: ?std.json.Value) ?f64 { return switch (maybe orelse return null) { .float => |n| n, .integer => |n| @floatFromInt(n), .number_string, .string => |s| std.fmt.parseFloat(f64, s) catch null, else => null, }; } fn jsonInt(maybe: ?std.json.Value) ?i64 { return switch (maybe orelse return null) { .integer => |n| n, .float => |n| @intFromFloat(n), .number_string, .string => |s| std.fmt.parseInt(i64, s, 10) catch null, else => null, }; } fn jsonCount(maybe: ?std.json.Value) u64 { return switch (maybe orelse return 0) { .integer => |n| if (n > 0) @intCast(n) else 0, .float => |n| if (n > 0) @intFromFloat(n) else 0, .number_string, .string => |s| std.fmt.parseInt(u64, s, 10) catch 0, else => 0, }; } const testing = std.testing; fn parseValue(body: []const u8) !std.json.Parsed(std.json.Value) { return std.json.parseFromSlice(std.json.Value, testing.allocator, body, .{}); } test "Address.fromJson reads every field and owns its strings" { const body = \\{"cep":"01001000","logradouro":"Praça da Sé","complemento":"lado ímpar", \\ "unidade":"","bairro":"Sé","localidade":"São Paulo","uf":"SP", \\ "estado":"São Paulo","regiao":"Sudeste","ibge":"3550308","gia":"1004", \\ "ddd":"11","siafi":"7107"} ; const parsed = try parseValue(body); defer parsed.deinit(); var address = try Address.fromJson(testing.allocator, parsed.value, body); defer address.deinit(testing.allocator); try testing.expectEqualStrings("01001000", address.cep); try testing.expectEqualStrings("Praça da Sé", address.logradouro); try testing.expectEqualStrings("São Paulo", address.localidade); try testing.expectEqualStrings("SP", address.uf); try testing.expectEqualStrings("7107", address.siafi); try testing.expectEqualStrings("", address.unidade); try testing.expect(address.geo == null); try testing.expectEqualStrings(body, address.raw); var buffer: [16]u8 = undefined; try testing.expectEqualStrings("01001-000", address.formattedCep(&buffer)); } test "Address.fromJson reads a geo block" { const body = \\{"cep":"01310100","geo":{"lat":-23.567202,"lon":-46.649659, \\ "raio_m":1084,"precisao":"logradouro"}} ; const parsed = try parseValue(body); defer parsed.deinit(); var address = try Address.fromJson(testing.allocator, parsed.value, body); defer address.deinit(testing.allocator); const geo = address.geo.?; try testing.expectApproxEqAbs(@as(f64, -23.567202), geo.lat, 1e-9); try testing.expectApproxEqAbs(@as(f64, -46.649659), geo.lon, 1e-9); try testing.expectEqual(@as(i64, 1084), geo.raio_m); try testing.expect(geo.isPrecise()); } test "a municipality coordinate is not precise" { const body = \\{"cep":"69900000","geo":{"lat":-9.9,"lon":-67.8,"raio_m":22000, \\ "precisao":"localidade"}} ; const parsed = try parseValue(body); defer parsed.deinit(); var address = try Address.fromJson(testing.allocator, parsed.value, body); defer address.deinit(testing.allocator); try testing.expect(!address.geo.?.isPrecise()); } test "a malformed geo block does not lose the address" { const body = \\{"cep":"01001000","localidade":"São Paulo","geo":{"lat":"not a number"}} ; const parsed = try parseValue(body); defer parsed.deinit(); var address = try Address.fromJson(testing.allocator, parsed.value, body); defer address.deinit(testing.allocator); try testing.expect(address.geo == null); try testing.expectEqualStrings("São Paulo", address.localidade); } test "an unknown field survives in raw and breaks nothing" { const body = \\{"cep":"01001000","localidade":"São Paulo","campo_do_futuro":"value"} ; const parsed = try parseValue(body); defer parsed.deinit(); var address = try Address.fromJson(testing.allocator, parsed.value, body); defer address.deinit(testing.allocator); try testing.expectEqualStrings("São Paulo", address.localidade); try testing.expect(std.mem.indexOf(u8, address.raw, "campo_do_futuro") != null); } test "a numeric field is rendered as text" { const body = \\{"cep":"01001000","ibge":3550308,"gia":null} ; const parsed = try parseValue(body); defer parsed.deinit(); var address = try Address.fromJson(testing.allocator, parsed.value, body); defer address.deinit(testing.allocator); try testing.expectEqualStrings("3550308", address.ibge); try testing.expectEqualStrings("", address.gia); } test "Address.fromCsvRecord maps by header and carries no geo" { const header = [_][]const u8{ "cep", "logradouro", "localidade", "uf", "coluna_nova" }; const record = [_][]const u8{ "01001000", "Praça da Sé", "São Paulo", "SP", "ignored" }; var address = try Address.fromCsvRecord(testing.allocator, &header, &record); defer address.deinit(testing.allocator); try testing.expectEqualStrings("01001000", address.cep); try testing.expectEqualStrings("Praça da Sé", address.logradouro); try testing.expectEqualStrings("SP", address.uf); try testing.expectEqualStrings("", address.siafi); try testing.expect(address.geo == null); try testing.expectEqualStrings("", address.raw); } test "formattedCep passes a malformed CEP through" { var address: Address = .{ .cep = "123" }; var buffer: [16]u8 = undefined; try testing.expectEqualStrings("123", address.formattedCep(&buffer)); } test "Health.fromJson reads counts and status" { const body = \\{"status":"ok","dne":"26071","ceps":1596168,"geo":894489, \\ "base_gerada_em":"2026-07-24T03:48:28Z"} ; const parsed = try parseValue(body); defer parsed.deinit(); var health = try Health.fromJson(testing.allocator, parsed.value, body); defer health.deinit(testing.allocator); try testing.expect(health.ok()); try testing.expectEqualStrings("26071", health.dne); try testing.expectEqual(@as(u64, 1_596_168), health.ceps); try testing.expectEqual(@as(u64, 894_489), health.geo); try testing.expectEqualStrings("2026-07-24T03:48:28Z", health.base_gerada_em); } test "a degraded Health is still a Health" { const body = \\{"status":"degradado"} ; const parsed = try parseValue(body); defer parsed.deinit(); var health = try Health.fromJson(testing.allocator, parsed.value, body); defer health.deinit(testing.allocator); try testing.expect(!health.ok()); try testing.expectEqual(@as(u64, 0), health.ceps); }