ts-procedures 6.0.0 → 6.0.2
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/claude-code/agents/ts-procedures-architect.md +1 -1
- package/agent_config/claude-code/skills/ts-procedures/SKILL.md +1 -1
- package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +2 -2
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +15 -27
- package/agent_config/claude-code/skills/ts-procedures/patterns.md +11 -4
- package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +2 -2
- package/agent_config/copilot/copilot-instructions.md +3 -2
- package/agent_config/cursor/cursorrules +3 -2
- package/build/codegen/targets/kotlin/ajsc-adapter.d.ts +24 -0
- package/build/codegen/targets/kotlin/ajsc-adapter.js +33 -0
- package/build/codegen/targets/kotlin/ajsc-adapter.js.map +1 -0
- package/build/codegen/targets/kotlin/ajsc-adapter.test.d.ts +1 -0
- package/build/codegen/targets/kotlin/ajsc-adapter.test.js +19 -0
- package/build/codegen/targets/kotlin/ajsc-adapter.test.js.map +1 -0
- package/build/codegen/targets/kotlin/e2e-compile.test.d.ts +1 -0
- package/build/codegen/targets/kotlin/e2e-compile.test.js +43 -0
- package/build/codegen/targets/kotlin/e2e-compile.test.js.map +1 -0
- package/build/codegen/targets/kotlin/emit-route-kotlin.d.ts +11 -0
- package/build/codegen/targets/kotlin/emit-route-kotlin.js +73 -0
- package/build/codegen/targets/kotlin/emit-route-kotlin.js.map +1 -0
- package/build/codegen/targets/kotlin/emit-route-kotlin.test.d.ts +1 -0
- package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +88 -0
- package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -0
- package/build/codegen/targets/kotlin/emit-scope-kotlin.d.ts +11 -0
- package/build/codegen/targets/kotlin/emit-scope-kotlin.js +35 -0
- package/build/codegen/targets/kotlin/emit-scope-kotlin.js.map +1 -0
- package/build/codegen/targets/kotlin/emit-scope-kotlin.test.d.ts +1 -0
- package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +52 -0
- package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -0
- package/build/codegen/targets/kotlin/format-kotlin.d.ts +4 -0
- package/build/codegen/targets/kotlin/format-kotlin.js +20 -0
- package/build/codegen/targets/kotlin/format-kotlin.js.map +1 -0
- package/build/codegen/targets/kotlin/format-kotlin.test.d.ts +1 -0
- package/build/codegen/targets/kotlin/format-kotlin.test.js +24 -0
- package/build/codegen/targets/kotlin/format-kotlin.test.js.map +1 -0
- package/build/codegen/targets/kotlin/integration.test.d.ts +1 -0
- package/build/codegen/targets/kotlin/integration.test.js +34 -0
- package/build/codegen/targets/kotlin/integration.test.js.map +1 -0
- package/build/implementations/http/doc-registry.d.ts +14 -19
- package/build/implementations/http/doc-registry.js +41 -46
- package/build/implementations/http/doc-registry.js.map +1 -1
- package/build/implementations/http/doc-registry.test.js +141 -10
- package/build/implementations/http/doc-registry.test.js.map +1 -1
- package/build/implementations/http/error-taxonomy.d.ts +11 -2
- package/build/implementations/http/error-taxonomy.js +24 -2
- package/build/implementations/http/error-taxonomy.js.map +1 -1
- package/build/implementations/http/route-errors.test.js +5 -6
- package/build/implementations/http/route-errors.test.js.map +1 -1
- package/build/implementations/types.d.ts +13 -1
- package/docs/http-integrations.md +39 -5
- package/docs/superpowers/plans/2026-04-24-doc-registry-simplification.md +886 -0
- package/docs/superpowers/plans/2026-04-24-kotlin-codegen-target.md +1265 -0
- package/docs/superpowers/specs/2026-04-24-kotlin-swift-codegen-design.md +401 -0
- package/package.json +1 -1
- package/src/implementations/http/README.md +4 -3
- package/src/implementations/http/doc-registry.test.ts +154 -10
- package/src/implementations/http/doc-registry.ts +46 -53
- package/src/implementations/http/error-taxonomy.ts +26 -2
- package/src/implementations/http/express-rpc/README.md +2 -2
- package/src/implementations/http/hono-rpc/README.md +2 -2
- package/src/implementations/http/hono-stream/README.md +15 -0
- package/src/implementations/http/route-errors.test.ts +5 -6
- package/src/implementations/types.ts +13 -1
|
@@ -37,7 +37,7 @@ When asked to plan an API or procedure set:
|
|
|
37
37
|
- AJV is configured with `allErrors: true`, `coerceTypes: true`, `removeAdditional: true`.
|
|
38
38
|
- `schema.params` and `schema.input` are mutually exclusive — defining both throws `ProcedureRegistrationError`.
|
|
39
39
|
- Path param names in route template (`:id`) must match `schema.input.pathParams` property names.
|
|
40
|
-
- Use `DocRegistry` to compose route docs from multiple builders — never manually wire `/docs` endpoints.
|
|
40
|
+
- Use `DocRegistry` to compose route docs from multiple builders — never manually wire `/docs` endpoints. Pass your taxonomy directly: `new DocRegistry({ errors: appErrors })` — framework defaults are auto-merged and deduped. For errors outside your taxonomy (middleware, infrastructure, doc-only), chain `.documentError(...docs)`.
|
|
41
41
|
- Two first-class peer error-handling modes: **declarative taxonomy** (`defineErrorTaxonomy` + `errors` config) OR **imperative callback** (`onError`). Neither is deprecated. Pick the taxonomy for structured apps with typed client dispatch; pick `onError` for simple apps or full response control. Mixing both is allowed — the taxonomy handles what it covers, `onError` handles the tail. The anti-pattern is `instanceof` ladders inside `onError` (see anti-pattern #20 in the skill reference) — that's exactly what the taxonomy expresses declaratively.
|
|
42
42
|
- Per-route `errors: ['UseCaseError', ...]` narrows typed errors on the generated client. Declare via `APIConfig<keyof typeof appErrors & string>` / `RPCConfig<keyof typeof appErrors & string>` for compile-time typo protection.
|
|
43
43
|
- Generated `_errors.ts` emits real runtime classes extending `${Service}ProcedureError` — consumers catch with `instanceof ${Service}Errors.${Name}` and access `err.body`, `err.status`, `err.procedureName`, `err.scope`. Use the generated `create${Service}Client(config)` factory to wire the error registry automatically.
|
|
@@ -171,7 +171,7 @@ The npm package ships user-facing documentation with narrative explanations and
|
|
|
171
171
|
|------|--------|
|
|
172
172
|
| `docs/core.md` | Procedures factory, Create, CreateStream, schema.input, error handling |
|
|
173
173
|
| `docs/streaming.md` | Streaming procedures, AbortSignal, SSE patterns |
|
|
174
|
-
| `docs/http-integrations.md` | Express RPC, Hono RPC/Stream/API builders, **error taxonomy (canonical)**, DocRegistry
|
|
174
|
+
| `docs/http-integrations.md` | Express RPC, Hono RPC/Stream/API builders, **error taxonomy (canonical)**, DocRegistry (unified constructor, `.documentError()`) |
|
|
175
175
|
| `docs/client-and-codegen.md` | Client code generation, `createApiClient`/`createClient`, typed error dispatch, per-route `Errors` unions, per-call options, client-level defaults, typed RequestMeta augmentation, CLI options |
|
|
176
176
|
| `CHANGELOG.md` | Release notes — see `[6.0.0]` for the current peer error-handling model (taxonomy + `onError` + `onRequestError`), per-route errors, and client runtime error classes. |
|
|
177
177
|
|
|
@@ -476,13 +476,13 @@ import { DocRegistry } from 'ts-procedures/http-docs'
|
|
|
476
476
|
const docs = new DocRegistry({
|
|
477
477
|
basePath: '/api',
|
|
478
478
|
headers: [{ name: 'Authorization', description: 'Bearer token' }],
|
|
479
|
-
errors:
|
|
479
|
+
errors: appErrors, // your ErrorTaxonomy — framework defaults auto-merged
|
|
480
480
|
}).from(rpcBuilder).from(apiBuilder).from(streamBuilder)
|
|
481
481
|
|
|
482
482
|
app.get('/docs', (c) => c.json(docs.toJSON()))
|
|
483
483
|
```
|
|
484
484
|
|
|
485
|
-
**Why:** `DocRegistry` provides a typed `DocEnvelope`,
|
|
485
|
+
**Why:** `DocRegistry` provides a typed `DocEnvelope`, auto-merges framework error defaults (`ProcedureError`, `ProcedureValidationError`, `ProcedureYieldValidationError`, `ProcedureRegistrationError`) with JSON Schemas, reads docs lazily (order-independent), and supports filtering and transformation via `toJSON()` options.
|
|
486
486
|
|
|
487
487
|
---
|
|
488
488
|
|
|
@@ -295,13 +295,7 @@ When `toResponse` is omitted, the body defaults to `{ name: key, message: err.me
|
|
|
295
295
|
|
|
296
296
|
Pre-built taxonomy covering `ProcedureValidationError` (400), `ProcedureYieldValidationError` (500), and direct `ProcedureError` (500, unwrapped throws only — wrapped ones fall through so user taxonomy / `unknownError` sees the real error). Applied as a fallback after the user taxonomy unless `includeDefaults: false`.
|
|
297
297
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
```typescript
|
|
301
|
-
function taxonomyToErrorDocs(taxonomy: ErrorTaxonomy): ErrorDoc[]
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
Converts a taxonomy to the `ErrorDoc[]` format consumed by `DocRegistry.errors` — single source of truth so the documented shape cannot drift from runtime behavior.
|
|
298
|
+
Taxonomy-to-doc conversion is handled automatically by `DocRegistry` when you pass your taxonomy to the constructor. There is no public helper.
|
|
305
299
|
|
|
306
300
|
### resolveErrorResponse(params)
|
|
307
301
|
|
|
@@ -795,30 +789,24 @@ import type { DocEnvelope, DocRegistryConfig, DocRegistryOutputOptions, HeaderDo
|
|
|
795
789
|
```
|
|
796
790
|
|
|
797
791
|
```typescript
|
|
792
|
+
import { DocRegistry } from 'ts-procedures/http-docs'
|
|
793
|
+
import type { DocEnvelope, DocRegistryConfig, DocRegistryOutputOptions, HeaderDoc, ErrorDoc, DocSource } from 'ts-procedures/http-docs'
|
|
794
|
+
|
|
795
|
+
interface DocRegistryConfig {
|
|
796
|
+
basePath?: string
|
|
797
|
+
headers?: HeaderDoc[]
|
|
798
|
+
errors?: ErrorTaxonomy | ErrorDoc[] // polymorphic — pass taxonomy or raw docs
|
|
799
|
+
includeDefaults?: boolean // default: true (auto-merges framework defaults)
|
|
800
|
+
}
|
|
801
|
+
|
|
798
802
|
class DocRegistry {
|
|
799
|
-
constructor(config?:
|
|
800
|
-
basePath?: string
|
|
801
|
-
headers?: HeaderDoc[]
|
|
802
|
-
errors?: ErrorDoc[]
|
|
803
|
-
})
|
|
803
|
+
constructor(config?: DocRegistryConfig)
|
|
804
804
|
|
|
805
805
|
from(source: DocSource<AnyHttpRouteDoc>): this
|
|
806
|
+
documentError(...docs: ErrorDoc[]): this
|
|
807
|
+
toJSON<T = DocEnvelope>(options?: DocRegistryOutputOptions<T>): T
|
|
806
808
|
|
|
807
|
-
toJSON<T = DocEnvelope>(options?: {
|
|
808
|
-
filter?: (route: AnyHttpRouteDoc) => boolean
|
|
809
|
-
transform?: (envelope: DocEnvelope) => T
|
|
810
|
-
}): T
|
|
811
|
-
|
|
812
|
-
// Framework error defaults derived from defaultErrorTaxonomy — single source
|
|
813
|
-
// of truth so runtime and documented shapes cannot drift.
|
|
814
809
|
static defaultErrors(): ErrorDoc[]
|
|
815
|
-
|
|
816
|
-
// Convenience: seed envelope errors from a taxonomy + framework defaults
|
|
817
|
-
// in one call (deduped — user entries win when keys overlap).
|
|
818
|
-
static fromTaxonomy(
|
|
819
|
-
taxonomy: ErrorTaxonomy,
|
|
820
|
-
config?: Omit<DocRegistryConfig, 'errors'> & { includeDefaults?: boolean }
|
|
821
|
-
): DocRegistry
|
|
822
810
|
}
|
|
823
811
|
```
|
|
824
812
|
|
|
@@ -875,7 +863,7 @@ type AnyHttpRouteDoc = RPCHttpRouteDoc | APIHttpRouteDoc | StreamHttpRouteDoc
|
|
|
875
863
|
|
|
876
864
|
- `from()` stores a **reference** to the builder — routes are read lazily at `toJSON()` time, so builders can be registered before or after `.build()`
|
|
877
865
|
- `toJSON()` collects routes via `flatMap(source => source.docs)`, applies optional `filter`, builds envelope, then applies optional `transform`
|
|
878
|
-
- `defaultErrors()` returns 4 `ErrorDoc` entries describing `ProcedureError` (500), `ProcedureValidationError` (400), `ProcedureYieldValidationError` (500), `ProcedureRegistrationError` (500) — each with a JSON Schema for the error response body shape
|
|
866
|
+
- `defaultErrors()` returns 4 `ErrorDoc` entries describing `ProcedureError` (500), `ProcedureValidationError` (400), `ProcedureYieldValidationError` (500), `ProcedureRegistrationError` (500) — each with a JSON Schema for the error response body shape. Most consumers do not need to call this directly; the `DocRegistry` constructor auto-merges these unless `includeDefaults: false` is passed.
|
|
879
867
|
- `JSON.stringify(registry)` works directly (calls `toJSON()` implicitly)
|
|
880
868
|
|
|
881
869
|
---
|
|
@@ -186,7 +186,7 @@ Seed the DocEnvelope with both taxonomy errors and framework defaults in one cal
|
|
|
186
186
|
```typescript
|
|
187
187
|
import { DocRegistry } from 'ts-procedures/http-docs'
|
|
188
188
|
|
|
189
|
-
const envelope = DocRegistry
|
|
189
|
+
const envelope = new DocRegistry({ errors: appErrors, basePath: '/api' })
|
|
190
190
|
.from(apiApp)
|
|
191
191
|
.from(rpcApp)
|
|
192
192
|
.toJSON()
|
|
@@ -194,6 +194,13 @@ const envelope = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' })
|
|
|
194
194
|
// envelope.routes[i].errors: per-route subset declared on config.errors
|
|
195
195
|
```
|
|
196
196
|
|
|
197
|
+
```typescript
|
|
198
|
+
// For errors outside your taxonomy (middleware, infrastructure, doc-only):
|
|
199
|
+
const docsWithExtras = new DocRegistry({ errors: appErrors, basePath: '/api' })
|
|
200
|
+
.from(apiBuilder)
|
|
201
|
+
.documentError({ name: 'RateLimitExceeded', statusCode: 429, description: 'too many requests' })
|
|
202
|
+
```
|
|
203
|
+
|
|
197
204
|
### Typed catch blocks on the client (via codegen)
|
|
198
205
|
|
|
199
206
|
The codegen emits runtime error classes (not just types) and a registry object. `createApiClient` wires the registry automatically so non-2xx responses arrive as typed class instances.
|
|
@@ -805,7 +812,7 @@ import { DocRegistry } from 'ts-procedures/http-docs'
|
|
|
805
812
|
const docs = new DocRegistry({
|
|
806
813
|
basePath: '/api',
|
|
807
814
|
headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
|
|
808
|
-
errors:
|
|
815
|
+
errors: appErrors, // your ErrorTaxonomy — framework defaults auto-merged and deduped
|
|
809
816
|
})
|
|
810
817
|
.from(rpcBuilder)
|
|
811
818
|
.from(apiBuilder)
|
|
@@ -824,7 +831,7 @@ app.get('/docs', (c) => c.json(docs.toJSON({
|
|
|
824
831
|
**Key points:**
|
|
825
832
|
- `from()` stores a reference — register builders before or after `.build()`
|
|
826
833
|
- `toJSON()` reads docs lazily, so late-registered procedures are included
|
|
827
|
-
- `
|
|
834
|
+
- Pass your `ErrorTaxonomy` directly to `errors` — the constructor auto-merges framework defaults and dedupes; opt out with `includeDefaults: false`
|
|
828
835
|
- Accepts any object satisfying `{ readonly docs: AnyHttpRouteDoc[] }` — not limited to built-in builders
|
|
829
836
|
|
|
830
837
|
---
|
|
@@ -926,7 +933,7 @@ import { DocRegistry } from 'ts-procedures/http-docs'
|
|
|
926
933
|
const docs = new DocRegistry({
|
|
927
934
|
basePath: '/api',
|
|
928
935
|
headers: [{ name: 'Authorization', description: 'Bearer token' }],
|
|
929
|
-
errors:
|
|
936
|
+
errors: appErrors, // your ErrorTaxonomy — framework defaults auto-merged
|
|
930
937
|
})
|
|
931
938
|
.from(rpcBuilder)
|
|
932
939
|
.from(apiBuilder)
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
|
|
75
75
|
### SUGGESTION
|
|
76
76
|
- [ ] Route documentation accessed via `builder.docs` for OpenAPI generation
|
|
77
|
-
- [ ] Uses `DocRegistry
|
|
77
|
+
- [ ] Uses `new DocRegistry({ errors: appErrors })` to compose docs from multiple builders
|
|
78
78
|
- [ ] Lifecycle hooks used for observability (logging, metrics)
|
|
79
79
|
- [ ] Per-route `errors: [...]` declared so generated clients can narrow `catch` types
|
|
80
80
|
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
|
|
116
116
|
### SUGGESTION
|
|
117
117
|
- [ ] Route documentation accessed via `builder.docs` for OpenAPI generation
|
|
118
|
-
- [ ] Uses `DocRegistry
|
|
118
|
+
- [ ] Uses `new DocRegistry({ errors: appErrors })` to compose docs across builders instead of manual assembly
|
|
119
119
|
- [ ] Custom `queryParser` provided if complex query string formats needed
|
|
120
120
|
- [ ] Lifecycle hooks used for observability (logging, metrics)
|
|
121
121
|
|
|
@@ -218,7 +218,8 @@ class UseCaseError extends Error {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
const appErrors = defineErrorTaxonomy({
|
|
221
|
-
//
|
|
221
|
+
// class: entries are topologically sorted (subclasses checked first) automatically.
|
|
222
|
+
// Predicate (match:) entries keep declared order — put narrower predicates first.
|
|
222
223
|
UseCaseError: {
|
|
223
224
|
class: UseCaseError,
|
|
224
225
|
statusCode: 422,
|
|
@@ -254,7 +255,7 @@ const API = Procedures<Ctx, MyAPIConfig>()
|
|
|
254
255
|
API.Create('GetUser', { path: '/users/:id', method: 'get', errors: ['UseCaseError'], /* ... */ }, ...)
|
|
255
256
|
|
|
256
257
|
// Seed envelope errors from the taxonomy + framework defaults in one call
|
|
257
|
-
const envelope = DocRegistry
|
|
258
|
+
const envelope = new DocRegistry({ errors: appErrors, basePath: '/api' }).from(apiApp).toJSON()
|
|
258
259
|
```
|
|
259
260
|
|
|
260
261
|
### Client-side typed catch blocks (via codegen)
|
|
@@ -218,7 +218,8 @@ class UseCaseError extends Error {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
const appErrors = defineErrorTaxonomy({
|
|
221
|
-
//
|
|
221
|
+
// class: entries are topologically sorted (subclasses checked first) automatically.
|
|
222
|
+
// Predicate (match:) entries keep declared order — put narrower predicates first.
|
|
222
223
|
UseCaseError: {
|
|
223
224
|
class: UseCaseError,
|
|
224
225
|
statusCode: 422,
|
|
@@ -254,7 +255,7 @@ const API = Procedures<Ctx, MyAPIConfig>()
|
|
|
254
255
|
API.Create('GetUser', { path: '/users/:id', method: 'get', errors: ['UseCaseError'], /* ... */ }, ...)
|
|
255
256
|
|
|
256
257
|
// Seed envelope errors from the taxonomy + framework defaults in one call
|
|
257
|
-
const envelope = DocRegistry
|
|
258
|
+
const envelope = new DocRegistry({ errors: appErrors, basePath: '/api' }).from(apiApp).toJSON()
|
|
258
259
|
```
|
|
259
260
|
|
|
260
261
|
### Client-side typed catch blocks (via codegen)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface KotlinEmitResult {
|
|
2
|
+
code: string;
|
|
3
|
+
rootTypeName: string;
|
|
4
|
+
extractedTypeNames: string[];
|
|
5
|
+
imports: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface KotlinEmitOptions {
|
|
8
|
+
rootTypeName: string;
|
|
9
|
+
inlineTypes?: boolean;
|
|
10
|
+
serializer?: 'kotlinx';
|
|
11
|
+
enumStyle?: string;
|
|
12
|
+
depluralize?: boolean;
|
|
13
|
+
arrayItemNaming?: string;
|
|
14
|
+
uncountableWords?: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface KotlinEmitter {
|
|
17
|
+
emit(schema: Record<string, unknown>, opts: KotlinEmitOptions): KotlinEmitResult;
|
|
18
|
+
}
|
|
19
|
+
export declare function createStubKotlinEmitter(results: Record<string, KotlinEmitResult>): KotlinEmitter;
|
|
20
|
+
/**
|
|
21
|
+
* Resolves the production Kotlin emitter from `ajsc`. Throws a clear error
|
|
22
|
+
* if the ajsc package does not yet expose `emitKotlin` (Phase A pending).
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveProductionKotlinEmitter(): Promise<KotlinEmitter>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function createStubKotlinEmitter(results) {
|
|
2
|
+
return {
|
|
3
|
+
emit(_schema, opts) {
|
|
4
|
+
const result = results[opts.rootTypeName];
|
|
5
|
+
if (result == null) {
|
|
6
|
+
throw new Error(`[stub-kotlin-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 Kotlin emitter from `ajsc`. Throws a clear error
|
|
15
|
+
* if the ajsc package does not yet expose `emitKotlin` (Phase A pending).
|
|
16
|
+
*/
|
|
17
|
+
export async function resolveProductionKotlinEmitter() {
|
|
18
|
+
// TODO(ajsc-phase-a): replace dynamic import with a static import once ajsc ships emitKotlin.
|
|
19
|
+
const ajsc = await import('ajsc').catch(() => null);
|
|
20
|
+
const emitKotlin = ajsc?.emitKotlin;
|
|
21
|
+
if (typeof emitKotlin !== 'function') {
|
|
22
|
+
throw new Error('[ts-procedures-codegen] ajsc.emitKotlin is not available. ' +
|
|
23
|
+
'Kotlin codegen requires ajsc Phase A. See docs/superpowers/specs/2026-04-24-kotlin-swift-codegen-design.md.');
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
emit(schema, opts) {
|
|
27
|
+
// ajsc's return shape is normalized to KotlinEmitResult here.
|
|
28
|
+
const r = emitKotlin(schema, opts);
|
|
29
|
+
return r;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=ajsc-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ajsc-adapter.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/ajsc-adapter.ts"],"names":[],"mappings":"AAqBA,MAAM,UAAU,uBAAuB,CACrC,OAAyC;IAEzC,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,6DAA6D,IAAI,CAAC,YAAY,KAAK;oBACjF,iCAAiC,CACpC,CAAA;YACH,CAAC;YACD,OAAO,MAAM,CAAA;QACf,CAAC;KACF,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,8BAA8B;IAClD,8FAA8F;IAC9F,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;IACnD,MAAM,UAAU,GAAI,IAAwC,EAAE,UAAU,CAAA;IACxE,IAAI,OAAO,UAAU,KAAK,UAAU,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,4DAA4D;YAC5D,6GAA6G,CAC9G,CAAA;IACH,CAAC;IACD,OAAO;QACL,IAAI,CAAC,MAAM,EAAE,IAAI;YACf,8DAA8D;YAC9D,MAAM,CAAC,GAAI,UAA2D,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;YACpF,OAAO,CAAC,CAAA;QACV,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createStubKotlinEmitter } from './ajsc-adapter.js';
|
|
3
|
+
describe('createStubKotlinEmitter', () => {
|
|
4
|
+
it('returns the configured EmitResult for the matching root name', () => {
|
|
5
|
+
const expected = {
|
|
6
|
+
code: '@Serializable data class User(val id: String)',
|
|
7
|
+
rootTypeName: 'User',
|
|
8
|
+
extractedTypeNames: [],
|
|
9
|
+
imports: ['kotlinx.serialization.Serializable'],
|
|
10
|
+
};
|
|
11
|
+
const emitter = createStubKotlinEmitter({ User: expected });
|
|
12
|
+
expect(emitter.emit({ type: 'object' }, { rootTypeName: 'User' })).toEqual(expected);
|
|
13
|
+
});
|
|
14
|
+
it('throws when asked to emit a name not in the stub map', () => {
|
|
15
|
+
const emitter = createStubKotlinEmitter({});
|
|
16
|
+
expect(() => emitter.emit({}, { rootTypeName: 'Missing' })).toThrow(/Missing/);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
//# sourceMappingURL=ajsc-adapter.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ajsc-adapter.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/ajsc-adapter.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,uBAAuB,EAAyB,MAAM,mBAAmB,CAAA;AAElF,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,8DAA8D,EAAE,GAAG,EAAE;QACtE,MAAM,QAAQ,GAAqB;YACjC,IAAI,EAAE,+CAA+C;YACrD,YAAY,EAAE,MAAM;YACpB,kBAAkB,EAAE,EAAE;YACtB,OAAO,EAAE,CAAC,oCAAoC,CAAC;SAChD,CAAA;QACD,MAAM,OAAO,GAAG,uBAAuB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAA;QAC3D,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IACtF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,OAAO,GAAG,uBAAuB,CAAC,EAAE,CAAC,CAAA;QAC3C,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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { runPipeline } from '../../pipeline.js';
|
|
9
|
+
import { resolveProductionKotlinEmitter } from './ajsc-adapter.js';
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
function kotlincAvailable() {
|
|
13
|
+
try {
|
|
14
|
+
execSync('kotlinc -version', { stdio: 'ignore' });
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
describe('kotlin codegen — kotlinc compile (gated)', () => {
|
|
22
|
+
it.skipIf(!kotlincAvailable() || process.env.TS_PROCEDURES_KOTLIN_E2E !== '1')('compiles generated output without errors', async () => {
|
|
23
|
+
const emitter = await resolveProductionKotlinEmitter();
|
|
24
|
+
const envelope = JSON.parse(await readFile(join(__dirname, '__fixtures__/users-envelope.json'), 'utf8'));
|
|
25
|
+
const files = await runPipeline({
|
|
26
|
+
envelope,
|
|
27
|
+
outDir: 'out',
|
|
28
|
+
dryRun: true,
|
|
29
|
+
target: 'kotlin',
|
|
30
|
+
kotlinPackage: 'com.example.api',
|
|
31
|
+
kotlinEmitter: emitter,
|
|
32
|
+
});
|
|
33
|
+
const dir = mkdtempSync(join(tmpdir(), 'tsp-kotlin-e2e-'));
|
|
34
|
+
for (const f of files) {
|
|
35
|
+
writeFileSync(join(dir, f.path.split('/').pop()), f.code);
|
|
36
|
+
}
|
|
37
|
+
// Compile against the kotlinx-serialization runtime jar present in the test env.
|
|
38
|
+
// If the jar isn't on the classpath, this fails; CI must provide it.
|
|
39
|
+
execSync(`kotlinc ${dir}/*.kt -d ${dir}/out.jar`, { stdio: 'inherit' });
|
|
40
|
+
expect(true).toBe(true); // reaching here means compile succeeded
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
//# sourceMappingURL=e2e-compile.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"e2e-compile.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/e2e-compile.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC7C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,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,8BAA8B,EAAE,MAAM,mBAAmB,CAAA;AAElE,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACjD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;AAErC,SAAS,gBAAgB;IACvB,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,QAAQ,CAAC,0CAA0C,EAAE,GAAG,EAAE;IACxD,EAAE,CAAC,MAAM,CAAC,CAAC,gBAAgB,EAAE,IAAI,OAAO,CAAC,GAAG,CAAC,wBAAwB,KAAK,GAAG,CAAC,CAC5E,0CAA0C,EAC1C,KAAK,IAAI,EAAE;QACT,MAAM,OAAO,GAAG,MAAM,8BAA8B,EAAE,CAAA;QACtD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CACzB,MAAM,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,kCAAkC,CAAC,EAAE,MAAM,CAAC,CAC5E,CAAA;QACD,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC;YAC9B,QAAQ;YACR,MAAM,EAAE,KAAK;YACb,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,QAAQ;YAChB,aAAa,EAAE,iBAAiB;YAChC,aAAa,EAAE,OAAO;SACvB,CAAC,CAAA;QACF,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,iBAAiB,CAAC,CAAC,CAAA;QAC1D,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAA;QAC5D,CAAC;QACD,iFAAiF;QACjF,qEAAqE;QACrE,QAAQ,CAAC,WAAW,GAAG,YAAY,GAAG,UAAU,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAA;QACvE,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA,CAAC,wCAAwC;IAClE,CAAC,CACF,CAAA;AACH,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AnyHttpRouteDoc } from '../../../implementations/types.js';
|
|
2
|
+
import type { KotlinEmitter } from './ajsc-adapter.js';
|
|
3
|
+
export interface EmitRouteResult {
|
|
4
|
+
/** Inner body of the `object 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 `object RouteName` identifier. */
|
|
9
|
+
routeName: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function emitKotlinRoute(route: AnyHttpRouteDoc, emitter: KotlinEmitter, errorSchemas: Map<string, unknown>): EmitRouteResult;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { indent } from './format-kotlin.js';
|
|
2
|
+
const COLON_PARAM_RE = /:([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
3
|
+
function toBracePath(template) {
|
|
4
|
+
return template.replace(COLON_PARAM_RE, '{$1}');
|
|
5
|
+
}
|
|
6
|
+
function pathParamNames(template) {
|
|
7
|
+
const names = [];
|
|
8
|
+
for (const match of template.matchAll(COLON_PARAM_RE))
|
|
9
|
+
names.push(match[1]);
|
|
10
|
+
return names;
|
|
11
|
+
}
|
|
12
|
+
function buildPathFn(bracePath, params) {
|
|
13
|
+
if (params.length === 0)
|
|
14
|
+
return `const val path = "${bracePath}"`;
|
|
15
|
+
let body = bracePath;
|
|
16
|
+
for (const name of params)
|
|
17
|
+
body = body.replace(`{${name}}`, `\${p.${name}}`);
|
|
18
|
+
return `fun path(p: PathParams): String = "${body}"`;
|
|
19
|
+
}
|
|
20
|
+
export function emitKotlinRoute(route, emitter, errorSchemas) {
|
|
21
|
+
const kind = route.kind;
|
|
22
|
+
if (kind === 'stream') {
|
|
23
|
+
console.warn(`[ts-procedures-codegen] Skipping stream route "${route.name}" — streams are out of scope for kotlin target.`);
|
|
24
|
+
return { code: '', imports: [], routeName: route.name };
|
|
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 lines = [
|
|
32
|
+
`const val method = "${method}"`,
|
|
33
|
+
`const val pathTemplate = "${bracePath}"`,
|
|
34
|
+
buildPathFn(bracePath, params),
|
|
35
|
+
];
|
|
36
|
+
const imports = [];
|
|
37
|
+
const schema = route.schema ?? {};
|
|
38
|
+
const input = (schema.input ?? {});
|
|
39
|
+
// Per-slot emission. Order is fixed for deterministic output.
|
|
40
|
+
const slots = [
|
|
41
|
+
{ key: 'pathParams', rootName: 'PathParams', source: input.pathParams },
|
|
42
|
+
{ key: 'query', rootName: 'Query', source: input.query },
|
|
43
|
+
{ key: 'body', rootName: 'Body', source: input.body },
|
|
44
|
+
{ key: 'response', rootName: 'Response', source: schema.returnType },
|
|
45
|
+
];
|
|
46
|
+
for (const slot of slots) {
|
|
47
|
+
if (slot.source == null)
|
|
48
|
+
continue;
|
|
49
|
+
const result = emitter.emit(slot.source, { rootTypeName: slot.rootName });
|
|
50
|
+
lines.push('');
|
|
51
|
+
lines.push(result.code);
|
|
52
|
+
imports.push(...result.imports);
|
|
53
|
+
}
|
|
54
|
+
// Errors namespace — route.errors is `string[]` of taxonomy keys; look up each schema
|
|
55
|
+
// from the envelope-level errors map. Keys without schemas are skipped silently
|
|
56
|
+
// (matching the existing TS scope emitter's `errorKeys` filter).
|
|
57
|
+
const routeErrorKeys = (route.errors ?? [])
|
|
58
|
+
.filter((key) => errorSchemas.has(key));
|
|
59
|
+
if (routeErrorKeys.length > 0) {
|
|
60
|
+
const inner = [];
|
|
61
|
+
for (const key of routeErrorKeys) {
|
|
62
|
+
const r = emitter.emit(errorSchemas.get(key), { rootTypeName: key });
|
|
63
|
+
inner.push(r.code);
|
|
64
|
+
imports.push(...r.imports);
|
|
65
|
+
}
|
|
66
|
+
lines.push('');
|
|
67
|
+
lines.push('object Errors {');
|
|
68
|
+
lines.push(indent(inner.join('\n\n'), 1));
|
|
69
|
+
lines.push('}');
|
|
70
|
+
}
|
|
71
|
+
return { code: lines.join('\n'), imports, routeName: route.name };
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=emit-route-kotlin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emit-route-kotlin.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/emit-route-kotlin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { emitKotlinRoute } from './emit-route-kotlin.js';
|
|
3
|
+
import { createStubKotlinEmitter } from './ajsc-adapter.js';
|
|
4
|
+
const ok = (code, rootTypeName) => ({
|
|
5
|
+
code,
|
|
6
|
+
rootTypeName,
|
|
7
|
+
extractedTypeNames: [],
|
|
8
|
+
imports: ['kotlinx.serialization.Serializable'],
|
|
9
|
+
});
|
|
10
|
+
const noErrors = new Map();
|
|
11
|
+
describe('emitKotlinRoute', () => {
|
|
12
|
+
it('emits an api-kind route with path params and a response', () => {
|
|
13
|
+
const route = {
|
|
14
|
+
kind: 'api',
|
|
15
|
+
name: 'GetUser',
|
|
16
|
+
method: 'GET',
|
|
17
|
+
fullPath: '/users/:id',
|
|
18
|
+
schema: {
|
|
19
|
+
input: {
|
|
20
|
+
pathParams: { type: 'object' },
|
|
21
|
+
},
|
|
22
|
+
returnType: { type: 'object' },
|
|
23
|
+
},
|
|
24
|
+
errors: [],
|
|
25
|
+
};
|
|
26
|
+
const emitter = createStubKotlinEmitter({
|
|
27
|
+
PathParams: ok('@Serializable data class PathParams(val id: String)', 'PathParams'),
|
|
28
|
+
Response: ok('@Serializable data class Response(val id: String, val name: String)', 'Response'),
|
|
29
|
+
});
|
|
30
|
+
const result = emitKotlinRoute(route, emitter, noErrors);
|
|
31
|
+
expect(result.imports).toContain('kotlinx.serialization.Serializable');
|
|
32
|
+
expect(result.code).toContain('const val method = "GET"');
|
|
33
|
+
expect(result.code).toContain('const val pathTemplate = "/users/{id}"');
|
|
34
|
+
expect(result.code).toContain('fun path(p: PathParams): String = "/users/${p.id}"');
|
|
35
|
+
expect(result.code).toContain('@Serializable data class PathParams(val id: String)');
|
|
36
|
+
expect(result.code).toContain('@Serializable data class Response(val id: String, val name: String)');
|
|
37
|
+
});
|
|
38
|
+
it('emits a route with no path params using a path constant', () => {
|
|
39
|
+
const route = {
|
|
40
|
+
kind: 'api',
|
|
41
|
+
name: 'CreateUser',
|
|
42
|
+
method: 'POST',
|
|
43
|
+
fullPath: '/users',
|
|
44
|
+
schema: { input: { body: { type: 'object' } }, returnType: { type: 'object' } },
|
|
45
|
+
errors: [],
|
|
46
|
+
};
|
|
47
|
+
const emitter = createStubKotlinEmitter({
|
|
48
|
+
Body: ok('@Serializable data class Body(val name: String)', 'Body'),
|
|
49
|
+
Response: ok('@Serializable data class Response(val id: String)', 'Response'),
|
|
50
|
+
});
|
|
51
|
+
const result = emitKotlinRoute(route, emitter, noErrors);
|
|
52
|
+
expect(result.code).toContain('const val path = "/users"');
|
|
53
|
+
expect(result.code).not.toContain('fun path(');
|
|
54
|
+
});
|
|
55
|
+
it('emits an Errors namespace for routes whose error keys have schemas in the envelope', () => {
|
|
56
|
+
const route = {
|
|
57
|
+
kind: 'api',
|
|
58
|
+
name: 'GetUser',
|
|
59
|
+
method: 'GET',
|
|
60
|
+
fullPath: '/users/:id',
|
|
61
|
+
schema: { input: { pathParams: { type: 'object' } }, returnType: { type: 'object' } },
|
|
62
|
+
errors: ['NotFound'],
|
|
63
|
+
};
|
|
64
|
+
const emitter = createStubKotlinEmitter({
|
|
65
|
+
PathParams: ok('@Serializable data class PathParams(val id: String)', 'PathParams'),
|
|
66
|
+
Response: ok('@Serializable data class Response(val id: String)', 'Response'),
|
|
67
|
+
NotFound: ok('@Serializable data class NotFound(val name: String, val message: String)', 'NotFound'),
|
|
68
|
+
});
|
|
69
|
+
const errorSchemas = new Map([['NotFound', { type: 'object' }]]);
|
|
70
|
+
const result = emitKotlinRoute(route, emitter, errorSchemas);
|
|
71
|
+
expect(result.code).toContain('object Errors {');
|
|
72
|
+
expect(result.code).toContain('@Serializable data class NotFound(val name: String, val message: String)');
|
|
73
|
+
});
|
|
74
|
+
it('silently skips error keys with no schema in the envelope map', () => {
|
|
75
|
+
const route = {
|
|
76
|
+
kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users',
|
|
77
|
+
schema: {}, errors: ['UnknownTaxonomyKey'],
|
|
78
|
+
};
|
|
79
|
+
const result = emitKotlinRoute(route, createStubKotlinEmitter({}), new Map());
|
|
80
|
+
expect(result.code).not.toContain('object Errors {');
|
|
81
|
+
});
|
|
82
|
+
it('skips stream routes with a warning', () => {
|
|
83
|
+
const route = { kind: 'stream', name: 'WatchUsers', method: 'GET', path: '/users/stream', schema: {}, errors: [] };
|
|
84
|
+
const result = emitKotlinRoute(route, createStubKotlinEmitter({}), noErrors);
|
|
85
|
+
expect(result.code).toBe('');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
//# sourceMappingURL=emit-route-kotlin.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emit-route-kotlin.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/emit-route-kotlin.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ScopeGroup } from '../../group-routes.js';
|
|
2
|
+
import type { KotlinEmitter } from './ajsc-adapter.js';
|
|
3
|
+
export interface EmitScopeOptions {
|
|
4
|
+
kotlinPackage: string;
|
|
5
|
+
sourceHash: string;
|
|
6
|
+
}
|
|
7
|
+
export interface EmittedKotlinFile {
|
|
8
|
+
filename: string;
|
|
9
|
+
code: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function emitKotlinScope(group: ScopeGroup, opts: EmitScopeOptions, emitter: KotlinEmitter, errorSchemas: Map<string, unknown>): EmittedKotlinFile;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { emitKotlinRoute } from './emit-route-kotlin.js';
|
|
2
|
+
import { kotlinPackageDecl, kotlinSourceHashHeader, kotlinImports, indent } from './format-kotlin.js';
|
|
3
|
+
function pascalCase(scope) {
|
|
4
|
+
return scope
|
|
5
|
+
.split('-')
|
|
6
|
+
.filter((p) => p.length > 0)
|
|
7
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
8
|
+
.join('');
|
|
9
|
+
}
|
|
10
|
+
export function emitKotlinScope(group, opts, emitter, errorSchemas) {
|
|
11
|
+
const scopeName = pascalCase(group.scopeKey);
|
|
12
|
+
const allImports = [];
|
|
13
|
+
const routeBlocks = [];
|
|
14
|
+
for (const route of group.routes) {
|
|
15
|
+
const r = emitKotlinRoute(route, emitter, errorSchemas);
|
|
16
|
+
if (r.code === '')
|
|
17
|
+
continue;
|
|
18
|
+
allImports.push(...r.imports);
|
|
19
|
+
const wrapped = `object ${r.routeName} {\n${indent(r.code, 1)}\n}`;
|
|
20
|
+
routeBlocks.push(wrapped);
|
|
21
|
+
}
|
|
22
|
+
const innerScope = routeBlocks.length === 0 ? '' : indent(routeBlocks.join('\n\n'), 1);
|
|
23
|
+
const scopeBlock = innerScope === ''
|
|
24
|
+
? `object ${scopeName} {\n}`
|
|
25
|
+
: `object ${scopeName} {\n${innerScope}\n}`;
|
|
26
|
+
const importsBlock = kotlinImports(allImports);
|
|
27
|
+
const parts = [
|
|
28
|
+
kotlinPackageDecl(opts.kotlinPackage),
|
|
29
|
+
kotlinSourceHashHeader(opts.sourceHash),
|
|
30
|
+
importsBlock,
|
|
31
|
+
scopeBlock,
|
|
32
|
+
].filter((p) => p.length > 0);
|
|
33
|
+
return { filename: `${scopeName}.kt`, code: parts.join('\n\n') + '\n' };
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=emit-scope-kotlin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emit-scope-kotlin.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/emit-scope-kotlin.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAYrG,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK;SACT,KAAK,CAAC,GAAG,CAAC;SACV,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;SAC3B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;SAClD,IAAI,CAAC,EAAE,CAAC,CAAA;AACb,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,KAAiB,EACjB,IAAsB,EACtB,OAAsB,EACtB,YAAkC;IAElC,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IAC5C,MAAM,UAAU,GAAa,EAAE,CAAA;IAC/B,MAAM,WAAW,GAAa,EAAE,CAAA;IAEhC,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACjC,MAAM,CAAC,GAAG,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,CAAC,CAAA;QACvD,IAAI,CAAC,CAAC,IAAI,KAAK,EAAE;YAAE,SAAQ;QAC3B,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAA;QAC7B,MAAM,OAAO,GAAG,UAAU,CAAC,CAAC,SAAS,OAAO,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAA;QAClE,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC3B,CAAC;IAED,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAA;IACtF,MAAM,UAAU,GAAG,UAAU,KAAK,EAAE;QAClC,CAAC,CAAC,UAAU,SAAS,OAAO;QAC5B,CAAC,CAAC,UAAU,SAAS,OAAO,UAAU,KAAK,CAAA;IAE7C,MAAM,YAAY,GAAG,aAAa,CAAC,UAAU,CAAC,CAAA;IAC9C,MAAM,KAAK,GAAG;QACZ,iBAAiB,CAAC,IAAI,CAAC,aAAa,CAAC;QACrC,sBAAsB,CAAC,IAAI,CAAC,UAAU,CAAC;QACvC,YAAY;QACZ,UAAU;KACX,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAE7B,OAAO,EAAE,QAAQ,EAAE,GAAG,SAAS,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAA;AACzE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|