summaryrefslogtreecommitdiff
path: root/LocalAPIService.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-24 23:14:23 -0500
committerChristian Cleberg <[email protected]>2026-07-24 23:29:01 -0500
commita52dee116d4066d1b59bd90b4ebc4def4e1597d6 (patch)
tree77f852ffea7f6940710a532190225673275b9279 /LocalAPIService.swift
parent7001577e6798530c943ac4cf03258bc172927a67 (diff)
downloaddomain-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.swift47
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
}