@myrobotaxi/contracts 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +18 -4
- package/README.md +60 -2
- package/package.json +2 -1
package/CONTRIBUTING.md
CHANGED
|
@@ -11,8 +11,8 @@ This means any schema change requires:
|
|
|
11
11
|
1. Open a PR in [`myrobotaxi/telemetry`](https://github.com/myrobotaxi/telemetry) that edits the source `.schema.json` file under `docs/contracts/schemas/`. Get it reviewed and merged.
|
|
12
12
|
2. Open a paired PR in this repo that:
|
|
13
13
|
- Copies the new schema file from the telemetry repo into `schemas/`
|
|
14
|
-
- Runs `npm run codegen` to regenerate `src/generated/`
|
|
15
|
-
- Commits the regenerated `.ts` files
|
|
14
|
+
- Runs `npm run codegen` to regenerate `src/generated/` and `npm run codegen:swift` to regenerate `Sources/MyRobotaxiContracts/Generated/`
|
|
15
|
+
- Commits the regenerated `.ts` and `.swift` files
|
|
16
16
|
- Bumps the version per the [versioning policy](#versioning-policy)
|
|
17
17
|
|
|
18
18
|
Until [Phase 2](#phase-2-migration-authoring-home) ships, this paired-PR convention is enforced by review (no automation).
|
|
@@ -40,9 +40,23 @@ The MyRoboTaxi schemas use these non-standard keywords:
|
|
|
40
40
|
| `x-encrypted` / `x-encrypted-at-rest` | AES-256-GCM at rest |
|
|
41
41
|
| `x-tesla-proto-field` | Source Tesla protobuf field number |
|
|
42
42
|
|
|
43
|
-
The codegen pipeline (`scripts/codegen.mjs`) **folds these into TSDoc comments** at generation time, so they survive into the consumer's editor as `@classification "P0"` annotations.
|
|
43
|
+
The codegen pipeline (`scripts/codegen.mjs`) **folds these into TSDoc comments** at generation time, so they survive into the consumer's editor as `@classification "P0"` annotations. The Swift pipeline (`scripts/codegen-swift.mjs`) does the same folding into SwiftDoc (`///`) comments.
|
|
44
44
|
|
|
45
|
-
If you add a new `x-*` keyword to a schema, add it to the `ANNOTATIONS` array in `scripts/codegen.mjs` and re-run `npm run codegen`.
|
|
45
|
+
If you add a new `x-*` keyword to a schema, add it to the `ANNOTATIONS` array in **both** `scripts/codegen.mjs` and `scripts/codegen-swift.mjs`, then re-run `npm run codegen` and `npm run codegen:swift`.
|
|
46
|
+
|
|
47
|
+
## Swift codegen pipeline
|
|
48
|
+
|
|
49
|
+
The repo doubles as a Swift package (`Package.swift` at the root, product `MyRobotaxiContracts`). Swift types are generated from the same `schemas/*.json` source of truth:
|
|
50
|
+
|
|
51
|
+
- **Generator**: `scripts/codegen-swift.mjs` — a small custom Node script (zero npm dependencies; runs without `npm ci`). One `Codable`/`Equatable`/`Sendable` struct per root schema object and per `$defs` entry (websocket-protocol.md §9.2); string enums become **decode-tolerant** Swift enums — a `RawRepresentable` enum with the known cases plus a catch-all `case unrecognized(String)` (MYR-195). Unknown wire values decode to `.unrecognized(raw)` instead of throwing (so a newer contracts version adding an enum member never hard-fails an older installed client's whole-frame decode) and re-encode byte-identically. The generator emits a small suite of unit tests (`src/codegen-swift.test.ts`) alongside the Swift `swift test` round-trip tests.
|
|
52
|
+
- **Loud codegen errors**: to prevent silent wire drift, `scripts/codegen-swift.mjs` hard-errors (naming the schema path) rather than degrading when it hits a construct it can't faithfully map:
|
|
53
|
+
- a property enum whose members match a `$defs` enum but in a **different order** (would otherwise silently mint a near-duplicate nested type) — reorder the property enum to match the `$defs` order, or replace it with a `$ref`;
|
|
54
|
+
- schema composition (`oneOf` / `anyOf` / `allOf`) and **non-string enums** (would otherwise silently degrade to `JSONValue` / `Int`) — model the position as a concrete typed object, a string enum, or an explicit open `JSONValue`.
|
|
55
|
+
- **Output**: `Sources/MyRobotaxiContracts/Generated/*.swift` — **committed**, never hand-edited (same rule as `src/generated/`). Regeneration is deterministic: emission order follows the schema JSON, so re-running on an unchanged schema is a no-op diff.
|
|
56
|
+
- **Hand-written support code**: `Sources/MyRobotaxiContracts/JSONValue.swift` (arbitrary-JSON enum used for `WebSocketEnvelope.payload` and `VehicleUpdatePayload.fields`) is NOT generated and may be edited.
|
|
57
|
+
- **Regenerate**: `npm run codegen:swift` after any schema change, and commit the result alongside the regenerated TypeScript.
|
|
58
|
+
- **CI**: the `swift` job on `macos-latest` runs `swift build`, `swift test`, then `npm run codegen:swift` and fails on drift in `Sources/MyRobotaxiContracts/Generated/` — mirroring the TypeScript `codegen-drift` job.
|
|
59
|
+
- **Consumption**: Swift consumers resolve the package by git URL + tag (`from: "0.x.y"`); there is no Swift registry publish. The `v0.x.y` git tags pushed for npm releases are also what SwiftPM resolves against, so the versioning policy above applies to the Swift surface too.
|
|
46
60
|
|
|
47
61
|
## Release process
|
|
48
62
|
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ import { schemas } from '@myrobotaxi/contracts';
|
|
|
28
28
|
// schemas.vehicleState, schemas.wsMessages, schemas.wsEnvelope
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Raw JSON for non-JS consumers (Go
|
|
31
|
+
Raw JSON for non-JS consumers (Go):
|
|
32
32
|
|
|
33
33
|
```
|
|
34
34
|
./node_modules/@myrobotaxi/contracts/schemas/vehicle-state.schema.json
|
|
@@ -36,6 +36,60 @@ Raw JSON for non-JS consumers (Go, Swift):
|
|
|
36
36
|
./node_modules/@myrobotaxi/contracts/schemas/ws-envelope.schema.json
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## Swift
|
|
40
|
+
|
|
41
|
+
This repo is also a Swift package (`MyRobotaxiContracts`) with **pre-generated `Codable`/`Sendable` structs** under `Sources/MyRobotaxiContracts/Generated/` — produced by `scripts/codegen-swift.mjs` and committed for diff review, exactly like the TypeScript types. Swift consumers resolve by git URL + semver tag (no registry publish).
|
|
42
|
+
|
|
43
|
+
In your `Package.swift`:
|
|
44
|
+
|
|
45
|
+
```swift
|
|
46
|
+
dependencies: [
|
|
47
|
+
.package(url: "https://github.com/myrobotaxi/contracts.git", from: "0.4.0")
|
|
48
|
+
],
|
|
49
|
+
targets: [
|
|
50
|
+
.target(
|
|
51
|
+
name: "MyApp",
|
|
52
|
+
dependencies: [.product(name: "MyRobotaxiContracts", package: "contracts")]
|
|
53
|
+
)
|
|
54
|
+
]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
|
|
59
|
+
```swift
|
|
60
|
+
import MyRobotaxiContracts
|
|
61
|
+
|
|
62
|
+
let state = try JSONDecoder().decode(VehicleState.self, from: snapshotData)
|
|
63
|
+
let envelope = try JSONDecoder().decode(WebSocketEnvelope.self, from: frameData)
|
|
64
|
+
if envelope.type == .vehicleUpdate { /* decode VehicleUpdatePayload from envelope.payload */ }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The same custom annotations are folded into the generated SwiftDoc comments, so `grep '@classification "P1"'` works inside the checked-out package too. Wire enums (`status`, `chargeState`, `MessageType`, error codes, …) generate as **decode-tolerant** Swift enums: a `RawRepresentable` enum with the known cases plus a catch-all `case unrecognized(String)` (MYR-195). Nullable / non-required schema fields are Swift optionals. The intentionally-open wire positions — `WebSocketEnvelope.payload` and `VehicleUpdatePayload.fields` — are typed via the hand-written `JSONValue` enum, which preserves explicit JSON `null`s (required for atomic-group clears per NFR-3.9).
|
|
68
|
+
|
|
69
|
+
### Decode-tolerant wire enums (forward-compat for installed clients)
|
|
70
|
+
|
|
71
|
+
A plain `enum: String` (Swift's synthesized `Codable`) throws on the first unrecognized value, which would make **one** new enum member shipped in a newer contracts version hard-fail the *entire* frame/snapshot decode on a not-yet-updated iOS app. To keep installed clients forward-compatible, each string wire enum is emitted as:
|
|
72
|
+
|
|
73
|
+
```swift
|
|
74
|
+
public enum Status: RawRepresentable, Codable, Hashable, Sendable {
|
|
75
|
+
case driving
|
|
76
|
+
case parked
|
|
77
|
+
// …known cases…
|
|
78
|
+
/// A wire value not known to this build of the contracts package.
|
|
79
|
+
case unrecognized(String)
|
|
80
|
+
// init(rawValue:) / rawValue / init(from:) / encode(to:) map every string,
|
|
81
|
+
// falling back to .unrecognized(raw) instead of throwing.
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- **Unknown values never throw** — they decode to `.unrecognized(raw)`, so the surrounding `VehicleState` / `WebSocketEnvelope` still decodes.
|
|
86
|
+
- **Round-trips byte-identically** — the raw string is preserved and re-encoded verbatim, so a relaying client never corrupts a value it doesn't understand.
|
|
87
|
+
- **Ergonomic for consumers** — `switch status { case .driving: …; case .unrecognized(let raw): … }` stays exhaustive and compiler-checked; `== .parked` and `.rawValue` work as before. The catch-all is named `unrecognized` (not `unknown`) because `ChargeState` already has a known member whose wire value is `"Unknown"`.
|
|
88
|
+
|
|
89
|
+
## Tool choice (Swift): custom generator
|
|
90
|
+
|
|
91
|
+
Like the TypeScript side, the Swift generator is a small custom script (`scripts/codegen-swift.mjs`, no `quicktype`): it keeps the annotation-folding grep affordance, emits exactly one type per root schema and per `$defs` entry (websocket-protocol.md §9.2), needs no npm install (zero dependencies), and is deterministic so CI can diff the committed output.
|
|
92
|
+
|
|
39
93
|
## What's in the generated TypeScript
|
|
40
94
|
|
|
41
95
|
Custom JSON Schema annotations from the source schemas — `x-classification`, `x-atomic-group`, `x-unit`, `x-encrypted`, `x-tesla-proto-field` — are folded into the generated TSDoc comments at codegen time. This means consumers can `grep '@classification "P1"'` inside their `node_modules` to find every sensitive field without leaving their editor.
|
|
@@ -82,12 +136,16 @@ A planned follow-up moves the schema authoring home from `telemetry/docs/contrac
|
|
|
82
136
|
```bash
|
|
83
137
|
npm install
|
|
84
138
|
npm run codegen # regenerate src/generated/ from schemas/
|
|
139
|
+
npm run codegen:swift # regenerate Sources/MyRobotaxiContracts/Generated/ from schemas/
|
|
85
140
|
npm run typecheck
|
|
86
141
|
npm run test
|
|
87
142
|
npm run build # tsup → dist/
|
|
143
|
+
|
|
144
|
+
swift build # Swift package
|
|
145
|
+
swift test
|
|
88
146
|
```
|
|
89
147
|
|
|
90
|
-
CI runs
|
|
148
|
+
CI runs both codegens and fails if `src/generated/` or `Sources/MyRobotaxiContracts/Generated/` drifts from the schemas — every schema bump must be paired with a regenerate-and-commit (both languages).
|
|
91
149
|
|
|
92
150
|
## License
|
|
93
151
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myrobotaxi/contracts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Canonical wire-protocol schemas for the MyRoboTaxi platform. JSON Schema (draft-2020-12) + pre-generated TypeScript types. Consumed by SDKs (TS, Swift) and the Go telemetry server.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
],
|
|
53
53
|
"scripts": {
|
|
54
54
|
"codegen": "node scripts/codegen.mjs",
|
|
55
|
+
"codegen:swift": "node scripts/codegen-swift.mjs",
|
|
55
56
|
"build": "tsup",
|
|
56
57
|
"clean": "rm -rf dist coverage",
|
|
57
58
|
"lint": "eslint .",
|