diff options
| author | Christian Cleberg <[email protected]> | 2026-07-24 23:14:23 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-24 23:29:01 -0500 |
| commit | a52dee116d4066d1b59bd90b4ebc4def4e1597d6 (patch) | |
| tree | 77f852ffea7f6940710a532190225673275b9279 /LocalAPIService.swift | |
| parent | 7001577e6798530c943ac4cf03258bc172927a67 (diff) | |
| download | domain-dig-a52dee116d4066d1b59bd90b4ebc4def4e1597d6.tar.gz domain-dig-a52dee116d4066d1b59bd90b4ebc4def4e1597d6.tar.bz2 domain-dig-a52dee116d4066d1b59bd90b4ebc4def4e1597d6.zip | |
feat: stabilize and document the Local API v1 response contract (v5 step 3)
Second v5.0.0 roadmap item: make the Local API's public JSON contract explicit,
documented, and regression-locked, so external consumers (Shortcuts, scripts,
integrations) have a stable surface with a defined compatibility promise.
- LocalAPIContract: new single source of truth for the wire-format version
("v1") and the canonical JSON encoder (ISO-8601 dates, sorted keys). Both the
success and error paths in LocalAPIService now route through it, so the format
can't drift between them, and the ad-hoc per-call-site encoders are gone.
- The response envelope and every payload struct are promoted from `private` to
internal so the contract is a first-class, testable part of the module. The
transport/handler internals (request parser, HTTP response, secret store)
stay private.
- Docs/local-api.md documents the base URL/auth, the envelope, the encoding
conventions (notably: absent optionals are omitted, not null), every endpoint
and its payload fields, the error codes, and the semantic-version-style
compatibility policy (additive changes keep v1; renames/removals/type changes
bump the version). Linked from the README.
- LocalAPIContractTests: 16 structure/"golden" tests pinning the envelope shape,
each payload's field names, the enum encodings, and the ISO-8601 date format.
They assert structure, not values, so ordinary behavior changes don't churn
them but a renamed or dropped field fails CI. Full unit suite: 52 passing.
Diffstat (limited to 'LocalAPIService.swift')
| -rw-r--r-- | LocalAPIService.swift | 47 |
1 files changed, 19 insertions, 28 deletions
diff --git a/LocalAPIService.swift b/LocalAPIService.swift index 0a5f8a2..0ce7033 100644 --- a/LocalAPIService.swift +++ b/LocalAPIService.swift @@ -3,8 +3,6 @@ import Network import Observation import Security -private let localAPIVersion = "v1" - private enum LocalAPIServerError: LocalizedError { case missingSecret case secretPersistenceFailed @@ -508,12 +506,7 @@ private final class LocalAPIServer: @unchecked Sendable { } private struct LocalAPIRequestHandler { - private let encoder: JSONEncoder = { - let encoder = JSONEncoder() - encoder.dateEncodingStrategy = .iso8601 - encoder.outputFormatting = [.sortedKeys] - return encoder - }() + private let encoder = LocalAPIContract.makeEncoder() private let decoder: JSONDecoder = { let decoder = JSONDecoder() @@ -606,7 +599,7 @@ private struct LocalAPIRequestHandler { } private func successResponse<Value: Encodable>(_ value: Value) -> LocalAPIHTTPResponse { - let envelope = LocalAPIEnvelope(success: true, data: value, error: nil, version: localAPIVersion) + let envelope = LocalAPIEnvelope(success: true, data: value, error: nil, version: LocalAPIContract.version) guard let body = try? encoder.encode(envelope) else { return .error(statusCode: 500, code: "encoding_failed", message: "Could not encode the Local API response.") } @@ -836,12 +829,10 @@ private struct LocalAPIHTTPResponse { success: false, data: nil, error: LocalAPIErrorPayload(code: code, message: message), - version: localAPIVersion + version: LocalAPIContract.version ) - let encoder = JSONEncoder() - encoder.outputFormatting = [.sortedKeys] - let body = (try? encoder.encode(payload)) ?? Data() + let body = (try? LocalAPIContract.makeEncoder().encode(payload)) ?? Data() return LocalAPIHTTPResponse(statusCode: statusCode, body: body) } @@ -951,25 +942,25 @@ private enum LocalAPIHTTPParser { } } -private struct LocalAPIEnvelope<DataPayload: Encodable>: Encodable { +struct LocalAPIEnvelope<DataPayload: Encodable>: Encodable { let success: Bool let data: DataPayload? let error: LocalAPIErrorPayload? let version: String } -private struct LocalAPIErrorPayload: Encodable { +struct LocalAPIErrorPayload: Encodable { let code: String let message: String } -private struct EmptyPayload: Encodable {} +struct EmptyPayload: Encodable {} -private struct PortfolioPayload: Encodable { +struct PortfolioPayload: Encodable { let summary: PortfolioSummary } -private struct PortfolioSummary: Encodable { +struct PortfolioSummary: Encodable { let totalDomains: Int let healthyCount: Int let warningCount: Int @@ -979,26 +970,26 @@ private struct PortfolioSummary: Encodable { let unreachableCount: Int } -private struct DomainListPayload: Encodable { +struct DomainListPayload: Encodable { let domains: [TrackedDomain] } -private struct DomainDetailPayload: Encodable { +struct DomainDetailPayload: Encodable { let domain: String let trackedDomain: TrackedDomain? let latestReport: DomainReport? } -private struct DomainHistoryPayload: Encodable { +struct DomainHistoryPayload: Encodable { let domain: String let history: [HistoryEntry] } -private struct RecentEventsPayload: Encodable { +struct RecentEventsPayload: Encodable { let events: [RecentEventPayload] } -private struct RecentEventPayload: Encodable { +struct RecentEventPayload: Encodable { let timestamp: Date let domain: String let summary: String @@ -1006,14 +997,14 @@ private struct RecentEventPayload: Encodable { let severity: String } -private struct MonitoringPayload: Encodable { +struct MonitoringPayload: Encodable { let isEnabled: Bool let scope: MonitoringScope let alertsEnabled: Bool let monitoredDomains: [MonitoringDomainPayload] } -private struct MonitoringDomainPayload: Encodable { +struct MonitoringDomainPayload: Encodable { let domain: String let monitoringEnabled: Bool let lastMonitoredAt: Date? @@ -1021,15 +1012,15 @@ private struct MonitoringDomainPayload: Encodable { let certificateWarningLevel: CertificateWarningLevel } -private struct MonitoringMutationPayload: Encodable { +struct MonitoringMutationPayload: Encodable { let domain: String let monitoringEnabled: Bool } -private struct InspectRequestPayload: Decodable { +struct InspectRequestPayload: Decodable { let domain: String } -private struct InspectResponsePayload: Encodable { +struct InspectResponsePayload: Encodable { let report: DomainReport } |
