ts-procedures 6.1.0 → 6.2.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.
Files changed (163) hide show
  1. package/agent_config/bin/setup.mjs +2 -2
  2. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +1 -0
  3. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +1 -1
  4. package/agent_config/claude-code/skills/ts-procedures-kotlin/SKILL.md +1 -1
  5. package/agent_config/claude-code/skills/ts-procedures-swift/SKILL.md +119 -0
  6. package/agent_config/copilot/copilot-instructions.md +1 -0
  7. package/agent_config/cursor/cursorrules +1 -0
  8. package/agent_config/lib/install-claude.mjs +1 -1
  9. package/build/codegen/bin/cli.d.ts +17 -3
  10. package/build/codegen/bin/cli.js +79 -3
  11. package/build/codegen/bin/cli.js.map +1 -1
  12. package/build/codegen/index.d.ts +18 -1
  13. package/build/codegen/index.js +3 -0
  14. package/build/codegen/index.js.map +1 -1
  15. package/build/codegen/pipeline.d.ts +16 -5
  16. package/build/codegen/pipeline.js +44 -143
  17. package/build/codegen/pipeline.js.map +1 -1
  18. package/build/codegen/targets/_shared/error-schemas.d.ts +10 -0
  19. package/build/codegen/targets/_shared/error-schemas.js +17 -0
  20. package/build/codegen/targets/_shared/error-schemas.js.map +1 -0
  21. package/build/codegen/targets/_shared/error-schemas.test.d.ts +1 -0
  22. package/build/codegen/targets/_shared/error-schemas.test.js +38 -0
  23. package/build/codegen/targets/_shared/error-schemas.test.js.map +1 -0
  24. package/build/codegen/targets/_shared/indent.d.ts +6 -0
  25. package/build/codegen/targets/_shared/indent.js +13 -0
  26. package/build/codegen/targets/_shared/indent.js.map +1 -0
  27. package/build/codegen/targets/_shared/indent.test.d.ts +1 -0
  28. package/build/codegen/targets/_shared/indent.test.js +21 -0
  29. package/build/codegen/targets/_shared/indent.test.js.map +1 -0
  30. package/build/codegen/targets/_shared/pascal-case.d.ts +6 -0
  31. package/build/codegen/targets/_shared/pascal-case.js +13 -0
  32. package/build/codegen/targets/_shared/pascal-case.js.map +1 -0
  33. package/build/codegen/targets/_shared/pascal-case.test.d.ts +1 -0
  34. package/build/codegen/targets/_shared/pascal-case.test.js +25 -0
  35. package/build/codegen/targets/_shared/pascal-case.test.js.map +1 -0
  36. package/build/codegen/targets/_shared/path-utils.d.ts +12 -0
  37. package/build/codegen/targets/_shared/path-utils.js +20 -0
  38. package/build/codegen/targets/_shared/path-utils.js.map +1 -0
  39. package/build/codegen/targets/_shared/path-utils.test.d.ts +1 -0
  40. package/build/codegen/targets/_shared/path-utils.test.js +42 -0
  41. package/build/codegen/targets/_shared/path-utils.test.js.map +1 -0
  42. package/build/codegen/targets/_shared/pick-defined.d.ts +11 -0
  43. package/build/codegen/targets/_shared/pick-defined.js +21 -0
  44. package/build/codegen/targets/_shared/pick-defined.js.map +1 -0
  45. package/build/codegen/targets/_shared/pick-defined.test.d.ts +1 -0
  46. package/build/codegen/targets/_shared/pick-defined.test.js +25 -0
  47. package/build/codegen/targets/_shared/pick-defined.test.js.map +1 -0
  48. package/build/codegen/targets/_shared/route-slots.d.ts +17 -0
  49. package/build/codegen/targets/_shared/route-slots.js +17 -0
  50. package/build/codegen/targets/_shared/route-slots.js.map +1 -0
  51. package/build/codegen/targets/_shared/route-slots.test.d.ts +1 -0
  52. package/build/codegen/targets/_shared/route-slots.test.js +43 -0
  53. package/build/codegen/targets/_shared/route-slots.test.js.map +1 -0
  54. package/build/codegen/targets/_shared/target-run.d.ts +27 -0
  55. package/build/codegen/targets/_shared/target-run.js +2 -0
  56. package/build/codegen/targets/_shared/target-run.js.map +1 -0
  57. package/build/codegen/targets/_shared/write-files.d.ts +24 -0
  58. package/build/codegen/targets/_shared/write-files.js +35 -0
  59. package/build/codegen/targets/_shared/write-files.js.map +1 -0
  60. package/build/codegen/targets/_shared/write-files.test.d.ts +1 -0
  61. package/build/codegen/targets/_shared/write-files.test.js +79 -0
  62. package/build/codegen/targets/_shared/write-files.test.js.map +1 -0
  63. package/build/codegen/targets/kotlin/e2e-compile.test.js +1 -1
  64. package/build/codegen/targets/kotlin/e2e-compile.test.js.map +1 -1
  65. package/build/codegen/targets/kotlin/emit-route-kotlin.js +5 -22
  66. package/build/codegen/targets/kotlin/emit-route-kotlin.js.map +1 -1
  67. package/build/codegen/targets/kotlin/emit-scope-kotlin.js +4 -8
  68. package/build/codegen/targets/kotlin/emit-scope-kotlin.js.map +1 -1
  69. package/build/codegen/targets/kotlin/format-kotlin.d.ts +0 -12
  70. package/build/codegen/targets/kotlin/format-kotlin.js +0 -27
  71. package/build/codegen/targets/kotlin/format-kotlin.js.map +1 -1
  72. package/build/codegen/targets/kotlin/format-kotlin.test.js +1 -34
  73. package/build/codegen/targets/kotlin/format-kotlin.test.js.map +1 -1
  74. package/build/codegen/targets/kotlin/integration.test.js +1 -1
  75. package/build/codegen/targets/kotlin/integration.test.js.map +1 -1
  76. package/build/codegen/targets/kotlin/run.d.ts +11 -0
  77. package/build/codegen/targets/kotlin/run.js +51 -0
  78. package/build/codegen/targets/kotlin/run.js.map +1 -0
  79. package/build/codegen/targets/swift/access-level.test.d.ts +1 -0
  80. package/build/codegen/targets/swift/access-level.test.js +98 -0
  81. package/build/codegen/targets/swift/access-level.test.js.map +1 -0
  82. package/build/codegen/targets/swift/ajsc-adapter.d.ts +27 -0
  83. package/build/codegen/targets/swift/ajsc-adapter.js +38 -0
  84. package/build/codegen/targets/swift/ajsc-adapter.js.map +1 -0
  85. package/build/codegen/targets/swift/ajsc-adapter.test.d.ts +1 -0
  86. package/build/codegen/targets/swift/ajsc-adapter.test.js +37 -0
  87. package/build/codegen/targets/swift/ajsc-adapter.test.js.map +1 -0
  88. package/build/codegen/targets/swift/e2e-compile.test.d.ts +1 -0
  89. package/build/codegen/targets/swift/e2e-compile.test.js +57 -0
  90. package/build/codegen/targets/swift/e2e-compile.test.js.map +1 -0
  91. package/build/codegen/targets/swift/emit-route-swift.d.ts +15 -0
  92. package/build/codegen/targets/swift/emit-route-swift.js +64 -0
  93. package/build/codegen/targets/swift/emit-route-swift.js.map +1 -0
  94. package/build/codegen/targets/swift/emit-route-swift.test.d.ts +1 -0
  95. package/build/codegen/targets/swift/emit-route-swift.test.js +258 -0
  96. package/build/codegen/targets/swift/emit-route-swift.test.js.map +1 -0
  97. package/build/codegen/targets/swift/emit-scope-swift.d.ts +13 -0
  98. package/build/codegen/targets/swift/emit-scope-swift.js +36 -0
  99. package/build/codegen/targets/swift/emit-scope-swift.js.map +1 -0
  100. package/build/codegen/targets/swift/emit-scope-swift.test.d.ts +1 -0
  101. package/build/codegen/targets/swift/emit-scope-swift.test.js +136 -0
  102. package/build/codegen/targets/swift/emit-scope-swift.test.js.map +1 -0
  103. package/build/codegen/targets/swift/format-swift.d.ts +2 -0
  104. package/build/codegen/targets/swift/format-swift.js +10 -0
  105. package/build/codegen/targets/swift/format-swift.js.map +1 -0
  106. package/build/codegen/targets/swift/format-swift.test.d.ts +1 -0
  107. package/build/codegen/targets/swift/format-swift.test.js +14 -0
  108. package/build/codegen/targets/swift/format-swift.test.js.map +1 -0
  109. package/build/codegen/targets/swift/integration.test.d.ts +1 -0
  110. package/build/codegen/targets/swift/integration.test.js +53 -0
  111. package/build/codegen/targets/swift/integration.test.js.map +1 -0
  112. package/build/codegen/targets/swift/run.d.ts +11 -0
  113. package/build/codegen/targets/swift/run.js +47 -0
  114. package/build/codegen/targets/swift/run.js.map +1 -0
  115. package/build/codegen/targets/ts/run.d.ts +4 -0
  116. package/build/codegen/targets/ts/run.js +86 -0
  117. package/build/codegen/targets/ts/run.js.map +1 -0
  118. package/docs/codegen-kotlin.md +1 -0
  119. package/docs/codegen-swift.md +314 -0
  120. package/docs/superpowers/specs/2026-04-24-kotlin-swift-codegen-design.md +1 -1
  121. package/docs/superpowers/specs/2026-04-25-ajsc-v7-kotlin-polish-design.md +1 -1
  122. package/docs/superpowers/specs/2026-04-25-swift-codegen-design.md +264 -0
  123. package/package.json +2 -2
  124. package/src/codegen/bin/cli.ts +91 -7
  125. package/src/codegen/index.ts +24 -1
  126. package/src/codegen/pipeline.ts +52 -174
  127. package/src/codegen/targets/_shared/error-schemas.test.ts +42 -0
  128. package/src/codegen/targets/_shared/error-schemas.ts +17 -0
  129. package/src/codegen/targets/_shared/indent.test.ts +25 -0
  130. package/src/codegen/targets/_shared/indent.ts +12 -0
  131. package/src/codegen/targets/_shared/pascal-case.test.ts +30 -0
  132. package/src/codegen/targets/_shared/pascal-case.ts +12 -0
  133. package/src/codegen/targets/_shared/path-utils.test.ts +51 -0
  134. package/src/codegen/targets/_shared/path-utils.ts +21 -0
  135. package/src/codegen/targets/_shared/pick-defined.test.ts +48 -0
  136. package/src/codegen/targets/_shared/pick-defined.ts +23 -0
  137. package/src/codegen/targets/_shared/route-slots.test.ts +55 -0
  138. package/src/codegen/targets/_shared/route-slots.ts +32 -0
  139. package/src/codegen/targets/_shared/target-run.ts +28 -0
  140. package/src/codegen/targets/_shared/write-files.test.ts +110 -0
  141. package/src/codegen/targets/_shared/write-files.ts +53 -0
  142. package/src/codegen/targets/kotlin/e2e-compile.test.ts +1 -1
  143. package/src/codegen/targets/kotlin/emit-route-kotlin.ts +5 -25
  144. package/src/codegen/targets/kotlin/emit-scope-kotlin.ts +4 -9
  145. package/src/codegen/targets/kotlin/format-kotlin.test.ts +0 -44
  146. package/src/codegen/targets/kotlin/format-kotlin.ts +0 -32
  147. package/src/codegen/targets/kotlin/integration.test.ts +1 -1
  148. package/src/codegen/targets/kotlin/run.ts +78 -0
  149. package/src/codegen/targets/swift/__fixtures__/users-golden.swift +123 -0
  150. package/src/codegen/targets/swift/access-level.test.ts +108 -0
  151. package/src/codegen/targets/swift/ajsc-adapter.test.ts +47 -0
  152. package/src/codegen/targets/swift/ajsc-adapter.ts +67 -0
  153. package/src/codegen/targets/swift/e2e-compile.test.ts +66 -0
  154. package/src/codegen/targets/swift/emit-route-swift.test.ts +300 -0
  155. package/src/codegen/targets/swift/emit-route-swift.ts +90 -0
  156. package/src/codegen/targets/swift/emit-scope-swift.test.ts +164 -0
  157. package/src/codegen/targets/swift/emit-scope-swift.ts +59 -0
  158. package/src/codegen/targets/swift/format-swift.test.ts +23 -0
  159. package/src/codegen/targets/swift/format-swift.ts +9 -0
  160. package/src/codegen/targets/swift/integration.test.ts +80 -0
  161. package/src/codegen/targets/swift/run.ts +74 -0
  162. package/src/codegen/targets/ts/run.ts +117 -0
  163. /package/src/codegen/{targets/kotlin/__fixtures__ → __fixtures__}/users-envelope.json +0 -0
@@ -0,0 +1,314 @@
1
+ # Swift Codegen Setup Guide
2
+
3
+ Generated by `ts-procedures-codegen --target swift`. One `.swift` file per scope; types are nested under route enum namespaces (`Users.GetUser.Response`, `Users.GetUser.Response.Address`, `Users.GetUser.Errors.NotFound`).
4
+
5
+ ## Quickstart
6
+
7
+ ```bash
8
+ npx ts-procedures-codegen \
9
+ --target swift \
10
+ --url https://api.example.com/_ts-procedures.json \
11
+ --out ./Sources/MyApp/Generated
12
+ ```
13
+
14
+ Each scope produces one file (e.g. `Users.swift`). Access generated types as `Users.GetUser.Response`, `Users.GetUser.PathParams`, `Users.GetUser.Errors.NotFound`.
15
+
16
+ Unlike the Kotlin target, **no package/module flag is required.** Swift modules are defined by Xcode/SPM targets, not by file-level declarations.
17
+
18
+ ## Swift Package Manager / Xcode integration
19
+
20
+ The generated files are plain `.swift` source — drop them into any target's source directory and they compile as-is. There is no required configuration.
21
+
22
+ ### Swift Package Manager
23
+
24
+ Point a target's `path:` (or `sources:`) at the directory you generated into:
25
+
26
+ ```swift
27
+ // Package.swift
28
+ let package = Package(
29
+ name: "MyApp",
30
+ products: [.library(name: "MyApp", targets: ["MyApp"])],
31
+ targets: [
32
+ .target(
33
+ name: "MyApp",
34
+ path: "Sources/MyApp"
35
+ // Generated files live under Sources/MyApp/Generated/ and
36
+ // are picked up automatically by SPM's recursive source globbing.
37
+ ),
38
+ ]
39
+ )
40
+ ```
41
+
42
+ If you keep the generated dir as its own target, declare it as a dependency of any target that needs to call the API:
43
+
44
+ ```swift
45
+ .target(name: "MyAppAPI", path: "Sources/MyApp/Generated"),
46
+ .target(name: "MyApp", dependencies: ["MyAppAPI"]),
47
+ ```
48
+
49
+ ### Xcode (project-based)
50
+
51
+ In Xcode: **File → Add Files to "<TargetName>"…**, select the generated directory, and ensure the target membership checkbox is set. Re-running codegen overwrites the files in place; Xcode picks up the changes on next build.
52
+
53
+ ## JSONDecoder configuration
54
+
55
+ **This is required.** Schemas with `format: date-time` are emitted as `Foundation.Date`. Decoding fails (`DecodingError.typeMismatch`) unless you tell `JSONDecoder` how to parse the wire format:
56
+
57
+ ```swift
58
+ let decoder = JSONDecoder()
59
+ decoder.dateDecodingStrategy = .iso8601
60
+ ```
61
+
62
+ For symmetric encoding (e.g. when sending request bodies):
63
+
64
+ ```swift
65
+ let encoder = JSONEncoder()
66
+ encoder.dateEncodingStrategy = .iso8601
67
+ ```
68
+
69
+ The server emits ISO-8601 strings by default; `.iso8601` is the matching strategy on the Swift side. If your server emits epoch-millis or another format, swap to `.millisecondsSince1970` / `.formatted(_:)` / `.custom(_:)` accordingly.
70
+
71
+ `format: uuid` and `format: uri` map to `Foundation.UUID` / `Foundation.URL`, both of which are `Codable` natively and need no extra configuration.
72
+
73
+ ## Sample output
74
+
75
+ Given a `users` scope with a single `GetUser` route declaring path params, a response with a nested `Address`, and a `NotFound` error:
76
+
77
+ ```swift
78
+ // Source hash: 9a1b3c…
79
+ // Generated by ts-procedures-codegen — do not edit.
80
+ import Foundation
81
+
82
+ public enum Users {
83
+ public enum GetUser {
84
+ public static let method = "GET"
85
+ public static let pathTemplate = "/users/{id}"
86
+ public static func path(_ p: PathParams) -> String { return "/users/\(p.id)" }
87
+
88
+ public struct PathParams: Codable {
89
+ public let id: String
90
+ }
91
+
92
+ public struct Response: Codable {
93
+ public let id: String
94
+ public let name: String
95
+ /// ISO-8601 — set JSONDecoder.dateDecodingStrategy = .iso8601
96
+ public let createdAt: Date
97
+ public let address: Address
98
+
99
+ enum CodingKeys: String, CodingKey {
100
+ case id, name
101
+ case createdAt = "created-at"
102
+ case address
103
+ }
104
+
105
+ public struct Address: Codable {
106
+ public let street: String
107
+ public let city: String
108
+ }
109
+ }
110
+
111
+ public enum Errors {
112
+ public struct NotFound: Codable {
113
+ public let name: String
114
+ public let message: String
115
+ }
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ Routes without path params get `public static let path = "/users"` (a constant, not a function).
122
+
123
+ ## Discriminated unions
124
+
125
+ A `oneOf` whose variants share a const-valued discriminator (e.g. `kind: "guest" | "registered"`) emits as a Swift `enum` with associated values. Because Swift's standard `Codable` has no built-in tagged-union support, ajsc emits a hand-rolled `init(from:)` / `encode(to:)` that dispatches on the discriminator field:
126
+
127
+ ```swift
128
+ public enum Body: Codable {
129
+ case guest(Guest)
130
+ case registered(Registered)
131
+
132
+ public struct Guest: Codable {
133
+ public let displayName: String
134
+ }
135
+
136
+ public struct Registered: Codable {
137
+ public let email: String
138
+ public let name: String
139
+ }
140
+
141
+ private enum DiscriminatorKeys: String, CodingKey { case kind }
142
+
143
+ public init(from decoder: Decoder) throws {
144
+ let c = try decoder.container(keyedBy: DiscriminatorKeys.self)
145
+ let kind = try c.decode(String.self, forKey: .kind)
146
+ switch kind {
147
+ case "guest": self = .guest(try Guest(from: decoder))
148
+ case "registered": self = .registered(try Registered(from: decoder))
149
+ default: throw DecodingError.dataCorruptedError(
150
+ forKey: .kind, in: c,
151
+ debugDescription: "Unknown discriminator: \(kind)")
152
+ }
153
+ }
154
+
155
+ public func encode(to encoder: Encoder) throws {
156
+ switch self {
157
+ case .guest(let v): try v.encode(to: encoder)
158
+ case .registered(let v): try v.encode(to: encoder)
159
+ }
160
+ }
161
+ }
162
+ ```
163
+
164
+ **Nothing for the consumer to configure.** The discriminator handling is baked into the generated code — `JSONDecoder().decode(Body.self, from: data)` works directly.
165
+
166
+ The discriminator field (`kind`) is **retained** on each variant struct (unlike the Kotlin target, which erases it via `@SerialName`). This is a Codable necessity: the variant struct's `init(from:)` is invoked with the same decoder that just read the discriminator, so the field has to be present in the variant's shape too.
167
+
168
+ ## JSON-key sanitization
169
+
170
+ Kebab-case and snake-case JSON keys become camelCase Swift property names; ajsc auto-emits a nested `enum CodingKeys: String, CodingKey` to map between them:
171
+
172
+ ```swift
173
+ public struct Response: Codable {
174
+ public let createdAt: Date
175
+ public let userName: String
176
+
177
+ enum CodingKeys: String, CodingKey {
178
+ case createdAt = "created-at"
179
+ case userName = "user_name"
180
+ }
181
+ }
182
+ ```
183
+
184
+ Reserved Swift keywords get backtick-escaped: a JSON key `class` becomes `` `class` `` on the Swift side (still valid as a property name; the surrounding code references it as `value.`class`).
185
+
186
+ ## Switching off Codable (`--swift-serializer none`)
187
+
188
+ `--swift-serializer none` emits plain structs **without `Codable` conformance and without `CodingKeys`**:
189
+
190
+ ```swift
191
+ public struct Response {
192
+ public let id: String
193
+ public let createdAt: Date
194
+ }
195
+ ```
196
+
197
+ Use cases:
198
+
199
+ - You're using **SwiftyJSON**, **Argo**, or another non-Codable serialization library.
200
+ - You want to hand-roll `Codable` conformance in extensions (e.g. to centralize `dateDecodingStrategy` per type, or to handle a Codable-incompatible shape).
201
+ - You're integrating with an Objective-C bridge that doesn't speak `Codable`.
202
+
203
+ With `none`, the consumer is fully responsible for serialization. CodingKeys are NOT emitted — kebab/snake-case keys aren't sanitized at all on the wire side, so you'll need to handle that mapping yourself.
204
+
205
+ ## `--swift-access-level public | internal`
206
+
207
+ Defaults to `public`. Pass `--swift-access-level internal` when the generated types are consumed only within a single module (e.g. an app target that talks to one backend) and you don't want them appearing in the module's public ABI.
208
+
209
+ ```bash
210
+ npx ts-procedures-codegen --target swift --swift-access-level internal --url ... --out ...
211
+ ```
212
+
213
+ The flag threads through to ajsc's `accessLevel` option and applies uniformly to every emitted type, the namespace enums, and the static `method` / `path` / `pathTemplate` declarations.
214
+
215
+ ## Error types
216
+
217
+ Each route that declares errors gets a nested `Errors` enum (caseless namespace) containing one `Codable` struct per error name:
218
+
219
+ ```swift
220
+ public enum Users {
221
+ public enum GetUser {
222
+ // ... method, path, types ...
223
+ public enum Errors {
224
+ public struct NotFound: Codable {
225
+ public let name: String
226
+ public let message: String
227
+ }
228
+ }
229
+ }
230
+ }
231
+ ```
232
+
233
+ Access generated error types as `Users.GetUser.Errors.NotFound`.
234
+
235
+ **No runtime dispatch.** Like the Kotlin target, the Swift target ships **types only** — there is no error registry, no `instanceof`-style lookup, no `dispatchTypedError`. Consumers catch HTTP failures themselves and dispatch on status code or `body.name`:
236
+
237
+ ```swift
238
+ func loadUser(id: String) async throws -> Users.GetUser.Response {
239
+ var req = URLRequest(url: URL(string: "https://api.example.com" + Users.GetUser.path(.init(id: id)))!)
240
+ req.httpMethod = Users.GetUser.method
241
+
242
+ let (data, response) = try await URLSession.shared.data(for: req)
243
+ guard let http = response as? HTTPURLResponse else {
244
+ throw URLError(.badServerResponse)
245
+ }
246
+
247
+ let decoder = JSONDecoder()
248
+ decoder.dateDecodingStrategy = .iso8601
249
+
250
+ switch http.statusCode {
251
+ case 200:
252
+ return try decoder.decode(Users.GetUser.Response.self, from: data)
253
+ case 404:
254
+ throw try decoder.decode(Users.GetUser.Errors.NotFound.self, from: data)
255
+ default:
256
+ throw URLError(.badServerResponse)
257
+ }
258
+ }
259
+ ```
260
+
261
+ For the error structs to also conform to `Error`, declare a single-line empty extension in your own code (kept out of generated files so codegen overwrites are safe):
262
+
263
+ ```swift
264
+ extension Users.GetUser.Errors.NotFound: Error {}
265
+ ```
266
+
267
+ Choosing the dispatch strategy (status-code, `body.name`, your own `enum APIError: Error { case ... }` wrapper, etc.) is intentionally left to consumers.
268
+
269
+ ## Untagged unions
270
+
271
+ **Unlike the Kotlin target, `--unsupported-unions fallback` actually works on Swift.** ajsc emits a self-contained `AnyCodable` helper struct directly inside the generated file (no external dependency, no separate runtime to install) to model schemas that use untagged `anyOf` / `oneOf` with no shared discriminator.
272
+
273
+ Default: `--unsupported-unions throw` — codegen raises an error with the schema path of the offending union, so you can fix it at the source.
274
+
275
+ ```bash
276
+ # Opt in to the fallback. Generated code becomes:
277
+ npx ts-procedures-codegen --target swift --unsupported-unions fallback --url ... --out ...
278
+ ```
279
+
280
+ ```swift
281
+ public struct AnyCodable: Codable {
282
+ public let value: Any
283
+ // ... init(from:), encode(to:) handle Bool/Int/Double/String/Array/Dictionary/null
284
+ }
285
+
286
+ public struct MixedField: Codable {
287
+ public let value: AnyCodable // was: oneOf: [{ type: 'string' }, { type: 'integer' }]
288
+ }
289
+ ```
290
+
291
+ You lose static typing on the union'd value — consumers introspect at runtime. **Prefer adding a discriminator** to the server-side schema if at all possible; the `fallback` mode is an escape hatch for third-party / locked schemas you can't change.
292
+
293
+ ## Documented limitations
294
+
295
+ The following ajsc behaviors are intentional and documented; they are **not bugs**:
296
+
297
+ - **`format: date` and `format: time` map to `String`.** Swift's Foundation has no native date-only or time-only type (`Date` is a point-in-time, not a calendar date). Parse manually with `DateFormatter` if you need a typed value.
298
+ - **`type: integer` maps to `Int64`** (not `Int`). Reason: 32-bit Apple platforms (older watchOS, some embedded targets) have 32-bit `Int`. `Int64` guarantees range parity with the JSON Schema integer type.
299
+ - **`type: number` maps to `Double`.** For monetary or other precision-sensitive values, decode into `Double` and convert to `Decimal` at the boundary:
300
+ ```swift
301
+ let amount = Decimal(response.totalPrice)
302
+ ```
303
+ This is a one-time conversion at the parse boundary; subsequent arithmetic on `Decimal` is precision-safe.
304
+ - **`additionalProperties: { type: T }` is silently dropped** with a `/// Note: schema permits additional keys of type T — not modeled.` doc-comment. Add a sibling `[String: T]` field by hand or write a custom `init(from:)` if your contract uses extra keys.
305
+ - **Heterogeneous tuples throw under Codable.** Swift tuples are not `Codable`. Schemas with positional-tuple `items: [...]` arrays throw at codegen time. Refactor to a struct schema upstream.
306
+ - **`not` and `patternProperties` keywords throw at codegen time** with a path-bearing error message. These don't have idiomatic Swift mappings; the schema needs simplification at the source.
307
+ - **Schema-level `examples` are not modeled.** They're documentation-only on the server side; consumers don't see them.
308
+
309
+ ## Reference
310
+
311
+ - Spec: [`docs/superpowers/specs/2026-04-25-swift-codegen-design.md`](./superpowers/specs/2026-04-25-swift-codegen-design.md)
312
+ - For Kotlin / Android consumers, see [`docs/codegen-kotlin.md`](./codegen-kotlin.md) — same types-only design with Kotlin-specific flags and `kotlinx.serialization` setup notes.
313
+ - ajsc README: `node_modules/ajsc/README.md` (or [npmjs.com/package/ajsc](https://www.npmjs.com/package/ajsc))
314
+ - ts-procedures-codegen CLI flags: see `CLAUDE.md` (search for "Swift target") and the spec linked above. (`--help` is not currently implemented; pass invalid/missing args to see error messages with usage hints.)
@@ -1,6 +1,6 @@
1
1
  # Kotlin & Swift Codegen for ts-procedures
2
2
 
3
- **Status:** Design
3
+ **Status:** Shipped (both targets — Kotlin and Swift)
4
4
  **Date:** 2026-04-24
5
5
  **Author:** Cory Robinson
6
6
  **Implementation order:** Kotlin first, Swift second.
@@ -1,6 +1,6 @@
1
1
  # ajsc v7.2 Kotlin Codegen Polish
2
2
 
3
- **Status:** Design
3
+ **Status:** Shipped
4
4
  **Date:** 2026-04-25
5
5
  **Author:** Cory Robinson
6
6
  **Supersedes parts of:** [`2026-04-24-kotlin-swift-codegen-design.md`](./2026-04-24-kotlin-swift-codegen-design.md) — Kotlin sections only. Swift sections of the prior design remain in force and unscheduled.
@@ -0,0 +1,264 @@
1
+ # Swift Codegen Target — Design & Implementation Plan
2
+
3
+ Date: 2026-04-25
4
+ Branch: codegen-kotlin-swift
5
+ Status: shipped
6
+
7
+ ## Outcome
8
+
9
+ Implemented as designed: `--target swift`, `--swift-serializer`, `--swift-access-level`, full integration test against `users-golden.swift`, plus a `swiftc -parse` e2e gated by `swiftc` availability. One post-implementation deviation worth noting: the originally-spec'd files in `src/codegen/targets/swift/` were later supplemented by an extracted `src/codegen/targets/_shared/` directory of language-agnostic utilities (`path-utils`, `pick-defined`, `indent`, `pascal-case`, `route-slots`, `error-schemas`, `write-files`, `target-run`). The Swift run module (`src/codegen/targets/swift/run.ts`) and the Kotlin run module both consume `_shared/` so the dispatcher in `src/codegen/pipeline.ts` stays thin (~94 lines).
10
+
11
+ ## Goal
12
+
13
+ Add a `--target swift` codegen target to `ts-procedures-codegen` that emits idiomatic, types-only Swift source from a `DocEnvelope`. Design mirrors the existing Kotlin target so iOS / Apple-platform consumers get parity DX with Android consumers — both targets are types-only, no runtime, no error registry, no HTTP adapter.
14
+
15
+ ## Non-goals
16
+
17
+ - HTTP client, networking, async/await wrappers (consumers own this).
18
+ - SSE / streams (skipped, same as Kotlin).
19
+ - Hooks, per-call options, error dispatch logic.
20
+ - Swift Package Manager scaffolding (consumers create `Package.swift` themselves).
21
+ - Module declarations (Swift modules are defined by Xcode/SPM targets, not per-file like Kotlin packages).
22
+ - ObjC interop, `@objc` annotations.
23
+
24
+ ## Output shape
25
+
26
+ One `.swift` file per scope. Idiomatic Swift namespacing via **caseless enums** (the standard Swift idiom for namespaces — uninstantiable, zero runtime cost):
27
+
28
+ ```swift
29
+ // Source hash: <md5>
30
+ // Generated by ts-procedures-codegen — do not edit.
31
+ import Foundation
32
+
33
+ public enum Users {
34
+ public enum GetUser {
35
+ public static let method = "GET"
36
+ public static let pathTemplate = "/users/{id}"
37
+ public static func path(_ p: PathParams) -> String { return "/users/\(p.id)" }
38
+
39
+ public struct PathParams: Codable {
40
+ public let id: String
41
+ }
42
+
43
+ public struct Response: Codable {
44
+ public let id: String
45
+ public let name: String
46
+ /// ISO-8601 — set JSONDecoder.dateDecodingStrategy = .iso8601
47
+ public let createdAt: Date
48
+ public let address: Address
49
+
50
+ enum CodingKeys: String, CodingKey {
51
+ case id, name
52
+ case createdAt = "created-at"
53
+ case address
54
+ }
55
+
56
+ public struct Address: Codable {
57
+ public let street: String
58
+ public let city: String
59
+ }
60
+ }
61
+
62
+ public enum Errors {
63
+ public struct NotFound: Codable {
64
+ public let name: String
65
+ public let message: String
66
+ }
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ Routes without path params get `public static let path = "/users"` (constant, not function).
73
+
74
+ ## CLI surface
75
+
76
+ New flag: `--target swift` (extends current `'ts' | 'kotlin'` union to `'ts' | 'kotlin' | 'swift'`).
77
+
78
+ Swift-specific flags:
79
+
80
+ | Flag | Default | Notes |
81
+ |---|---|---|
82
+ | `--swift-serializer <codable\|none>` | `codable` | Emits `: Codable`/`CodingKeys`. `none` for plain structs (consumer handles serialization). |
83
+ | `--swift-access-level <public\|internal>` | `public` | Threads through to ajsc `accessLevel`. |
84
+
85
+ Reused (apply to all targets): `--unsupported-unions`, `--array-item-naming`, `--depluralize`, `--uncountable-words`.
86
+
87
+ **Important:** Unlike Kotlin, `--unsupported-unions fallback` actually works on Swift — ajsc emits a self-contained `AnyCodable` helper struct. Do NOT add the kotlin-style "no-op warning" for Swift.
88
+
89
+ **No `--swift-package` flag.** Swift has no file-level package/module declaration; modules are defined by SPM/Xcode targets. This is a deliberate DX win — one fewer required arg vs Kotlin.
90
+
91
+ ## Config file shape
92
+
93
+ ```json
94
+ {
95
+ "target": "swift",
96
+ "swift": {
97
+ "serializer": "codable",
98
+ "accessLevel": "public"
99
+ }
100
+ }
101
+ ```
102
+
103
+ ## File layout
104
+
105
+ ```
106
+ src/codegen/targets/swift/
107
+ ├── ajsc-adapter.ts # SwiftEmitter interface + production wrapper around ajsc.emitSwift
108
+ ├── ajsc-adapter.test.ts
109
+ ├── format-swift.ts # header, imports, indent, pickDefined helpers
110
+ ├── format-swift.test.ts
111
+ ├── emit-route-swift.ts # per-route emitter (PathParams, Query, Body, Response, Errors)
112
+ ├── emit-route-swift.test.ts
113
+ ├── emit-scope-swift.ts # per-scope emitter (wraps routes in nested enum namespace)
114
+ ├── emit-scope-swift.test.ts
115
+ ├── integration.test.ts # golden-file end-to-end with stub emitter
116
+ ├── e2e-compile.test.ts # SKIPPED by default — runs swiftc to validate generated output
117
+ └── __fixtures__/
118
+ ├── users-envelope.json # copy of kotlin fixture (same input)
119
+ └── users-golden.swift
120
+ ```
121
+
122
+ ## Architectural decisions
123
+
124
+ ### 1. Namespacing via caseless enums
125
+
126
+ Swift idiom: `public enum Users {}` for namespace. Better than `struct Users {}` (no init, no instantiation possible, zero runtime cost). Better than nested `class` (reference type, unnecessary).
127
+
128
+ ### 2. Static members for path/method
129
+
130
+ Inside an enum namespace, all members must be `static`. So:
131
+ ```swift
132
+ public static let method = "GET"
133
+ public static let pathTemplate = "/users/{id}"
134
+ public static func path(_ p: PathParams) -> String { return "/users/\(p.id)" }
135
+ ```
136
+
137
+ (Kotlin uses `const val` and top-level `fun`; Swift uses `public static let` and `public static func`.)
138
+
139
+ ### 3. ajsc options threaded
140
+
141
+ Per route slot we call `emitSwift(slotSchema, { rootTypeName, inlineTypes: true, ...passthrough })`. Passthrough: `serializer`, `accessLevel`, `unsupportedUnions`, `arrayItemNaming`, `depluralize`, `uncountableWords`.
142
+
143
+ `inlineTypes: true` is critical — same as Kotlin. Without it, nested object types extract to siblings and clutter the namespace.
144
+
145
+ ### 4. Imports merge per file
146
+
147
+ ajsc returns imports per emit call (typically `["Foundation"]` only when Date/UUID/URL is in the schema). We dedupe + sort and emit once at the top of the scope file.
148
+
149
+ ### 5. No errors file, no index file
150
+
151
+ Mirrors Kotlin: errors are nested as `enum Errors { struct NotFound: Codable { ... } }` per route. No `_errors.swift`, no barrel/index, no factories.
152
+
153
+ ### 6. Path interpolation
154
+
155
+ Swift string interpolation is `\(expr)` (backslash + paren). Path builder template:
156
+ ```swift
157
+ public static func path(_ p: PathParams) -> String { return "/users/\(p.id)" }
158
+ ```
159
+
160
+ For no-params: `public static let path = "/users"`.
161
+
162
+ ### 7. Source hash header
163
+
164
+ `// Source hash: <md5>` as second line (after the optional `// Generated by ...` line). Same as kotlin/ts.
165
+
166
+ ### 8. Stream skipping
167
+
168
+ Same as Kotlin — skip stream routes with a single console log per scope.
169
+
170
+ ### 9. Reserved word + identifier sanitization
171
+
172
+ ajsc handles this via `sanitizeSwiftIdentifier` (already verified in ajsc/swift exports). Property and case names are already sanitized in ajsc's output. Our route/scope names use PascalCase already; we don't need to re-sanitize.
173
+
174
+ ### 10. Self-contained emitter (no extra runtime files)
175
+
176
+ Like Kotlin: zero runtime files emitted. ajsc handles `AnyCodable` inline when `unsupportedUnions: 'fallback'` is set — we don't emit a separate helper file.
177
+
178
+ ## Pipeline integration
179
+
180
+ `src/codegen/pipeline.ts` gets a third branch parallel to kotlin:
181
+
182
+ ```ts
183
+ if (options.target === 'kotlin') { ... }
184
+ if (options.target === 'swift') {
185
+ if (options.swiftEmitter == null) throw new Error(...)
186
+ // ... build errorSchemas, iterate groups, call emitSwiftScope, write files
187
+ }
188
+ ```
189
+
190
+ Naming inside `PipelineOptions`:
191
+ - `target?: 'ts' | 'kotlin' | 'swift'`
192
+ - `swiftSerializer?: 'codable' | 'none'`
193
+ - `swiftAccessLevel?: 'public' | 'internal'`
194
+ - `swiftEmitter?: SwiftEmitter` (test-injection hook, mirrors `kotlinEmitter`)
195
+
196
+ Reused: `unsupportedUnions` (already on options).
197
+
198
+ ## CLI integration
199
+
200
+ `src/codegen/bin/cli.ts`:
201
+
202
+ 1. Extend `target` parser to accept `'swift'`.
203
+ 2. Add flags: `--swift-serializer`, `--swift-access-level`.
204
+ 3. Add `swift?: { serializer?, accessLevel? }` to `CodegenConfig` and `ParsedArgs`.
205
+ 4. No package validation (Swift has no required field).
206
+ 5. Extend `printPostRunHints` for swift target → link `docs/codegen-swift.md`.
207
+ 6. Watch-mode: resolve swift emitter once at startup, parallel to kotlin.
208
+ 7. Do NOT add a Swift no-op-flag warning for `--unsupported-unions` (it works on Swift).
209
+
210
+ ## Tests
211
+
212
+ Mirror the Kotlin test layout:
213
+
214
+ 1. **`ajsc-adapter.test.ts`** — stub creator + production resolver (mocking `ajsc` module to assert error messages when missing or `emitSwift` is undefined).
215
+ 2. **`format-swift.test.ts`** — `swiftHeader`, `swiftImports` dedupe+sort, `indent`, `pickDefined`.
216
+ 3. **`emit-route-swift.test.ts`** — path-builder for params/no-params, slot order (PathParams, Query, Body, Response), Errors namespace, stream-skipping.
217
+ 4. **`emit-scope-swift.test.ts`** — PascalCase scope name, file-name `Foo.swift`, imports dedupe across routes, empty scope, option threading.
218
+ 5. **`integration.test.ts`** — golden-file test using the `users-envelope.json` fixture and a hand-stubbed emitter, byte-identical against `users-golden.swift`.
219
+ 6. **`e2e-compile.test.ts`** — `it.skipIf(no swiftc)` — invokes `swiftc -parse` on the generated file to verify syntactic correctness. Skipped by default; only runs when `swiftc` is on `PATH`.
220
+
221
+ Tests inject `createStubSwiftEmitter()` so output is deterministic and independent of ajsc's per-version evolution. The E2E compile test is the only one that exercises real ajsc.
222
+
223
+ ## Documentation
224
+
225
+ 1. **`docs/codegen-swift.md`** — full setup guide:
226
+ - Quickstart example
227
+ - SPM/Xcode integration (target setup, file inclusion)
228
+ - JSONDecoder configuration (`.iso8601` for Date)
229
+ - Sample output
230
+ - Discriminated unions (Swift uses `init(from:)` / `encode(to:)`)
231
+ - JSON-key sanitization (CodingKeys)
232
+ - `--swift-serializer none` (when consumers want plain structs for SwiftJSON or hand-rolled coding)
233
+ - Error types (nested under `Errors`)
234
+ - Untagged unions (`fallback` works on Swift, emits `AnyCodable`)
235
+ - Documented limitations
236
+
237
+ 2. **`CLAUDE.md`** — add Swift target paragraph parallel to Kotlin; emphasize differences (`--unsupported-unions fallback` works for Swift; no `--swift-package` requirement).
238
+
239
+ 3. **`agent_config/claude-code/skills/ts-procedures-swift/SKILL.md`** — parallel to `ts-procedures-kotlin/SKILL.md`. Cross-link both kotlin and swift skills from the main `ts-procedures` skill.
240
+
241
+ ## Implementation phases
242
+
243
+ **Phase 1 (foundation):** ajsc-adapter, format helpers, emit-route, emit-scope (+ unit tests for each). Single sub-agent.
244
+
245
+ **Phase 2 (integration):** Wire into pipeline.ts, index.ts, cli.ts. Add integration test with golden file. Single sub-agent depending on Phase 1.
246
+
247
+ **Phase 3 (docs+skill, parallel with Phase 1/2):** Write `docs/codegen-swift.md`, update CLAUDE.md, create agent_config skill. Single sub-agent independent of code.
248
+
249
+ **Phase 4 (verify):** `npm run build && npm test && npm run lint`. Inspect generated golden file. Confirm CLI invocation works.
250
+
251
+ ## Risk / open questions
252
+
253
+ - **swiftc on dev machines** — E2E test is skipped by default to avoid CI failures. Same pattern as kotlin's `e2e-compile.test.ts`.
254
+ - **`type: integer` → `Int64`** — Swift idiom is `Int` for general use; `Int64` is heavier visually. Accept ajsc's choice for now (can revisit if mobile devs request `Int`).
255
+ - **`type: number` → `Double`** — for monetary values, mobile devs should use `Decimal`. Document the workaround in `docs/codegen-swift.md`.
256
+ - **CodingKeys are mandatory** when keys differ — ajsc emits these automatically. We just have to make sure our golden file includes them and tests don't lock them away.
257
+
258
+ ## Success criteria
259
+
260
+ - `npm test` passes (all kotlin tests still pass, new swift tests pass).
261
+ - `npm run build` succeeds.
262
+ - `npm run lint` clean.
263
+ - Manual run: `npx ts-procedures-codegen --target swift --url ... --out ...` produces a valid `Users.swift` (or whatever scope) that compiles with `swiftc -parse`.
264
+ - Docs and skill files committed; `agent_config/postinstall` will distribute them on next install.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-procedures",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
4
4
  "description": "A TypeScript RPC framework that creates type-safe, schema-validated procedure calls with a single function definition. Define your procedures once and get full type inference, runtime validation, and framework integration hooks.",
5
5
  "main": "build/exports.js",
6
6
  "types": "build/exports.d.ts",
@@ -78,7 +78,7 @@
78
78
  "framework"
79
79
  ],
80
80
  "optionalDependencies": {
81
- "ajsc": "7.2",
81
+ "ajsc": "7.2.0",
82
82
  "express": "^5.2.1",
83
83
  "hono": "^4.7.4"
84
84
  },