summaryrefslogtreecommitdiff
path: root/LocalAPIContract.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 /LocalAPIContract.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 'LocalAPIContract.swift')
-rw-r--r--LocalAPIContract.swift39
1 files changed, 39 insertions, 0 deletions
diff --git a/LocalAPIContract.swift b/LocalAPIContract.swift
new file mode 100644
index 0000000..f5f2463
--- /dev/null
+++ b/LocalAPIContract.swift
@@ -0,0 +1,39 @@
+import Foundation
+
+/// The versioned wire contract for the Local API.
+///
+/// External consumers — Shortcuts, scripts, and third-party integrations — depend
+/// on the JSON this API produces: the response envelope, the field names of every
+/// payload, and the encoding conventions. This type is the single source of truth
+/// for the parts that must stay stable, and `LocalAPIContractTests` pins them so
+/// an accidental rename or shape change fails CI instead of silently breaking a
+/// consumer.
+///
+/// ## Compatibility policy
+///
+/// The `version` string reported in every envelope follows a semantic-version-style
+/// promise:
+///
+/// - **Backward-compatible** changes keep `version` at `"v1"`: adding a new
+/// endpoint, or adding a new field to a payload. Consumers must ignore unknown
+/// fields, so additions never require a bump.
+/// - **Breaking** changes require bumping `version` (and updating `Docs/local-api.md`
+/// plus the contract tests): renaming or removing a field, changing a field's
+/// type, or changing the meaning/units of an existing field.
+///
+/// See `Docs/local-api.md` for the full endpoint and schema reference.
+enum LocalAPIContract {
+ /// Wire-format version reported in every envelope's `version` field.
+ static let version = "v1"
+
+ /// The canonical encoder for every Local API response. ISO-8601 dates and
+ /// sorted keys keep the output deterministic, which is what lets the contract
+ /// tests pin the shape. Both the success and error paths route through this so
+ /// the wire format can never drift between them.
+ static func makeEncoder() -> JSONEncoder {
+ let encoder = JSONEncoder()
+ encoder.dateEncodingStrategy = .iso8601
+ encoder.outputFormatting = [.sortedKeys]
+ return encoder
+ }
+}