From a52dee116d4066d1b59bd90b4ebc4def4e1597d6 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Fri, 24 Jul 2026 23:14:23 -0500 Subject: 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. --- LocalAPIContract.swift | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 LocalAPIContract.swift (limited to 'LocalAPIContract.swift') 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 + } +} -- cgit v1.2.3