ts-procedures 6.0.2 → 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.
- package/agent_config/bin/setup.mjs +2 -2
- package/agent_config/claude-code/skills/ts-procedures/SKILL.md +2 -0
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +2 -0
- package/agent_config/claude-code/skills/ts-procedures-kotlin/SKILL.md +106 -0
- package/agent_config/claude-code/skills/ts-procedures-swift/SKILL.md +119 -0
- package/agent_config/copilot/copilot-instructions.md +3 -0
- package/agent_config/cursor/cursorrules +3 -0
- package/agent_config/lib/install-claude.mjs +1 -1
- package/build/codegen/bin/cli.d.ts +39 -0
- package/build/codegen/bin/cli.js +164 -0
- package/build/codegen/bin/cli.js.map +1 -1
- package/build/codegen/bin/cli.test.js +180 -1
- package/build/codegen/bin/cli.test.js.map +1 -1
- package/build/codegen/index.d.ts +36 -0
- package/build/codegen/index.js +8 -0
- package/build/codegen/index.js.map +1 -1
- package/build/codegen/pipeline.d.ts +22 -4
- package/build/codegen/pipeline.js +44 -86
- package/build/codegen/pipeline.js.map +1 -1
- package/build/codegen/pipeline.test.js +162 -0
- package/build/codegen/pipeline.test.js.map +1 -1
- package/build/codegen/targets/_shared/error-schemas.d.ts +10 -0
- package/build/codegen/targets/_shared/error-schemas.js +17 -0
- package/build/codegen/targets/_shared/error-schemas.js.map +1 -0
- package/build/codegen/targets/_shared/error-schemas.test.d.ts +1 -0
- package/build/codegen/targets/_shared/error-schemas.test.js +38 -0
- package/build/codegen/targets/_shared/error-schemas.test.js.map +1 -0
- package/build/codegen/targets/_shared/indent.d.ts +6 -0
- package/build/codegen/targets/_shared/indent.js +13 -0
- package/build/codegen/targets/_shared/indent.js.map +1 -0
- package/build/codegen/targets/_shared/indent.test.d.ts +1 -0
- package/build/codegen/targets/_shared/indent.test.js +21 -0
- package/build/codegen/targets/_shared/indent.test.js.map +1 -0
- package/build/codegen/targets/_shared/pascal-case.d.ts +6 -0
- package/build/codegen/targets/_shared/pascal-case.js +13 -0
- package/build/codegen/targets/_shared/pascal-case.js.map +1 -0
- package/build/codegen/targets/_shared/pascal-case.test.d.ts +1 -0
- package/build/codegen/targets/_shared/pascal-case.test.js +25 -0
- package/build/codegen/targets/_shared/pascal-case.test.js.map +1 -0
- package/build/codegen/targets/_shared/path-utils.d.ts +12 -0
- package/build/codegen/targets/_shared/path-utils.js +20 -0
- package/build/codegen/targets/_shared/path-utils.js.map +1 -0
- package/build/codegen/targets/_shared/path-utils.test.d.ts +1 -0
- package/build/codegen/targets/_shared/path-utils.test.js +42 -0
- package/build/codegen/targets/_shared/path-utils.test.js.map +1 -0
- package/build/codegen/targets/_shared/pick-defined.d.ts +11 -0
- package/build/codegen/targets/_shared/pick-defined.js +21 -0
- package/build/codegen/targets/_shared/pick-defined.js.map +1 -0
- package/build/codegen/targets/_shared/pick-defined.test.d.ts +1 -0
- package/build/codegen/targets/_shared/pick-defined.test.js +25 -0
- package/build/codegen/targets/_shared/pick-defined.test.js.map +1 -0
- package/build/codegen/targets/_shared/route-slots.d.ts +17 -0
- package/build/codegen/targets/_shared/route-slots.js +17 -0
- package/build/codegen/targets/_shared/route-slots.js.map +1 -0
- package/build/codegen/targets/_shared/route-slots.test.d.ts +1 -0
- package/build/codegen/targets/_shared/route-slots.test.js +43 -0
- package/build/codegen/targets/_shared/route-slots.test.js.map +1 -0
- package/build/codegen/targets/_shared/target-run.d.ts +27 -0
- package/build/codegen/targets/_shared/target-run.js +2 -0
- package/build/codegen/targets/_shared/target-run.js.map +1 -0
- package/build/codegen/targets/_shared/write-files.d.ts +24 -0
- package/build/codegen/targets/_shared/write-files.js +35 -0
- package/build/codegen/targets/_shared/write-files.js.map +1 -0
- package/build/codegen/targets/_shared/write-files.test.d.ts +1 -0
- package/build/codegen/targets/_shared/write-files.test.js +79 -0
- package/build/codegen/targets/_shared/write-files.test.js.map +1 -0
- package/build/codegen/targets/kotlin/ajsc-adapter.d.ts +6 -4
- package/build/codegen/targets/kotlin/ajsc-adapter.js +12 -7
- package/build/codegen/targets/kotlin/ajsc-adapter.js.map +1 -1
- package/build/codegen/targets/kotlin/ajsc-adapter.test.js +20 -2
- package/build/codegen/targets/kotlin/ajsc-adapter.test.js.map +1 -1
- package/build/codegen/targets/kotlin/e2e-compile.test.js +41 -9
- package/build/codegen/targets/kotlin/e2e-compile.test.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-route-kotlin.d.ts +6 -2
- package/build/codegen/targets/kotlin/emit-route-kotlin.js +18 -28
- package/build/codegen/targets/kotlin/emit-route-kotlin.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +120 -1
- package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-scope-kotlin.d.ts +4 -1
- package/build/codegen/targets/kotlin/emit-scope-kotlin.js +12 -11
- package/build/codegen/targets/kotlin/emit-scope-kotlin.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +39 -0
- package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -1
- package/build/codegen/targets/kotlin/format-kotlin.d.ts +0 -1
- package/build/codegen/targets/kotlin/format-kotlin.js +0 -7
- package/build/codegen/targets/kotlin/format-kotlin.js.map +1 -1
- package/build/codegen/targets/kotlin/format-kotlin.test.js +1 -8
- package/build/codegen/targets/kotlin/format-kotlin.test.js.map +1 -1
- package/build/codegen/targets/kotlin/integration.test.js +27 -10
- package/build/codegen/targets/kotlin/integration.test.js.map +1 -1
- package/build/codegen/targets/kotlin/probe-unsupported-unions.test.d.ts +1 -0
- package/build/codegen/targets/kotlin/probe-unsupported-unions.test.js +50 -0
- package/build/codegen/targets/kotlin/probe-unsupported-unions.test.js.map +1 -0
- package/build/codegen/targets/kotlin/run.d.ts +11 -0
- package/build/codegen/targets/kotlin/run.js +51 -0
- package/build/codegen/targets/kotlin/run.js.map +1 -0
- package/build/codegen/targets/swift/access-level.test.d.ts +1 -0
- package/build/codegen/targets/swift/access-level.test.js +98 -0
- package/build/codegen/targets/swift/access-level.test.js.map +1 -0
- package/build/codegen/targets/swift/ajsc-adapter.d.ts +27 -0
- package/build/codegen/targets/swift/ajsc-adapter.js +38 -0
- package/build/codegen/targets/swift/ajsc-adapter.js.map +1 -0
- package/build/codegen/targets/swift/ajsc-adapter.test.d.ts +1 -0
- package/build/codegen/targets/swift/ajsc-adapter.test.js +37 -0
- package/build/codegen/targets/swift/ajsc-adapter.test.js.map +1 -0
- package/build/codegen/targets/swift/e2e-compile.test.d.ts +1 -0
- package/build/codegen/targets/swift/e2e-compile.test.js +57 -0
- package/build/codegen/targets/swift/e2e-compile.test.js.map +1 -0
- package/build/codegen/targets/swift/emit-route-swift.d.ts +15 -0
- package/build/codegen/targets/swift/emit-route-swift.js +64 -0
- package/build/codegen/targets/swift/emit-route-swift.js.map +1 -0
- package/build/codegen/targets/swift/emit-route-swift.test.d.ts +1 -0
- package/build/codegen/targets/swift/emit-route-swift.test.js +258 -0
- package/build/codegen/targets/swift/emit-route-swift.test.js.map +1 -0
- package/build/codegen/targets/swift/emit-scope-swift.d.ts +13 -0
- package/build/codegen/targets/swift/emit-scope-swift.js +36 -0
- package/build/codegen/targets/swift/emit-scope-swift.js.map +1 -0
- package/build/codegen/targets/swift/emit-scope-swift.test.d.ts +1 -0
- package/build/codegen/targets/swift/emit-scope-swift.test.js +136 -0
- package/build/codegen/targets/swift/emit-scope-swift.test.js.map +1 -0
- package/build/codegen/targets/swift/format-swift.d.ts +2 -0
- package/build/codegen/targets/swift/format-swift.js +10 -0
- package/build/codegen/targets/swift/format-swift.js.map +1 -0
- package/build/codegen/targets/swift/format-swift.test.d.ts +1 -0
- package/build/codegen/targets/swift/format-swift.test.js +14 -0
- package/build/codegen/targets/swift/format-swift.test.js.map +1 -0
- package/build/codegen/targets/swift/integration.test.d.ts +1 -0
- package/build/codegen/targets/swift/integration.test.js +53 -0
- package/build/codegen/targets/swift/integration.test.js.map +1 -0
- package/build/codegen/targets/swift/run.d.ts +11 -0
- package/build/codegen/targets/swift/run.js +47 -0
- package/build/codegen/targets/swift/run.js.map +1 -0
- package/build/codegen/targets/ts/run.d.ts +4 -0
- package/build/codegen/targets/ts/run.js +86 -0
- package/build/codegen/targets/ts/run.js.map +1 -0
- package/build/codegen/test-helpers/golden.d.ts +15 -0
- package/build/codegen/test-helpers/golden.js +30 -0
- package/build/codegen/test-helpers/golden.js.map +1 -0
- package/build/codegen/test-helpers/golden.test.d.ts +1 -0
- package/build/codegen/test-helpers/golden.test.js +76 -0
- package/build/codegen/test-helpers/golden.test.js.map +1 -0
- package/docs/codegen-kotlin.md +176 -0
- package/docs/codegen-swift.md +314 -0
- package/docs/superpowers/plans/2026-04-25-ajsc-v7-kotlin-polish.md +1993 -0
- package/docs/superpowers/specs/2026-04-24-kotlin-swift-codegen-design.md +1 -1
- package/docs/superpowers/specs/2026-04-25-ajsc-v7-kotlin-polish-design.md +314 -0
- package/docs/superpowers/specs/2026-04-25-swift-codegen-design.md +264 -0
- package/package.json +2 -2
- package/src/codegen/__fixtures__/users-envelope.json +144 -0
- package/src/codegen/bin/cli.test.ts +200 -1
- package/src/codegen/bin/cli.ts +187 -0
- package/src/codegen/index.ts +50 -0
- package/src/codegen/pipeline.test.ts +175 -0
- package/src/codegen/pipeline.ts +58 -101
- package/src/codegen/targets/_shared/error-schemas.test.ts +42 -0
- package/src/codegen/targets/_shared/error-schemas.ts +17 -0
- package/src/codegen/targets/_shared/indent.test.ts +25 -0
- package/src/codegen/targets/_shared/indent.ts +12 -0
- package/src/codegen/targets/_shared/pascal-case.test.ts +30 -0
- package/src/codegen/targets/_shared/pascal-case.ts +12 -0
- package/src/codegen/targets/_shared/path-utils.test.ts +51 -0
- package/src/codegen/targets/_shared/path-utils.ts +21 -0
- package/src/codegen/targets/_shared/pick-defined.test.ts +48 -0
- package/src/codegen/targets/_shared/pick-defined.ts +23 -0
- package/src/codegen/targets/_shared/route-slots.test.ts +55 -0
- package/src/codegen/targets/_shared/route-slots.ts +32 -0
- package/src/codegen/targets/_shared/target-run.ts +28 -0
- package/src/codegen/targets/_shared/write-files.test.ts +110 -0
- package/src/codegen/targets/_shared/write-files.ts +53 -0
- package/src/codegen/targets/kotlin/__fixtures__/users-golden.kt +121 -0
- package/src/codegen/targets/kotlin/__snapshots__/probe-unsupported-unions.test.ts.snap +27 -0
- package/src/codegen/targets/kotlin/ajsc-adapter.test.ts +47 -0
- package/src/codegen/targets/kotlin/ajsc-adapter.ts +66 -0
- package/src/codegen/targets/kotlin/e2e-compile.test.ts +86 -0
- package/src/codegen/targets/kotlin/emit-route-kotlin.test.ts +239 -0
- package/src/codegen/targets/kotlin/emit-route-kotlin.ts +89 -0
- package/src/codegen/targets/kotlin/emit-scope-kotlin.test.ts +112 -0
- package/src/codegen/targets/kotlin/emit-scope-kotlin.ts +60 -0
- package/src/codegen/targets/kotlin/format-kotlin.test.ts +26 -0
- package/src/codegen/targets/kotlin/format-kotlin.ts +13 -0
- package/src/codegen/targets/kotlin/integration.test.ts +77 -0
- package/src/codegen/targets/kotlin/probe-unsupported-unions.test.ts +64 -0
- package/src/codegen/targets/kotlin/run.ts +78 -0
- package/src/codegen/targets/swift/__fixtures__/users-golden.swift +123 -0
- package/src/codegen/targets/swift/access-level.test.ts +108 -0
- package/src/codegen/targets/swift/ajsc-adapter.test.ts +47 -0
- package/src/codegen/targets/swift/ajsc-adapter.ts +67 -0
- package/src/codegen/targets/swift/e2e-compile.test.ts +66 -0
- package/src/codegen/targets/swift/emit-route-swift.test.ts +300 -0
- package/src/codegen/targets/swift/emit-route-swift.ts +90 -0
- package/src/codegen/targets/swift/emit-scope-swift.test.ts +164 -0
- package/src/codegen/targets/swift/emit-scope-swift.ts +59 -0
- package/src/codegen/targets/swift/format-swift.test.ts +23 -0
- package/src/codegen/targets/swift/format-swift.ts +9 -0
- package/src/codegen/targets/swift/integration.test.ts +80 -0
- package/src/codegen/targets/swift/run.ts +74 -0
- package/src/codegen/targets/ts/run.ts +117 -0
- package/src/codegen/test-helpers/golden.test.ts +80 -0
- package/src/codegen/test-helpers/golden.ts +34 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { assertGoldenOrUpdate } from './golden.js';
|
|
6
|
+
let tmpDir;
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
if (tmpDir != null) {
|
|
9
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
10
|
+
tmpDir = undefined;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
function makeTmp() {
|
|
14
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'tsp-golden-'));
|
|
15
|
+
return tmpDir;
|
|
16
|
+
}
|
|
17
|
+
describe('assertGoldenOrUpdate', () => {
|
|
18
|
+
it('asserts byte-equality against golden when produced matches (with hash splice)', async () => {
|
|
19
|
+
const dir = makeTmp();
|
|
20
|
+
const goldenPath = join(dir, 'expected.txt');
|
|
21
|
+
writeFileSync(goldenPath, 'hello\n// Source hash: <PLACEHOLDER>\nworld\n', 'utf-8');
|
|
22
|
+
const produced = 'hello\n// Source hash: abc123def456\nworld\n';
|
|
23
|
+
await expect(assertGoldenOrUpdate(produced, goldenPath)).resolves.toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
it('throws when produced does not match golden', async () => {
|
|
26
|
+
const dir = makeTmp();
|
|
27
|
+
const goldenPath = join(dir, 'expected.txt');
|
|
28
|
+
writeFileSync(goldenPath, 'expected content\n', 'utf-8');
|
|
29
|
+
const produced = 'different content\n';
|
|
30
|
+
await expect(assertGoldenOrUpdate(produced, goldenPath)).rejects.toThrow();
|
|
31
|
+
});
|
|
32
|
+
it('writes a portable golden when UPDATE_GOLDENS=1', async () => {
|
|
33
|
+
const original = process.env.UPDATE_GOLDENS;
|
|
34
|
+
process.env.UPDATE_GOLDENS = '1';
|
|
35
|
+
try {
|
|
36
|
+
const dir = makeTmp();
|
|
37
|
+
const goldenPath = join(dir, 'expected.txt');
|
|
38
|
+
const produced = 'hello\n// Source hash: deadbeef1234\nworld\n';
|
|
39
|
+
await assertGoldenOrUpdate(produced, goldenPath);
|
|
40
|
+
const written = readFileSync(goldenPath, 'utf-8');
|
|
41
|
+
expect(written).toBe('hello\n// Source hash: <PLACEHOLDER>\nworld\n');
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
if (original === undefined)
|
|
45
|
+
delete process.env.UPDATE_GOLDENS;
|
|
46
|
+
else
|
|
47
|
+
process.env.UPDATE_GOLDENS = original;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
it('handles produced output without a source-hash line', async () => {
|
|
51
|
+
const dir = makeTmp();
|
|
52
|
+
const goldenPath = join(dir, 'expected.txt');
|
|
53
|
+
writeFileSync(goldenPath, 'plain content\n', 'utf-8');
|
|
54
|
+
// No source hash; splice is a no-op on both sides.
|
|
55
|
+
await expect(assertGoldenOrUpdate('plain content\n', goldenPath)).resolves.toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
it('regenerate mode handles produced output without a source-hash line', async () => {
|
|
58
|
+
const original = process.env.UPDATE_GOLDENS;
|
|
59
|
+
process.env.UPDATE_GOLDENS = '1';
|
|
60
|
+
try {
|
|
61
|
+
const dir = makeTmp();
|
|
62
|
+
const goldenPath = join(dir, 'expected.txt');
|
|
63
|
+
const produced = 'no hash line here\n';
|
|
64
|
+
await assertGoldenOrUpdate(produced, goldenPath);
|
|
65
|
+
const written = readFileSync(goldenPath, 'utf-8');
|
|
66
|
+
expect(written).toBe('no hash line here\n');
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
if (original === undefined)
|
|
70
|
+
delete process.env.UPDATE_GOLDENS;
|
|
71
|
+
else
|
|
72
|
+
process.env.UPDATE_GOLDENS = original;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
//# sourceMappingURL=golden.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"golden.test.js","sourceRoot":"","sources":["../../../src/codegen/test-helpers/golden.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Kotlin Codegen Setup Guide
|
|
2
|
+
|
|
3
|
+
Generated by `ts-procedures-codegen --target kotlin`. One `.kt` file per scope; types are nested under route objects (`Users.GetUser.Response`, `Users.GetUser.Body.Address`).
|
|
4
|
+
|
|
5
|
+
## Quickstart
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx ts-procedures-codegen \
|
|
9
|
+
--target kotlin \
|
|
10
|
+
--kotlin-package com.example.api \
|
|
11
|
+
--url https://api.example.com/_ts-procedures.json \
|
|
12
|
+
--out ./src/main/kotlin/com/example/api
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Each scope produces one file (e.g. `Users.kt`). Access generated types as `Users.GetUser.Response`, `Users.GetUser.Body.GuestBody`, `Users.GetUser.Errors.NotFound`.
|
|
16
|
+
|
|
17
|
+
## Gradle setup
|
|
18
|
+
|
|
19
|
+
The default `--kotlin-serializer kotlinx` mode emits `@Serializable` data classes. Add the kotlinx-serialization plugin and runtime:
|
|
20
|
+
|
|
21
|
+
```kotlin
|
|
22
|
+
// build.gradle.kts
|
|
23
|
+
plugins {
|
|
24
|
+
kotlin("jvm") version "<your-kotlin-version>"
|
|
25
|
+
kotlin("plugin.serialization") version "<your-kotlin-version>"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
dependencies {
|
|
29
|
+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:<version>")
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
(`kotlinx-serialization-core` is a transitive dependency of `-json`; you don't need to declare it explicitly.)
|
|
34
|
+
|
|
35
|
+
**JVM only.** Kotlin Multiplatform is not yet supported. If you need KMP-portable types, fall back to `--kotlin-serializer none` or post-process the emitted code.
|
|
36
|
+
|
|
37
|
+
## Contextual serializers
|
|
38
|
+
|
|
39
|
+
`format: date-time`, `format: uuid`, `format: uri`, `format: date`, `format: time` map to JVM stdlib types (`java.time.Instant`, `java.util.UUID`, etc.) annotated with `@Contextual`. Kotlinx-serialization does not know how to serialize these natively — register contextual serializers in your `Json` configuration:
|
|
40
|
+
|
|
41
|
+
```kotlin
|
|
42
|
+
import kotlinx.serialization.json.Json
|
|
43
|
+
import kotlinx.serialization.modules.SerializersModule
|
|
44
|
+
import kotlinx.serialization.modules.contextual
|
|
45
|
+
|
|
46
|
+
val json = Json {
|
|
47
|
+
serializersModule = SerializersModule {
|
|
48
|
+
contextual(java.time.Instant::class, InstantSerializer)
|
|
49
|
+
contextual(java.util.UUID::class, UUIDSerializer)
|
|
50
|
+
contextual(java.net.URI::class, URISerializer)
|
|
51
|
+
// ...etc for any format you use
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The serializers themselves are your responsibility (we don't ship them — the choice between ISO-8601 strings, epoch milliseconds, etc. is application-specific). Public Gist-quality implementations are widely available; we recommend ISO-8601 to match the server.
|
|
57
|
+
|
|
58
|
+
## Discriminated unions
|
|
59
|
+
|
|
60
|
+
A schema with a `oneOf` whose variants share a const-valued discriminator (e.g. `kind: "guest" | "registered"`) emits as a sealed interface:
|
|
61
|
+
|
|
62
|
+
```kotlin
|
|
63
|
+
@Serializable
|
|
64
|
+
@JsonClassDiscriminator("kind")
|
|
65
|
+
sealed interface Body {
|
|
66
|
+
@Serializable
|
|
67
|
+
@SerialName("guest")
|
|
68
|
+
data class GuestBody(val displayName: String) : Body
|
|
69
|
+
|
|
70
|
+
@Serializable
|
|
71
|
+
@SerialName("registered")
|
|
72
|
+
data class RegisteredBody(val email: String, val name: String) : Body
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The discriminator field (`kind`) is **erased** from each variant under `--kotlin-serializer kotlinx` — `@SerialName` carries it on the wire. With `--kotlin-serializer none` the discriminator field is retained.
|
|
77
|
+
|
|
78
|
+
`@JsonClassDiscriminator` is read automatically by the kotlinx `Json` instance — no extra config needed.
|
|
79
|
+
|
|
80
|
+
## JSON-key sanitization
|
|
81
|
+
|
|
82
|
+
Kebab-case and snake-case JSON keys become camelCase property names with `@SerialName` auto-emitted so the wire format stays correct:
|
|
83
|
+
|
|
84
|
+
```kotlin
|
|
85
|
+
@Serializable
|
|
86
|
+
data class Response(
|
|
87
|
+
@SerialName("created-at")
|
|
88
|
+
@Contextual
|
|
89
|
+
val createdAt: java.time.Instant,
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Reserved Kotlin words get a trailing underscore (`class` → `class_`).
|
|
94
|
+
|
|
95
|
+
## Switching off kotlinx (Moshi, Gson, hand-written)
|
|
96
|
+
|
|
97
|
+
`--kotlin-serializer none` emits plain `data class` types with no `@Serializable` annotation. You're then responsible for adapter setup:
|
|
98
|
+
|
|
99
|
+
- **Reflection-based libraries** (Gson) work without further changes.
|
|
100
|
+
- **Codegen-based** (Moshi with codegen) need their own annotation layer added.
|
|
101
|
+
- Sealed interfaces still emit; the discriminator field is **retained** in variants under `none` mode.
|
|
102
|
+
|
|
103
|
+
## Error types
|
|
104
|
+
|
|
105
|
+
Each route that declares errors gets a nested `Errors` object containing one `@Serializable data class` per error name:
|
|
106
|
+
|
|
107
|
+
```kotlin
|
|
108
|
+
object Users {
|
|
109
|
+
object GetUser {
|
|
110
|
+
// ... method, path, types ...
|
|
111
|
+
object Errors {
|
|
112
|
+
@Serializable
|
|
113
|
+
data class NotFound(
|
|
114
|
+
val name: String = "NotFound",
|
|
115
|
+
val message: String,
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Access generated error types as `Users.GetUser.Errors.NotFound`.
|
|
123
|
+
|
|
124
|
+
**No runtime dispatch.** Unlike the TypeScript target, the Kotlin target ships **types only** — there is no error registry, no `instanceof`-style lookup, no `dispatchTypedError`. Mobile consumers catch HTTP failures themselves and inspect `body.name` (which is a regular `String` field, not a type-system discriminator) to decide which error data class to deserialize against:
|
|
125
|
+
|
|
126
|
+
```kotlin
|
|
127
|
+
suspend fun loadUser(id: String): Result<Users.GetUser.Response> {
|
|
128
|
+
val response = httpClient.request(method = Users.GetUser.method, path = "...")
|
|
129
|
+
return when {
|
|
130
|
+
response.status == 200 -> Result.success(json.decodeFromString(response.body))
|
|
131
|
+
response.status == 404 -> {
|
|
132
|
+
val err = json.decodeFromString<Users.GetUser.Errors.NotFound>(response.body)
|
|
133
|
+
Result.failure(NotFoundException(err.message))
|
|
134
|
+
}
|
|
135
|
+
else -> Result.failure(IOException("HTTP ${response.status}"))
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Choosing the dispatch strategy (status-code based, body.name based, sealed-class hierarchy of your own, etc.) is intentionally left to consumers.
|
|
141
|
+
|
|
142
|
+
## Untagged unions
|
|
143
|
+
|
|
144
|
+
ajsc v7.2's Kotlin emitter silently produces an empty `@Serializable data class` for any untagged `oneOf` schema, regardless of whether `--unsupported-unions fallback` is set. There is no "throws on default" mode for Kotlin (that's Swift behavior). The `--unsupported-unions` flag is currently a no-op for the Kotlin target.
|
|
145
|
+
|
|
146
|
+
For example, the schema `oneOf: [{ type: 'string' }, { type: 'integer' }]` with `rootTypeName: 'Mixed'` produces:
|
|
147
|
+
|
|
148
|
+
```kotlin
|
|
149
|
+
@Serializable
|
|
150
|
+
data class Mixed()
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Imports: `kotlinx.serialization.Serializable`.
|
|
154
|
+
|
|
155
|
+
**Practical implication.** This empty data class won't round-trip your data — calling `Json.decodeFromString<Mixed>("\"hello\"")` will fail because `Mixed` has no payload. **Untagged unions in your schema cannot be consumed via the generated Kotlin types.** Workarounds:
|
|
156
|
+
|
|
157
|
+
1. **Add a discriminator** to your server-side schema so it becomes a tagged union (sealed interface emission — see "Discriminated unions" section above).
|
|
158
|
+
2. **Hand-write a `KSerializer<T>` `companion object`** in a sibling file that resolves the union at deserialization time.
|
|
159
|
+
3. **Strip the union field at the codegen boundary** — pre-process the envelope to replace untagged `oneOf` with a single permissive type (e.g. `JsonElement`).
|
|
160
|
+
|
|
161
|
+
We track this as an ajsc upstream limitation; if it's resolved in a future ajsc release, this section will be updated.
|
|
162
|
+
|
|
163
|
+
## Documented limitations
|
|
164
|
+
|
|
165
|
+
The following ajsc behaviors are intentional and documented; they are **not bugs**:
|
|
166
|
+
|
|
167
|
+
- `additionalProperties: { type: T }` is silently dropped with a `/** Note: schema permits additional keys of type T — not modeled. */` KDoc note. If your contract uses extra keys, add a sibling `Map<String, T>` field by hand or write a custom `KSerializer`.
|
|
168
|
+
- Tuples with 4 or more positional types throw (`Pair`/`Triple` only). Refactor to a struct schema upstream.
|
|
169
|
+
- Schema-level `examples` and `not` / `patternProperties` are not modeled (the latter throw with a path-bearing error).
|
|
170
|
+
|
|
171
|
+
## Reference
|
|
172
|
+
|
|
173
|
+
- Spec: [`docs/superpowers/specs/2026-04-25-ajsc-v7-kotlin-polish-design.md`](./superpowers/specs/2026-04-25-ajsc-v7-kotlin-polish-design.md)
|
|
174
|
+
- For iOS / macOS / Apple-platform consumers, see [`docs/codegen-swift.md`](./codegen-swift.md) — same types-only design with Swift-specific flags and `Codable` setup notes.
|
|
175
|
+
- ajsc README: `node_modules/ajsc/README.md` (or [npmjs.com/package/ajsc](https://www.npmjs.com/package/ajsc))
|
|
176
|
+
- ts-procedures-codegen CLI flags: see `CLAUDE.md` (search for "Kotlin target") and the spec linked above. (`--help` is not currently implemented; pass invalid/missing args to see error messages with usage hints.)
|
|
@@ -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.)
|