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.
- package/agent_config/bin/setup.mjs +2 -2
- package/agent_config/claude-code/skills/ts-procedures/SKILL.md +1 -0
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +1 -1
- package/agent_config/claude-code/skills/ts-procedures-kotlin/SKILL.md +1 -1
- package/agent_config/claude-code/skills/ts-procedures-swift/SKILL.md +119 -0
- package/agent_config/copilot/copilot-instructions.md +1 -0
- package/agent_config/cursor/cursorrules +1 -0
- package/agent_config/lib/install-claude.mjs +1 -1
- package/build/codegen/bin/cli.d.ts +17 -3
- package/build/codegen/bin/cli.js +79 -3
- package/build/codegen/bin/cli.js.map +1 -1
- package/build/codegen/index.d.ts +18 -1
- package/build/codegen/index.js +3 -0
- package/build/codegen/index.js.map +1 -1
- package/build/codegen/pipeline.d.ts +16 -5
- package/build/codegen/pipeline.js +44 -143
- package/build/codegen/pipeline.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/e2e-compile.test.js +1 -1
- package/build/codegen/targets/kotlin/e2e-compile.test.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-route-kotlin.js +5 -22
- package/build/codegen/targets/kotlin/emit-route-kotlin.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-scope-kotlin.js +4 -8
- package/build/codegen/targets/kotlin/emit-scope-kotlin.js.map +1 -1
- package/build/codegen/targets/kotlin/format-kotlin.d.ts +0 -12
- package/build/codegen/targets/kotlin/format-kotlin.js +0 -27
- package/build/codegen/targets/kotlin/format-kotlin.js.map +1 -1
- package/build/codegen/targets/kotlin/format-kotlin.test.js +1 -34
- package/build/codegen/targets/kotlin/format-kotlin.test.js.map +1 -1
- package/build/codegen/targets/kotlin/integration.test.js +1 -1
- package/build/codegen/targets/kotlin/integration.test.js.map +1 -1
- 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/docs/codegen-kotlin.md +1 -0
- package/docs/codegen-swift.md +314 -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 +1 -1
- package/docs/superpowers/specs/2026-04-25-swift-codegen-design.md +264 -0
- package/package.json +2 -2
- package/src/codegen/bin/cli.ts +91 -7
- package/src/codegen/index.ts +24 -1
- package/src/codegen/pipeline.ts +52 -174
- 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/e2e-compile.test.ts +1 -1
- package/src/codegen/targets/kotlin/emit-route-kotlin.ts +5 -25
- package/src/codegen/targets/kotlin/emit-scope-kotlin.ts +4 -9
- package/src/codegen/targets/kotlin/format-kotlin.test.ts +0 -44
- package/src/codegen/targets/kotlin/format-kotlin.ts +0 -32
- package/src/codegen/targets/kotlin/integration.test.ts +1 -1
- 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/{targets/kotlin/__fixtures__ → __fixtures__}/users-envelope.json +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { runPipeline } from '../../pipeline.js';
|
|
3
|
+
import { resolveProductionSwiftEmitter } from './ajsc-adapter.js';
|
|
4
|
+
/**
|
|
5
|
+
* End-to-end regression for `--swift-access-level`.
|
|
6
|
+
*
|
|
7
|
+
* Closes a coverage gap: existing unit tests verify our scope/route
|
|
8
|
+
* scaffolding (the outer `enum` keywords) honor the access level, but
|
|
9
|
+
* nothing asserts that real ajsc actually emits `internal struct` /
|
|
10
|
+
* `internal let` in its own output. If ajsc renamed `accessLevel` or
|
|
11
|
+
* changed its semantics, our codegen would silently regress to `public`.
|
|
12
|
+
*
|
|
13
|
+
* This test runs the production pipeline with the real ajsc emitter
|
|
14
|
+
* and pins the keyword presence in the generated source. No toolchain
|
|
15
|
+
* (no swiftc) is required — we only assert string contents.
|
|
16
|
+
*/
|
|
17
|
+
const envelope = {
|
|
18
|
+
basePath: '/api',
|
|
19
|
+
headers: [],
|
|
20
|
+
errors: [],
|
|
21
|
+
routes: [
|
|
22
|
+
{
|
|
23
|
+
kind: 'api',
|
|
24
|
+
name: 'GetThing',
|
|
25
|
+
scope: 'things',
|
|
26
|
+
method: 'get',
|
|
27
|
+
path: '/things/:id',
|
|
28
|
+
fullPath: '/things/:id',
|
|
29
|
+
jsonSchema: {},
|
|
30
|
+
// The swift route emitter reads `route.schema.input.*` and
|
|
31
|
+
// `route.schema.returnType` directly; mirror what the codegen
|
|
32
|
+
// pipeline downstream expects rather than the doc-builder
|
|
33
|
+
// `jsonSchema` shape.
|
|
34
|
+
schema: {
|
|
35
|
+
input: {
|
|
36
|
+
pathParams: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: { id: { type: 'string' } },
|
|
39
|
+
required: ['id'],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
returnType: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: { id: { type: 'string' } },
|
|
45
|
+
required: ['id'],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
errors: [],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
describe('swift codegen — --swift-access-level (real ajsc)', () => {
|
|
53
|
+
it('emits `internal` keywords on ajsc-generated structs when accessLevel="internal"', async () => {
|
|
54
|
+
const emitter = await resolveProductionSwiftEmitter();
|
|
55
|
+
const files = await runPipeline({
|
|
56
|
+
envelope,
|
|
57
|
+
outDir: 'out',
|
|
58
|
+
dryRun: true,
|
|
59
|
+
target: 'swift',
|
|
60
|
+
swiftAccessLevel: 'internal',
|
|
61
|
+
swiftEmitter: emitter,
|
|
62
|
+
});
|
|
63
|
+
expect(files).toHaveLength(1);
|
|
64
|
+
const code = files[0].code;
|
|
65
|
+
// Real ajsc output for the PathParams + Response slots.
|
|
66
|
+
expect(code).toContain('internal struct PathParams');
|
|
67
|
+
expect(code).toContain('internal struct Response');
|
|
68
|
+
// Property declarations should also flip to internal.
|
|
69
|
+
expect(code).toContain('internal let id: String');
|
|
70
|
+
// Our scope/route scaffolding flips too — see emit-scope-swift.ts
|
|
71
|
+
// and emit-route-swift.ts, which both consume `accessLevel`.
|
|
72
|
+
expect(code).toContain('internal enum Things');
|
|
73
|
+
expect(code).toContain('internal enum GetThing');
|
|
74
|
+
// Hard guarantee: nothing in the file leaks `public` declarations.
|
|
75
|
+
// If ajsc silently ignored `accessLevel: 'internal'`, this would fail.
|
|
76
|
+
expect(code).not.toMatch(/\bpublic\s+(struct|enum|let|var|func|static)\b/);
|
|
77
|
+
});
|
|
78
|
+
it('emits `public` keywords on ajsc-generated structs when accessLevel defaults to public', async () => {
|
|
79
|
+
const emitter = await resolveProductionSwiftEmitter();
|
|
80
|
+
const files = await runPipeline({
|
|
81
|
+
envelope,
|
|
82
|
+
outDir: 'out',
|
|
83
|
+
dryRun: true,
|
|
84
|
+
target: 'swift',
|
|
85
|
+
// intentionally omit swiftAccessLevel — default is 'public'
|
|
86
|
+
swiftEmitter: emitter,
|
|
87
|
+
});
|
|
88
|
+
expect(files).toHaveLength(1);
|
|
89
|
+
const code = files[0].code;
|
|
90
|
+
expect(code).toContain('public struct PathParams');
|
|
91
|
+
expect(code).toContain('public struct Response');
|
|
92
|
+
expect(code).toContain('public enum Things');
|
|
93
|
+
expect(code).toContain('public enum GetThing');
|
|
94
|
+
// No `internal` declarations should appear when access level is public.
|
|
95
|
+
expect(code).not.toMatch(/\binternal\s+(struct|enum|let|var|func|static)\b/);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
//# sourceMappingURL=access-level.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access-level.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/swift/access-level.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface SwiftEmitResult {
|
|
2
|
+
code: string;
|
|
3
|
+
rootTypeName: string;
|
|
4
|
+
extractedTypeNames: string[];
|
|
5
|
+
imports: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface SwiftEmitOptions {
|
|
8
|
+
rootTypeName: string;
|
|
9
|
+
/** Always set true at our call sites; v7.2 default is false. */
|
|
10
|
+
inlineTypes?: boolean;
|
|
11
|
+
serializer?: 'codable' | 'none';
|
|
12
|
+
accessLevel?: 'public' | 'internal';
|
|
13
|
+
unsupportedUnions?: 'throw' | 'fallback';
|
|
14
|
+
arrayItemNaming?: string | false;
|
|
15
|
+
depluralize?: boolean;
|
|
16
|
+
uncountableWords?: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface SwiftEmitter {
|
|
19
|
+
emit(schema: Record<string, unknown>, opts: SwiftEmitOptions): SwiftEmitResult;
|
|
20
|
+
}
|
|
21
|
+
export declare function createStubSwiftEmitter(results: Record<string, SwiftEmitResult>): SwiftEmitter;
|
|
22
|
+
/**
|
|
23
|
+
* Resolves the production Swift emitter from `ajsc`. Throws a clear error
|
|
24
|
+
* if ajsc is not installed or does not expose `emitSwift` (e.g. consumer
|
|
25
|
+
* ran `npm install --omit=optional` since ajsc is in optionalDependencies).
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveProductionSwiftEmitter(): Promise<SwiftEmitter>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function createStubSwiftEmitter(results) {
|
|
2
|
+
return {
|
|
3
|
+
emit(_schema, opts) {
|
|
4
|
+
const result = results[opts.rootTypeName];
|
|
5
|
+
if (result == null) {
|
|
6
|
+
throw new Error(`[stub-swift-emitter] No stubbed result for rootTypeName "${opts.rootTypeName}". ` +
|
|
7
|
+
`Provide one in the results map.`);
|
|
8
|
+
}
|
|
9
|
+
return result;
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolves the production Swift emitter from `ajsc`. Throws a clear error
|
|
15
|
+
* if ajsc is not installed or does not expose `emitSwift` (e.g. consumer
|
|
16
|
+
* ran `npm install --omit=optional` since ajsc is in optionalDependencies).
|
|
17
|
+
*/
|
|
18
|
+
export async function resolveProductionSwiftEmitter() {
|
|
19
|
+
let ajsc = null;
|
|
20
|
+
let importError;
|
|
21
|
+
try {
|
|
22
|
+
ajsc = (await import('ajsc'));
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
importError = err;
|
|
26
|
+
}
|
|
27
|
+
const emitSwift = ajsc?.emitSwift;
|
|
28
|
+
if (typeof emitSwift !== 'function') {
|
|
29
|
+
throw new Error('[ts-procedures-codegen] ajsc.emitSwift is not available. ' +
|
|
30
|
+
'Install ajsc (`npm install ajsc`) — it is an optional dependency.', importError !== undefined ? { cause: importError } : undefined);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
emit(schema, opts) {
|
|
34
|
+
return emitSwift(schema, opts);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=ajsc-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ajsc-adapter.js","sourceRoot":"","sources":["../../../../src/codegen/targets/swift/ajsc-adapter.ts"],"names":[],"mappings":"AAuBA,MAAM,UAAU,sBAAsB,CACpC,OAAwC;IAExC,OAAO;QACL,IAAI,CAAC,OAAO,EAAE,IAAI;YAChB,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;YACzC,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;gBACnB,MAAM,IAAI,KAAK,CACb,4DAA4D,IAAI,CAAC,YAAY,KAAK;oBAChF,iCAAiC,CACpC,CAAA;YACH,CAAC;YACD,OAAO,MAAM,CAAA;QACf,CAAC;KACF,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,6BAA6B;IACjD,IAAI,IAAI,GAAmC,IAAI,CAAA;IAC/C,IAAI,WAAoB,CAAA;IACxB,IAAI,CAAC;QACH,IAAI,GAAG,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,CAA4B,CAAA;IAC1D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,WAAW,GAAG,GAAG,CAAA;IACnB,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,EAAE,SAAS,CAAA;IACjC,IAAI,OAAO,SAAS,KAAK,UAAU,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CACb,2DAA2D;YAC3D,mEAAmE,EACnE,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAC/D,CAAA;IACH,CAAC;IACD,OAAO;QACL,IAAI,CAAC,MAAM,EAAE,IAAI;YACf,OAAQ,SAAyD,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;QACjF,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createStubSwiftEmitter, resolveProductionSwiftEmitter } from './ajsc-adapter.js';
|
|
3
|
+
describe('createStubSwiftEmitter', () => {
|
|
4
|
+
it('returns the configured EmitResult for the matching root name', () => {
|
|
5
|
+
const expected = {
|
|
6
|
+
code: 'public struct User: Codable { public let id: String }',
|
|
7
|
+
rootTypeName: 'User',
|
|
8
|
+
extractedTypeNames: [],
|
|
9
|
+
imports: ['Foundation'],
|
|
10
|
+
};
|
|
11
|
+
const emitter = createStubSwiftEmitter({ User: expected });
|
|
12
|
+
expect(emitter.emit({ type: 'object' }, {
|
|
13
|
+
rootTypeName: 'User',
|
|
14
|
+
inlineTypes: true,
|
|
15
|
+
serializer: 'codable',
|
|
16
|
+
unsupportedUnions: 'throw',
|
|
17
|
+
})).toEqual(expected);
|
|
18
|
+
});
|
|
19
|
+
it('throws when asked to emit a name not in the stub map', () => {
|
|
20
|
+
const emitter = createStubSwiftEmitter({});
|
|
21
|
+
expect(() => emitter.emit({}, { rootTypeName: 'Missing' })).toThrow(/Missing/);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('resolveProductionSwiftEmitter', () => {
|
|
25
|
+
it('returns a working emitter that invokes ajsc.emitSwift when ajsc is installed', async () => {
|
|
26
|
+
const emitter = await resolveProductionSwiftEmitter();
|
|
27
|
+
const result = emitter.emit({ type: 'object', properties: { id: { type: 'string' } }, required: ['id'] }, { rootTypeName: 'Probe' });
|
|
28
|
+
expect(typeof result.code).toBe('string');
|
|
29
|
+
expect(result.code.length).toBeGreaterThan(0);
|
|
30
|
+
expect(result.rootTypeName).toBe('Probe');
|
|
31
|
+
expect(Array.isArray(result.imports)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
// Note: testing the failure path (ajsc unavailable) requires module mocking;
|
|
34
|
+
// we leave that as a manual-verification path. The error message is pinned
|
|
35
|
+
// by the message text below so any change requires updating both call sites.
|
|
36
|
+
});
|
|
37
|
+
//# sourceMappingURL=ajsc-adapter.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ajsc-adapter.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/swift/ajsc-adapter.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,sBAAsB,EAAE,6BAA6B,EAAwB,MAAM,mBAAmB,CAAA;AAE/G,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,EAAE,CAAC,8DAA8D,EAAE,GAAG,EAAE;QACtE,MAAM,QAAQ,GAAoB;YAChC,IAAI,EAAE,uDAAuD;YAC7D,YAAY,EAAE,MAAM;YACpB,kBAAkB,EAAE,EAAE;YACtB,OAAO,EAAE,CAAC,YAAY,CAAC;SACxB,CAAA;QACD,MAAM,OAAO,GAAG,sBAAsB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAA;QAC1D,MAAM,CACJ,OAAO,CAAC,IAAI,CACV,EAAE,IAAI,EAAE,QAAQ,EAAE,EAClB;YACE,YAAY,EAAE,MAAM;YACpB,WAAW,EAAE,IAAI;YACjB,UAAU,EAAE,SAAS;YACrB,iBAAiB,EAAE,OAAO;SAC3B,CACF,CACF,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IACrB,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,OAAO,GAAG,sBAAsB,CAAC,EAAE,CAAC,CAAA;QAC1C,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IAChF,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,+BAA+B,EAAE,GAAG,EAAE;IAC7C,EAAE,CAAC,8EAA8E,EAAE,KAAK,IAAI,EAAE;QAC5F,MAAM,OAAO,GAAG,MAAM,6BAA6B,EAAE,CAAA;QACrD,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CACzB,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE,EAC5E,EAAE,YAAY,EAAE,OAAO,EAAE,CAC1B,CAAA;QACD,MAAM,CAAC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACzC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAA;QAC7C,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACzC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAClD,CAAC,CAAC,CAAA;IACF,6EAA6E;IAC7E,2EAA2E;IAC3E,6EAA6E;AAC/E,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { execFileSync, execSync } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { runPipeline } from '../../pipeline.js';
|
|
8
|
+
import { resolveProductionSwiftEmitter } from './ajsc-adapter.js';
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
function swiftcAvailable() {
|
|
12
|
+
try {
|
|
13
|
+
execSync('swiftc --version', { stdio: 'ignore' });
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const RUN = process.env.TS_PROCEDURES_SWIFT_E2E === '1';
|
|
21
|
+
/**
|
|
22
|
+
* E2E: real ajsc → real .swift output → real swiftc parse.
|
|
23
|
+
*
|
|
24
|
+
* Gated on (a) `swiftc` on PATH and (b) opt-in via env var so default
|
|
25
|
+
* `npm test` runs stay green for contributors without the toolchain.
|
|
26
|
+
*
|
|
27
|
+
* Unlike Kotlin, Swift's stdlib + Foundation already ship with `Codable`,
|
|
28
|
+
* so no extra classpath/jars are needed — `swiftc -parse` against a single
|
|
29
|
+
* file is sufficient to validate the output is syntactically well-formed.
|
|
30
|
+
*/
|
|
31
|
+
describe('swift codegen — swiftc compile (gated)', () => {
|
|
32
|
+
it.skipIf(!swiftcAvailable() || !RUN)('parses generated output without errors', async () => {
|
|
33
|
+
const emitter = await resolveProductionSwiftEmitter();
|
|
34
|
+
const envelope = JSON.parse(readFileSync(join(__dirname, '../../__fixtures__/users-envelope.json'), 'utf8'));
|
|
35
|
+
const files = await runPipeline({
|
|
36
|
+
envelope,
|
|
37
|
+
outDir: 'out',
|
|
38
|
+
dryRun: true,
|
|
39
|
+
target: 'swift',
|
|
40
|
+
swiftEmitter: emitter,
|
|
41
|
+
});
|
|
42
|
+
const dir = mkdtempSync(join(tmpdir(), 'tsp-swift-e2e-'));
|
|
43
|
+
const filePaths = [];
|
|
44
|
+
for (const f of files) {
|
|
45
|
+
const dest = join(dir, f.path.split('/').pop());
|
|
46
|
+
writeFileSync(dest, f.code);
|
|
47
|
+
filePaths.push(dest);
|
|
48
|
+
}
|
|
49
|
+
// -parse only checks syntax; no codegen, no linking, no module deps.
|
|
50
|
+
execFileSync('swiftc', ['-parse', ...filePaths], { stdio: 'inherit' });
|
|
51
|
+
// Sanity check — at least one .swift file was generated and persisted.
|
|
52
|
+
expect(filePaths.length).toBeGreaterThan(0);
|
|
53
|
+
for (const p of filePaths)
|
|
54
|
+
expect(existsSync(p)).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
//# sourceMappingURL=e2e-compile.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"e2e-compile.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/swift/e2e-compile.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAC9E,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,6BAA6B,EAAE,MAAM,mBAAmB,CAAA;AAEjE,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACjD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;AAErC,SAAS,eAAe;IACtB,IAAI,CAAC;QACH,QAAQ,CAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAA;QACjD,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAuB,KAAK,GAAG,CAAA;AAEvD;;;;;;;;;GASG;AACH,QAAQ,CAAC,wCAAwC,EAAE,GAAG,EAAE;IACtD,EAAE,CAAC,MAAM,CAAC,CAAC,eAAe,EAAE,IAAI,CAAC,GAAG,CAAC,CACnC,wCAAwC,EACxC,KAAK,IAAI,EAAE;QACT,MAAM,OAAO,GAAG,MAAM,6BAA6B,EAAE,CAAA;QACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CACzB,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,wCAAwC,CAAC,EAAE,MAAM,CAAC,CAChF,CAAA;QACD,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC;YAC9B,QAAQ;YACR,MAAM,EAAE,KAAK;YACb,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,OAAO;YACf,YAAY,EAAE,OAAO;SACtB,CAAC,CAAA;QAEF,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,gBAAgB,CAAC,CAAC,CAAA;QACzD,MAAM,SAAS,GAAa,EAAE,CAAA;QAC9B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC,CAAA;YAChD,aAAa,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAA;YAC3B,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACtB,CAAC;QAED,qEAAqE;QACrE,YAAY,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,GAAG,SAAS,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAA;QAEtE,uEAAuE;QACvE,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAA;QAC3C,KAAK,MAAM,CAAC,IAAI,SAAS;YAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC7D,CAAC,CACF,CAAA;AACH,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AnyHttpRouteDoc } from '../../../implementations/types.js';
|
|
2
|
+
import type { SwiftEmitter, SwiftEmitOptions } from './ajsc-adapter.js';
|
|
3
|
+
export interface EmitRouteResult {
|
|
4
|
+
/** Inner body of the `enum RouteName { ... }` block — already indented one level. */
|
|
5
|
+
code: string;
|
|
6
|
+
/** Imports collected from every ajsc emit + any helpers this route used. */
|
|
7
|
+
imports: string[];
|
|
8
|
+
/** Outer route name used as the `enum RouteName` identifier. */
|
|
9
|
+
routeName: string;
|
|
10
|
+
/** True when the route was a stream (out-of-scope). Caller logs once. */
|
|
11
|
+
skipped?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Subset of SwiftEmitOptions threaded by the pipeline; per-call rootTypeName is set inside. */
|
|
14
|
+
export type EmitRouteOpts = Omit<SwiftEmitOptions, 'rootTypeName' | 'inlineTypes'>;
|
|
15
|
+
export declare function emitSwiftRoute(route: AnyHttpRouteDoc, emitter: SwiftEmitter, errorSchemas: Map<string, unknown>, routeOpts?: EmitRouteOpts): EmitRouteResult;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { indent } from '../_shared/indent.js';
|
|
2
|
+
import { pickDefined } from '../_shared/pick-defined.js';
|
|
3
|
+
import { toBracePath, pathParamNames } from '../_shared/path-utils.js';
|
|
4
|
+
import { extractRouteSlots } from '../_shared/route-slots.js';
|
|
5
|
+
function buildPathFn(bracePath, params, access) {
|
|
6
|
+
if (params.length === 0)
|
|
7
|
+
return `${access} static let path = "${bracePath}"`;
|
|
8
|
+
let body = bracePath;
|
|
9
|
+
for (const name of params)
|
|
10
|
+
body = body.replace(`{${name}}`, `\\(p.${name})`);
|
|
11
|
+
return `${access} static func path(_ p: PathParams) -> String { return "${body}" }`;
|
|
12
|
+
}
|
|
13
|
+
function emitOptsFor(rootTypeName, routeOpts) {
|
|
14
|
+
const PASSTHROUGH_KEYS = ['serializer', 'accessLevel', 'unsupportedUnions', 'arrayItemNaming', 'depluralize', 'uncountableWords'];
|
|
15
|
+
return {
|
|
16
|
+
rootTypeName,
|
|
17
|
+
inlineTypes: true,
|
|
18
|
+
...pickDefined(routeOpts, PASSTHROUGH_KEYS),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function emitSwiftRoute(route, emitter, errorSchemas, routeOpts = {}) {
|
|
22
|
+
const kind = route.kind;
|
|
23
|
+
if (kind === 'stream') {
|
|
24
|
+
return { code: '', imports: [], routeName: route.name, skipped: true };
|
|
25
|
+
}
|
|
26
|
+
const isApi = kind === 'api' || 'fullPath' in route;
|
|
27
|
+
const rawPath = isApi ? route.fullPath : route.path;
|
|
28
|
+
const method = String(route.method).toUpperCase();
|
|
29
|
+
const bracePath = toBracePath(rawPath);
|
|
30
|
+
const params = pathParamNames(rawPath);
|
|
31
|
+
const access = routeOpts.accessLevel ?? 'public';
|
|
32
|
+
const lines = [
|
|
33
|
+
`${access} static let method = "${method}"`,
|
|
34
|
+
`${access} static let pathTemplate = "${bracePath}"`,
|
|
35
|
+
buildPathFn(bracePath, params, access),
|
|
36
|
+
];
|
|
37
|
+
const imports = [];
|
|
38
|
+
// Per-slot emission. Order is fixed for deterministic output.
|
|
39
|
+
for (const slot of extractRouteSlots(route)) {
|
|
40
|
+
const result = emitter.emit(slot.source, emitOptsFor(slot.rootName, routeOpts));
|
|
41
|
+
lines.push('');
|
|
42
|
+
lines.push(result.code);
|
|
43
|
+
imports.push(...result.imports);
|
|
44
|
+
}
|
|
45
|
+
// Errors namespace — route.errors is `string[]` of taxonomy keys; look up each schema
|
|
46
|
+
// from the envelope-level errors map. Keys without schemas are skipped silently
|
|
47
|
+
// (matching the existing TS scope emitter's `errorKeys` filter).
|
|
48
|
+
const routeErrorKeys = (route.errors ?? [])
|
|
49
|
+
.filter((key) => errorSchemas.has(key));
|
|
50
|
+
if (routeErrorKeys.length > 0) {
|
|
51
|
+
const inner = [];
|
|
52
|
+
for (const key of routeErrorKeys) {
|
|
53
|
+
const r = emitter.emit(errorSchemas.get(key), emitOptsFor(key, routeOpts));
|
|
54
|
+
inner.push(r.code);
|
|
55
|
+
imports.push(...r.imports);
|
|
56
|
+
}
|
|
57
|
+
lines.push('');
|
|
58
|
+
lines.push(`${access} enum Errors {`);
|
|
59
|
+
lines.push(indent(inner.join('\n\n'), 1));
|
|
60
|
+
lines.push('}');
|
|
61
|
+
}
|
|
62
|
+
return { code: lines.join('\n'), imports, routeName: route.name, skipped: false };
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=emit-route-swift.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emit-route-swift.js","sourceRoot":"","sources":["../../../../src/codegen/targets/swift/emit-route-swift.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { emitSwiftRoute } from './emit-route-swift.js';
|
|
3
|
+
import { createStubSwiftEmitter } from './ajsc-adapter.js';
|
|
4
|
+
const ok = (code, rootTypeName) => ({
|
|
5
|
+
code,
|
|
6
|
+
rootTypeName,
|
|
7
|
+
extractedTypeNames: [],
|
|
8
|
+
imports: ['Foundation'],
|
|
9
|
+
});
|
|
10
|
+
const noErrors = new Map();
|
|
11
|
+
function makeSpyEmitter(results) {
|
|
12
|
+
const calls = [];
|
|
13
|
+
const emitter = {
|
|
14
|
+
emit(schema, opts) {
|
|
15
|
+
calls.push({ schema, opts });
|
|
16
|
+
const r = results[opts.rootTypeName];
|
|
17
|
+
if (r == null)
|
|
18
|
+
throw new Error(`No stubbed result for "${opts.rootTypeName}"`);
|
|
19
|
+
return r;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
return { emitter, calls };
|
|
23
|
+
}
|
|
24
|
+
describe('emitSwiftRoute', () => {
|
|
25
|
+
it('emits an api-kind route with path params and a response', () => {
|
|
26
|
+
const route = {
|
|
27
|
+
kind: 'api',
|
|
28
|
+
name: 'GetUser',
|
|
29
|
+
method: 'GET',
|
|
30
|
+
fullPath: '/users/:id',
|
|
31
|
+
schema: {
|
|
32
|
+
input: {
|
|
33
|
+
pathParams: { type: 'object' },
|
|
34
|
+
},
|
|
35
|
+
returnType: { type: 'object' },
|
|
36
|
+
},
|
|
37
|
+
errors: [],
|
|
38
|
+
};
|
|
39
|
+
const emitter = createStubSwiftEmitter({
|
|
40
|
+
PathParams: ok('public struct PathParams: Codable { public let id: String }', 'PathParams'),
|
|
41
|
+
Response: ok('public struct Response: Codable { public let id: String; public let name: String }', 'Response'),
|
|
42
|
+
});
|
|
43
|
+
const result = emitSwiftRoute(route, emitter, noErrors);
|
|
44
|
+
expect(result.imports).toContain('Foundation');
|
|
45
|
+
expect(result.code).toContain('public static let method = "GET"');
|
|
46
|
+
expect(result.code).toContain('public static let pathTemplate = "/users/{id}"');
|
|
47
|
+
expect(result.code).toContain('public static func path(_ p: PathParams) -> String { return "/users/\\(p.id)" }');
|
|
48
|
+
expect(result.code).toContain('public struct PathParams: Codable { public let id: String }');
|
|
49
|
+
expect(result.code).toContain('public struct Response: Codable { public let id: String; public let name: String }');
|
|
50
|
+
});
|
|
51
|
+
it('emits a route with no path params using a path constant', () => {
|
|
52
|
+
const route = {
|
|
53
|
+
kind: 'api',
|
|
54
|
+
name: 'CreateUser',
|
|
55
|
+
method: 'POST',
|
|
56
|
+
fullPath: '/users',
|
|
57
|
+
schema: { input: { body: { type: 'object' } }, returnType: { type: 'object' } },
|
|
58
|
+
errors: [],
|
|
59
|
+
};
|
|
60
|
+
const emitter = createStubSwiftEmitter({
|
|
61
|
+
Body: ok('public struct Body: Codable { public let name: String }', 'Body'),
|
|
62
|
+
Response: ok('public struct Response: Codable { public let id: String }', 'Response'),
|
|
63
|
+
});
|
|
64
|
+
const result = emitSwiftRoute(route, emitter, noErrors);
|
|
65
|
+
expect(result.code).toContain('public static let path = "/users"');
|
|
66
|
+
expect(result.code).not.toContain('static func path(');
|
|
67
|
+
});
|
|
68
|
+
it('emits an Errors namespace for routes whose error keys have schemas in the envelope', () => {
|
|
69
|
+
const route = {
|
|
70
|
+
kind: 'api',
|
|
71
|
+
name: 'GetUser',
|
|
72
|
+
method: 'GET',
|
|
73
|
+
fullPath: '/users/:id',
|
|
74
|
+
schema: { input: { pathParams: { type: 'object' } }, returnType: { type: 'object' } },
|
|
75
|
+
errors: ['NotFound'],
|
|
76
|
+
};
|
|
77
|
+
const emitter = createStubSwiftEmitter({
|
|
78
|
+
PathParams: ok('public struct PathParams: Codable { public let id: String }', 'PathParams'),
|
|
79
|
+
Response: ok('public struct Response: Codable { public let id: String }', 'Response'),
|
|
80
|
+
NotFound: ok('public struct NotFound: Codable { public let name: String; public let message: String }', 'NotFound'),
|
|
81
|
+
});
|
|
82
|
+
const errorSchemas = new Map([['NotFound', { type: 'object' }]]);
|
|
83
|
+
const result = emitSwiftRoute(route, emitter, errorSchemas);
|
|
84
|
+
expect(result.code).toContain('public enum Errors {');
|
|
85
|
+
expect(result.code).toContain('public struct NotFound: Codable { public let name: String; public let message: String }');
|
|
86
|
+
});
|
|
87
|
+
it('silently skips error keys with no schema in the envelope map', () => {
|
|
88
|
+
const route = {
|
|
89
|
+
kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users',
|
|
90
|
+
schema: {}, errors: ['UnknownTaxonomyKey'],
|
|
91
|
+
};
|
|
92
|
+
const result = emitSwiftRoute(route, createStubSwiftEmitter({}), new Map());
|
|
93
|
+
expect(result.code).not.toContain('enum Errors {');
|
|
94
|
+
});
|
|
95
|
+
it('returns skipped:true for stream routes', () => {
|
|
96
|
+
const route = { kind: 'stream', name: 'WatchUsers', method: 'GET', path: '/users/stream', schema: {}, errors: [] };
|
|
97
|
+
const result = emitSwiftRoute(route, createStubSwiftEmitter({}), noErrors);
|
|
98
|
+
expect(result.code).toBe('');
|
|
99
|
+
expect(result.skipped).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
it('passes inlineTypes:true plus serializer/accessLevel/unsupportedUnions to every slot emit', () => {
|
|
102
|
+
const route = {
|
|
103
|
+
kind: 'api',
|
|
104
|
+
name: 'GetUser',
|
|
105
|
+
method: 'GET',
|
|
106
|
+
fullPath: '/users/:id',
|
|
107
|
+
schema: {
|
|
108
|
+
input: { pathParams: { type: 'object' } },
|
|
109
|
+
returnType: { type: 'object' },
|
|
110
|
+
},
|
|
111
|
+
errors: ['NotFound'],
|
|
112
|
+
};
|
|
113
|
+
const { emitter, calls } = makeSpyEmitter({
|
|
114
|
+
PathParams: ok('public struct PathParams { public let id: String }', 'PathParams'),
|
|
115
|
+
Response: ok('public struct Response { public let id: String }', 'Response'),
|
|
116
|
+
NotFound: ok('public struct NotFound { public let message: String }', 'NotFound'),
|
|
117
|
+
});
|
|
118
|
+
emitSwiftRoute(route, emitter, new Map([['NotFound', { type: 'object' }]]), {
|
|
119
|
+
serializer: 'none',
|
|
120
|
+
accessLevel: 'public',
|
|
121
|
+
unsupportedUnions: 'fallback',
|
|
122
|
+
});
|
|
123
|
+
// 3 emits: PathParams, Response, NotFound
|
|
124
|
+
expect(calls.length).toBe(3);
|
|
125
|
+
for (const c of calls) {
|
|
126
|
+
expect(c.opts.inlineTypes).toBe(true);
|
|
127
|
+
expect(c.opts.serializer).toBe('none');
|
|
128
|
+
expect(c.opts.accessLevel).toBe('public');
|
|
129
|
+
expect(c.opts.unsupportedUnions).toBe('fallback');
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
it('preserves slot order: pathParams → query → body → response → errors', () => {
|
|
133
|
+
const route = {
|
|
134
|
+
kind: 'api',
|
|
135
|
+
name: 'X',
|
|
136
|
+
method: 'POST',
|
|
137
|
+
fullPath: '/x/:id',
|
|
138
|
+
schema: {
|
|
139
|
+
input: {
|
|
140
|
+
pathParams: { type: 'object' },
|
|
141
|
+
query: { type: 'object' },
|
|
142
|
+
body: { type: 'object' },
|
|
143
|
+
},
|
|
144
|
+
returnType: { type: 'object' },
|
|
145
|
+
},
|
|
146
|
+
errors: ['Z'],
|
|
147
|
+
};
|
|
148
|
+
const { emitter, calls } = makeSpyEmitter({
|
|
149
|
+
PathParams: ok('a', 'PathParams'),
|
|
150
|
+
Query: ok('b', 'Query'),
|
|
151
|
+
Body: ok('c', 'Body'),
|
|
152
|
+
Response: ok('d', 'Response'),
|
|
153
|
+
Z: ok('e', 'Z'),
|
|
154
|
+
});
|
|
155
|
+
emitSwiftRoute(route, emitter, new Map([['Z', { type: 'object' }]]), {});
|
|
156
|
+
expect(calls.map((c) => c.opts.rootTypeName)).toEqual([
|
|
157
|
+
'PathParams', 'Query', 'Body', 'Response', 'Z',
|
|
158
|
+
]);
|
|
159
|
+
});
|
|
160
|
+
it('threads passthrough opts (arrayItemNaming/depluralize/uncountableWords) verbatim', () => {
|
|
161
|
+
const route = {
|
|
162
|
+
kind: 'api',
|
|
163
|
+
name: 'X',
|
|
164
|
+
method: 'GET',
|
|
165
|
+
fullPath: '/x',
|
|
166
|
+
schema: { returnType: { type: 'object' } },
|
|
167
|
+
errors: [],
|
|
168
|
+
};
|
|
169
|
+
const { emitter, calls } = makeSpyEmitter({
|
|
170
|
+
Response: ok('public struct Response: Codable { public let ok: Bool }', 'Response'),
|
|
171
|
+
});
|
|
172
|
+
emitSwiftRoute(route, emitter, new Map(), {
|
|
173
|
+
arrayItemNaming: false,
|
|
174
|
+
depluralize: true,
|
|
175
|
+
uncountableWords: ['data', 'metadata'],
|
|
176
|
+
});
|
|
177
|
+
expect(calls).toHaveLength(1);
|
|
178
|
+
expect(calls[0].opts.arrayItemNaming).toBe(false);
|
|
179
|
+
expect(calls[0].opts.depluralize).toBe(true);
|
|
180
|
+
expect(calls[0].opts.uncountableWords).toEqual(['data', 'metadata']);
|
|
181
|
+
});
|
|
182
|
+
it('does not include passthrough keys when caller omits them', () => {
|
|
183
|
+
const route = {
|
|
184
|
+
kind: 'api',
|
|
185
|
+
name: 'X',
|
|
186
|
+
method: 'GET',
|
|
187
|
+
fullPath: '/x',
|
|
188
|
+
schema: { returnType: { type: 'object' } },
|
|
189
|
+
errors: [],
|
|
190
|
+
};
|
|
191
|
+
const { emitter, calls } = makeSpyEmitter({
|
|
192
|
+
Response: ok('public struct Response: Codable { public let ok: Bool }', 'Response'),
|
|
193
|
+
});
|
|
194
|
+
emitSwiftRoute(route, emitter, new Map(), {});
|
|
195
|
+
expect(calls).toHaveLength(1);
|
|
196
|
+
// Conditional-spread invariant: undefined opts must NOT be forwarded as keys
|
|
197
|
+
// (otherwise ajsc would receive `{ arrayItemNaming: undefined }` etc, which
|
|
198
|
+
// could shadow ajsc-side defaults).
|
|
199
|
+
expect('arrayItemNaming' in calls[0].opts).toBe(false);
|
|
200
|
+
expect('depluralize' in calls[0].opts).toBe(false);
|
|
201
|
+
expect('uncountableWords' in calls[0].opts).toBe(false);
|
|
202
|
+
// serializer / accessLevel / unsupportedUnions same invariant
|
|
203
|
+
expect('serializer' in calls[0].opts).toBe(false);
|
|
204
|
+
expect('accessLevel' in calls[0].opts).toBe(false);
|
|
205
|
+
expect('unsupportedUnions' in calls[0].opts).toBe(false);
|
|
206
|
+
// inlineTypes is always set
|
|
207
|
+
expect(calls[0].opts.inlineTypes).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
it('defaults the static-member access level to public', () => {
|
|
210
|
+
const route = {
|
|
211
|
+
kind: 'api',
|
|
212
|
+
name: 'GetUser',
|
|
213
|
+
method: 'GET',
|
|
214
|
+
fullPath: '/users/:id',
|
|
215
|
+
schema: { input: { pathParams: { type: 'object' } } },
|
|
216
|
+
errors: [],
|
|
217
|
+
};
|
|
218
|
+
const emitter = createStubSwiftEmitter({
|
|
219
|
+
PathParams: ok('public struct PathParams { public let id: String }', 'PathParams'),
|
|
220
|
+
});
|
|
221
|
+
const result = emitSwiftRoute(route, emitter, noErrors);
|
|
222
|
+
expect(result.code).toContain('public static let method = "GET"');
|
|
223
|
+
expect(result.code).toContain('public static let pathTemplate = "/users/{id}"');
|
|
224
|
+
expect(result.code).toContain('public static func path(_ p: PathParams) -> String');
|
|
225
|
+
});
|
|
226
|
+
it('honors accessLevel="internal" on the static members and the Errors enum', () => {
|
|
227
|
+
const route = {
|
|
228
|
+
kind: 'api',
|
|
229
|
+
name: 'GetUser',
|
|
230
|
+
method: 'GET',
|
|
231
|
+
fullPath: '/users/:id',
|
|
232
|
+
schema: { input: { pathParams: { type: 'object' } } },
|
|
233
|
+
errors: ['NotFound'],
|
|
234
|
+
};
|
|
235
|
+
const emitter = createStubSwiftEmitter({
|
|
236
|
+
PathParams: ok('internal struct PathParams { internal let id: String }', 'PathParams'),
|
|
237
|
+
NotFound: ok('internal struct NotFound { internal let message: String }', 'NotFound'),
|
|
238
|
+
});
|
|
239
|
+
const result = emitSwiftRoute(route, emitter, new Map([['NotFound', { type: 'object' }]]), { accessLevel: 'internal' });
|
|
240
|
+
expect(result.code).toContain('internal static let method = "GET"');
|
|
241
|
+
expect(result.code).toContain('internal static let pathTemplate = "/users/{id}"');
|
|
242
|
+
expect(result.code).toContain('internal static func path(_ p: PathParams) -> String');
|
|
243
|
+
expect(result.code).toContain('internal enum Errors {');
|
|
244
|
+
});
|
|
245
|
+
it('emits accessLevel="internal" on the path constant for no-param routes', () => {
|
|
246
|
+
const route = {
|
|
247
|
+
kind: 'api',
|
|
248
|
+
name: 'CreateUser',
|
|
249
|
+
method: 'POST',
|
|
250
|
+
fullPath: '/users',
|
|
251
|
+
schema: {},
|
|
252
|
+
errors: [],
|
|
253
|
+
};
|
|
254
|
+
const result = emitSwiftRoute(route, createStubSwiftEmitter({}), noErrors, { accessLevel: 'internal' });
|
|
255
|
+
expect(result.code).toContain('internal static let path = "/users"');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
//# sourceMappingURL=emit-route-swift.test.js.map
|