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
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
# Kotlin & Swift Codegen for ts-procedures
|
|
2
|
+
|
|
3
|
+
**Status:** Design
|
|
4
|
+
**Date:** 2026-04-24
|
|
5
|
+
**Author:** Cory Robinson
|
|
6
|
+
**Implementation order:** Kotlin first, Swift second.
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
`ts-procedures` already generates a TypeScript client from a `DocEnvelope` so web developers consume backend procedures with full type safety and zero schema duplication. The same problem exists for native mobile consumers: Android and iOS developers currently hand-write request/response types that mirror the server's schema, then re-edit those types every time the backend changes. This is the precise pain the TS codegen was built to eliminate — extending it to Kotlin and Swift removes the same drift for mobile.
|
|
11
|
+
|
|
12
|
+
## Goals
|
|
13
|
+
|
|
14
|
+
1. Generate **Kotlin** and **Swift** type definitions plus URL/method primitives directly from a ts-procedures `DocEnvelope`.
|
|
15
|
+
2. Be **non-invasive**: do not impose an HTTP stack, networking adapter, or runtime dependency on downstream mobile codebases.
|
|
16
|
+
3. Preserve dot-notation ergonomics consistent with the TS namespace mode (`Users.GetUser.Response`) so mobile devs can reference generated types directly in their own code.
|
|
17
|
+
4. Keep the codegen surface and maintenance burden **small** — add Kotlin/Swift without forcing a parallel runtime client to track every TS-side change.
|
|
18
|
+
|
|
19
|
+
## Non-goals (explicit deferrals)
|
|
20
|
+
|
|
21
|
+
- **No call functions, HTTP adapter, or reference adapter implementation.** Mobile devs own the HTTP layer entirely.
|
|
22
|
+
- **No streams (SSE).** Stream support is deferred; mobile devs needing streaming wire it themselves.
|
|
23
|
+
- **No hooks pipeline** (`onBeforeRequest` / `onAfterResponse` / `onError` / `onRequestError`).
|
|
24
|
+
- **No per-call options** (`signal`, `timeout`, `headers`, `basePath`, `meta`).
|
|
25
|
+
- **No error registry or typed error dispatch logic.** Error *types* are emitted; runtime dispatch is the consumer's job.
|
|
26
|
+
- **No base-URL handling.** Generated paths are relative; consumers prepend their environment base URL.
|
|
27
|
+
- **No Kotlin Multiplatform-specific output.** Initial Kotlin target is JVM/Android; KMP-friendliness comes from kotlinx.serialization but is not a tested target.
|
|
28
|
+
|
|
29
|
+
These are deliberate scope cuts, not architectural impossibilities — they may be revisited in a follow-up once the minimum-viable target is in production use.
|
|
30
|
+
|
|
31
|
+
## Architecture overview
|
|
32
|
+
|
|
33
|
+
Two packages collaborate:
|
|
34
|
+
|
|
35
|
+
- **`ajsc`** owns "JSON Schema → target-language types." Today it emits TypeScript; this design extends it with Kotlin and Swift emitters that share its existing schema walker, naming-convention logic (`enumStyle`, `depluralize`, `arrayItemNaming`, `uncountableWords`), and sub-type extraction.
|
|
36
|
+
- **`ts-procedures`** owns "DocEnvelope → per-scope output files." It resolves the envelope, groups routes by scope, calls into ajsc once per schema slot (path params / query / body / response / per-error type), and wraps the returned code in route-level `object` (Kotlin) or case-less `enum` (Swift) blocks alongside HTTP method constants and path builders.
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
DocEnvelope
|
|
40
|
+
│
|
|
41
|
+
├── ts-procedures: resolveEnvelope → groupRoutesByScope
|
|
42
|
+
│
|
|
43
|
+
└── per route, per schema slot:
|
|
44
|
+
│
|
|
45
|
+
├── ajsc.emitKotlin(schema, opts) → { code, rootTypeName, extractedTypeNames, imports }
|
|
46
|
+
│
|
|
47
|
+
└── ts-procedures wraps emitted code into:
|
|
48
|
+
object Users { object GetUser {
|
|
49
|
+
const val method = "GET"
|
|
50
|
+
const val pathTemplate = "/users/{id}"
|
|
51
|
+
fun path(p: PathParams): String = "/users/${p.id}"
|
|
52
|
+
// <ajsc-emitted PathParams, Response, Errors.* types here>
|
|
53
|
+
} }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
ts-procedures never does schema → type emission itself. ajsc never knows about routes, scopes, or DocEnvelopes.
|
|
57
|
+
|
|
58
|
+
## Generated output shape
|
|
59
|
+
|
|
60
|
+
### Kotlin
|
|
61
|
+
|
|
62
|
+
One file per scope (mirrors the TS codegen). Path templates emit as constants when there are no path params; as a function when there are.
|
|
63
|
+
|
|
64
|
+
```kotlin
|
|
65
|
+
// users.kt — generated by ts-procedures-codegen, do not edit
|
|
66
|
+
package com.example.api // configurable
|
|
67
|
+
|
|
68
|
+
import kotlinx.serialization.Serializable
|
|
69
|
+
import kotlinx.serialization.SerialName
|
|
70
|
+
|
|
71
|
+
object Users {
|
|
72
|
+
object GetUser {
|
|
73
|
+
const val method = "GET"
|
|
74
|
+
const val pathTemplate = "/users/{id}"
|
|
75
|
+
fun path(p: PathParams): String = "/users/${p.id}"
|
|
76
|
+
|
|
77
|
+
@Serializable data class PathParams(val id: String)
|
|
78
|
+
|
|
79
|
+
@Serializable data class Response(
|
|
80
|
+
val id: String,
|
|
81
|
+
val name: String,
|
|
82
|
+
val address: Address,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
// sub-type extracted from Response.address
|
|
86
|
+
@Serializable data class Address(val street: String, val city: String)
|
|
87
|
+
|
|
88
|
+
object Errors {
|
|
89
|
+
// `name` is a regular String field with a constructor default matching the
|
|
90
|
+
// server's wire-protocol value. kotlinx.serialization does not enforce
|
|
91
|
+
// literal-type discriminators here — consumers identify error variants by
|
|
92
|
+
// checking `body.name` themselves (this is a deliberate non-goal of the
|
|
93
|
+
// generated code, see "Non-goals" → no error registry).
|
|
94
|
+
@Serializable data class NotFound(
|
|
95
|
+
val name: String = "NotFound",
|
|
96
|
+
val message: String,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
object CreateUser {
|
|
102
|
+
const val method = "POST"
|
|
103
|
+
const val path = "/users" // no path params → const, not a function
|
|
104
|
+
|
|
105
|
+
@Serializable data class Body(val name: String, val email: String)
|
|
106
|
+
@Serializable data class Response(val id: String)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Swift
|
|
112
|
+
|
|
113
|
+
Same structure using case-less `enum` as a namespace idiom. One file per scope.
|
|
114
|
+
|
|
115
|
+
```swift
|
|
116
|
+
// Users.swift — generated by ts-procedures-codegen, do not edit
|
|
117
|
+
import Foundation
|
|
118
|
+
|
|
119
|
+
public enum Users {
|
|
120
|
+
public enum GetUser {
|
|
121
|
+
public static let method = "GET"
|
|
122
|
+
public static let pathTemplate = "/users/{id}"
|
|
123
|
+
public static func path(_ p: PathParams) -> String { "/users/\(p.id)" }
|
|
124
|
+
|
|
125
|
+
public struct PathParams: Codable {
|
|
126
|
+
public let id: String
|
|
127
|
+
public init(id: String) { self.id = id }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public struct Response: Codable {
|
|
131
|
+
public let id: String
|
|
132
|
+
public let name: String
|
|
133
|
+
public let address: Address
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public struct Address: Codable {
|
|
137
|
+
public let street: String
|
|
138
|
+
public let city: String
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public enum Errors {
|
|
142
|
+
public struct NotFound: Codable, Error {
|
|
143
|
+
public let name: String
|
|
144
|
+
public let message: String
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public enum CreateUser {
|
|
150
|
+
public static let method = "POST"
|
|
151
|
+
public static let path = "/users"
|
|
152
|
+
|
|
153
|
+
public struct Body: Codable {
|
|
154
|
+
public let name: String
|
|
155
|
+
public let email: String
|
|
156
|
+
}
|
|
157
|
+
public struct Response: Codable { public let id: String }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Consumer usage (illustrative, not generated)
|
|
163
|
+
|
|
164
|
+
```kotlin
|
|
165
|
+
// downstream Android repo
|
|
166
|
+
suspend fun loadUser(id: String): Users.GetUser.Response {
|
|
167
|
+
val path = Users.GetUser.path(Users.GetUser.PathParams(id))
|
|
168
|
+
val raw = myHttpClient.request(method = Users.GetUser.method, path = "$baseUrl$path")
|
|
169
|
+
return Json.decodeFromString(raw.body)
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```swift
|
|
174
|
+
// downstream iOS repo
|
|
175
|
+
func loadUser(id: String) async throws -> Users.GetUser.Response {
|
|
176
|
+
let path = Users.GetUser.path(.init(id: id))
|
|
177
|
+
let data = try await myHTTP.get(baseURL + path)
|
|
178
|
+
return try JSONDecoder().decode(Users.GetUser.Response.self, from: data)
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## ajsc contract
|
|
183
|
+
|
|
184
|
+
The single most important artifact for cross-team coordination. ajsc adds two emitter entry points; ts-procedures depends on them.
|
|
185
|
+
|
|
186
|
+
### API
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
// ajsc/kotlin
|
|
190
|
+
emitKotlin(schema: JSONSchema, opts: KotlinEmitOptions): EmitResult
|
|
191
|
+
|
|
192
|
+
// ajsc/swift
|
|
193
|
+
emitSwift(schema: JSONSchema, opts: SwiftEmitOptions): EmitResult
|
|
194
|
+
|
|
195
|
+
interface EmitResult {
|
|
196
|
+
/** Target-language source (no surrounding namespace; ts-procedures wraps). */
|
|
197
|
+
code: string
|
|
198
|
+
/** Name of the top-level type emitted, derived from opts.rootTypeName. */
|
|
199
|
+
rootTypeName: string
|
|
200
|
+
/** Names of all extracted sub-types (for caller reference resolution). */
|
|
201
|
+
extractedTypeNames: string[]
|
|
202
|
+
/** Required import lines for this output (e.g., kotlinx.serialization.*). */
|
|
203
|
+
imports: string[]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
interface KotlinEmitOptions {
|
|
207
|
+
/** Caller-supplied root type name (e.g., "Response", "PathParams"). */
|
|
208
|
+
rootTypeName: string
|
|
209
|
+
/** Default false. When false, nested objects extract to named siblings. */
|
|
210
|
+
inlineTypes?: boolean
|
|
211
|
+
/** Phase 1: 'kotlinx' only. Reserved: 'moshi', 'none'. */
|
|
212
|
+
serializer?: 'kotlinx'
|
|
213
|
+
// carried over from existing TS-side ajsc options:
|
|
214
|
+
enumStyle?: ...
|
|
215
|
+
depluralize?: boolean
|
|
216
|
+
arrayItemNaming?: ...
|
|
217
|
+
uncountableWords?: string[]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
interface SwiftEmitOptions {
|
|
221
|
+
rootTypeName: string
|
|
222
|
+
inlineTypes?: boolean
|
|
223
|
+
// Codable is the only target; no serializer flag.
|
|
224
|
+
enumStyle?: ...
|
|
225
|
+
depluralize?: boolean
|
|
226
|
+
arrayItemNaming?: ...
|
|
227
|
+
uncountableWords?: string[]
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Why the return shape matters
|
|
232
|
+
|
|
233
|
+
ts-procedures must reference emitted type names (e.g., the path-builder function takes `PathParams`; the wrapping `object GetUser` body lists each emitted name). Returning `rootTypeName` and `extractedTypeNames` explicitly avoids string-parsing ajsc's output and survives changes to ajsc's formatting.
|
|
234
|
+
|
|
235
|
+
### Type-mapping specification (Kotlin)
|
|
236
|
+
|
|
237
|
+
| JSON Schema | Kotlin | Notes |
|
|
238
|
+
|---|---|---|
|
|
239
|
+
| `string` | `String` | |
|
|
240
|
+
| `string` + `format: "date-time"` | `String` (phase 1) | `kotlinx.datetime.Instant` reserved as opt-in. Default `String` to avoid forcing the `kotlinx-datetime` dependency on consumers. |
|
|
241
|
+
| `string` + `format: "uuid"` | `String` | No widely-adopted UUID type in kotlinx. |
|
|
242
|
+
| `integer` | `Long` | Safer default for backend IDs. `Int` not chosen because JSON Schema `integer` carries no size info. |
|
|
243
|
+
| `number` | `Double` | |
|
|
244
|
+
| `boolean` | `Boolean` | |
|
|
245
|
+
| `array` | `List<T>` | |
|
|
246
|
+
| `object` | `@Serializable data class` | |
|
|
247
|
+
| `enum` (string) | `enum class` with `@SerialName(...)` per variant | |
|
|
248
|
+
| `oneOf` with discriminator | `@Serializable sealed class` with `@JsonClassDiscriminator(...)` | |
|
|
249
|
+
| `oneOf` without discriminator | Phase 1: emit warning + `@Polymorphic Any`. Reconsider in phase 2. | |
|
|
250
|
+
| `type: ["X", "null"]` / `nullable: true` | `T?` | |
|
|
251
|
+
| Required vs optional property | Required → non-null; optional → nullable with `= null` default | |
|
|
252
|
+
|
|
253
|
+
### Type-mapping specification (Swift)
|
|
254
|
+
|
|
255
|
+
| JSON Schema | Swift | Notes |
|
|
256
|
+
|---|---|---|
|
|
257
|
+
| `string` | `String` | |
|
|
258
|
+
| `string` + `format: "date-time"` | `String` (phase 1) | `Date` opt-in via flag once `Codable` strategies are settled. |
|
|
259
|
+
| `string` + `format: "uuid"` | `String` | `UUID` opt-in. |
|
|
260
|
+
| `integer` | `Int64` | Mirrors Kotlin `Long` choice. |
|
|
261
|
+
| `number` | `Double` | |
|
|
262
|
+
| `boolean` | `Bool` | |
|
|
263
|
+
| `array` | `[T]` | |
|
|
264
|
+
| `object` | `struct ... : Codable` | |
|
|
265
|
+
| `enum` (string) | `enum: String, Codable` | |
|
|
266
|
+
| `oneOf` with discriminator | `enum` with associated values + custom `Codable` impl | |
|
|
267
|
+
| `oneOf` without discriminator | Phase 1: warning + emit unsupported marker. | |
|
|
268
|
+
| `type: ["X", "null"]` / `nullable: true` | `T?` | |
|
|
269
|
+
| Required vs optional property | Required → non-optional; optional → `T?` | |
|
|
270
|
+
|
|
271
|
+
### Naming rules
|
|
272
|
+
|
|
273
|
+
- Root type: caller passes via `opts.rootTypeName`. ajsc never infers from `schema.title`.
|
|
274
|
+
- Sub-types: derived from the parent property name via existing `depluralize`/`arrayItemNaming` logic. Collision suffixes appended deterministically.
|
|
275
|
+
- Output must be deterministic across runs so the source-hash staleness check continues to work.
|
|
276
|
+
|
|
277
|
+
## ts-procedures-side scope
|
|
278
|
+
|
|
279
|
+
### Pipeline extension
|
|
280
|
+
|
|
281
|
+
Extend `src/codegen/pipeline.ts` to dispatch on a target argument:
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
DocEnvelope
|
|
285
|
+
→ resolveEnvelope (shared)
|
|
286
|
+
→ groupRoutesByScope (shared)
|
|
287
|
+
→ for each target in {ts, kotlin, swift}:
|
|
288
|
+
→ emitScopeFile<target> per scope
|
|
289
|
+
→ emitErrorsFile (TS only — Kotlin/Swift errors live inline per-route, no registry)
|
|
290
|
+
→ emitIndexFile (TS only — Kotlin/Swift have no factory)
|
|
291
|
+
→ write
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Directory layout
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
src/codegen/
|
|
298
|
+
emit-types.ts # existing TS emitter (keep)
|
|
299
|
+
emit-scope.ts # existing TS scope emitter (keep)
|
|
300
|
+
...
|
|
301
|
+
targets/
|
|
302
|
+
kotlin/
|
|
303
|
+
emit-scope-kotlin.ts # wraps ajsc.emitKotlin into `object Scope { object Route { ... } }`
|
|
304
|
+
emit-scope-kotlin.test.ts
|
|
305
|
+
swift/
|
|
306
|
+
emit-scope-swift.ts
|
|
307
|
+
emit-scope-swift.test.ts
|
|
308
|
+
pipeline.ts # target-aware dispatcher
|
|
309
|
+
bin/cli.ts # adds --target flag
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The `targets/` directory is structured so a future split into sibling packages (`@ts-procedures/codegen-kotlin`, `@ts-procedures/codegen-swift`) is mechanical: each subdirectory is internally self-contained and depends only on shared core utilities.
|
|
313
|
+
|
|
314
|
+
### CLI
|
|
315
|
+
|
|
316
|
+
Single binary, multi-target via `--target`:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
npx ts-procedures-codegen --target kotlin --url https://api.example.com/_ts-procedures.json --out ./generated-kotlin
|
|
320
|
+
npx ts-procedures-codegen --target swift --file ./envelope.json --out ./generated-swift
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Default `--target ts` preserves backward compatibility — every existing invocation continues to work unchanged.
|
|
324
|
+
|
|
325
|
+
Target-specific flags:
|
|
326
|
+
|
|
327
|
+
- Kotlin: `--kotlin-package <com.example.api>` — sets the `package` declaration on every emitted file. Required, **unless supplied via the config file's `kotlin.package`**. CLI flag and config value are interchangeable; CLI overrides config when both are present. The codegen rejects invocations where neither path provides a package.
|
|
328
|
+
- Swift: `--swift-module-name <name>` — currently informational (Swift does not have file-level module declarations); may drive output directory naming.
|
|
329
|
+
|
|
330
|
+
Flags inherited from existing CLI: `--watch`, `--dry-run`, `--clean-out-dir`, `--config`, `--enum-style`, `--depluralize`, `--array-item-naming`, `--uncountable-words`.
|
|
331
|
+
|
|
332
|
+
`--service-name` (existing TS flag) is **not applicable** to Kotlin/Swift targets — those targets emit no service-level namespace, factory, or aggregate index. The flag is silently ignored when `--target` is `kotlin` or `swift`. (The Kotlin scope namespace is named after the route's scope, not the service.)
|
|
333
|
+
|
|
334
|
+
Flags **not applicable** to Kotlin/Swift targets (silently ignored or rejected): `--self-contained`, `--namespace-types` (always on for Kotlin/Swift; flat mode unsupported), `--client-import-path`, `--jsdoc`, `--service-name`.
|
|
335
|
+
|
|
336
|
+
Config file: existing `ts-procedures-codegen.config.json` extended with optional `kotlin` and `swift` sub-objects:
|
|
337
|
+
|
|
338
|
+
```json
|
|
339
|
+
{
|
|
340
|
+
"target": "kotlin",
|
|
341
|
+
"url": "https://api.example.com/_ts-procedures.json",
|
|
342
|
+
"out": "./generated",
|
|
343
|
+
"kotlin": { "package": "com.example.api" }
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Source-hash staleness
|
|
348
|
+
|
|
349
|
+
The existing source-hash-as-comment mechanism extends naturally — Kotlin and Swift both support line comments (`// ...`). ts-procedures injects `// source-hash: <hash>` at the top of each generated file; rerunning with an unchanged envelope produces a no-op diff.
|
|
350
|
+
|
|
351
|
+
## Testing strategy
|
|
352
|
+
|
|
353
|
+
### ajsc side
|
|
354
|
+
|
|
355
|
+
- Acceptance corpus: representative JSON Schemas + golden Kotlin and Swift outputs (`fixtures/<name>.kotlin.txt`, `fixtures/<name>.swift.txt`). Tests assert byte-identical output.
|
|
356
|
+
- Corpus seeded from ts-procedures' existing codegen test envelopes plus pathological cases: deeply nested objects, discriminated unions, self-referencing `$ref`, large enums, every nullable combination, arrays of arrays.
|
|
357
|
+
|
|
358
|
+
### ts-procedures side
|
|
359
|
+
|
|
360
|
+
- Unit tests for `emit-scope-kotlin.ts` and `emit-scope-swift.ts`: given a fake `groupedRoutes` + a stubbed ajsc, assert the wrapping output (object/enum nesting, method constants, path templates, path-builder shapes).
|
|
361
|
+
- Integration test: feed a known DocEnvelope through the full pipeline with a real ajsc dependency; assert generated file contents against golden files.
|
|
362
|
+
- E2E test (Kotlin): generate output, place in a small Gradle project, compile with `kotlinc`. Asserts output is syntactically valid and imports resolve.
|
|
363
|
+
- E2E test (Swift): generate output, place in a small SwiftPM package, compile with `swift build`. Same syntactic-validity guarantee.
|
|
364
|
+
|
|
365
|
+
The compile-check E2E tests are essential because Kotlin/Swift are statically typed and the only way to catch "ajsc emitted a name that ts-procedures referenced incorrectly" is to compile.
|
|
366
|
+
|
|
367
|
+
**Toolchain availability.** `kotlinc` and `swift build` are not currently part of this repo's CI environment. Plan options: (a) provision both toolchains in CI (Kotlin via the `kotlin/` GitHub Action, Swift via macOS runners or `swift-actions/setup-swift`); (b) gate the compile-check E2E tests behind an opt-in flag and run them locally / in a separate scheduled CI job. Decision deferred to the implementation plan; either path is acceptable, but at minimum a manual local run must be performed before each Kotlin/Swift release.
|
|
368
|
+
|
|
369
|
+
## Sequencing
|
|
370
|
+
|
|
371
|
+
| Phase | Owner | Deliverable |
|
|
372
|
+
|---|---|---|
|
|
373
|
+
| **A** | ajsc | `emitKotlin` exposed, type-mapping spec implemented, fixture corpus with golden outputs, semver minor bump. |
|
|
374
|
+
| **B** | ts-procedures | `--target kotlin` end-to-end: scope wrapper, path builders, method constants, CLI flag, config-file integration, kotlinc E2E test. |
|
|
375
|
+
| **C** | ajsc | `emitSwift` exposed, Swift fixture corpus. |
|
|
376
|
+
| **D** | ts-procedures | `--target swift` end-to-end: scope wrapper, swift build E2E test. |
|
|
377
|
+
|
|
378
|
+
Phases A→B and C→D are sequential within each target. Kotlin (A+B) ships fully before Swift work begins to avoid co-designing two emitters before either has shipped to a real consumer.
|
|
379
|
+
|
|
380
|
+
### Coordination
|
|
381
|
+
|
|
382
|
+
- ajsc ships a `0.x` minor with each emitter; ts-procedures pins the minor range.
|
|
383
|
+
- Sample fixtures live in ajsc; ts-procedures duplicates a small subset for its own integration tests so the two repos cannot drift silently.
|
|
384
|
+
|
|
385
|
+
## Open questions / explicit deferrals
|
|
386
|
+
|
|
387
|
+
1. **Cross-route shared types** (e.g., a `User` type referenced by multiple routes via `$ref`). Phase 1 emits per-route copies, matching current TS behavior. Cross-route dedup (`Users.Shared.User` or top-level `Shared.User`) is a candidate refinement once consumer pain is known.
|
|
388
|
+
2. **`format: "date-time"` → `Instant` / `Date` upgrade.** Default to `String` in phase 1 to avoid forcing kotlinx-datetime / Date-strategy decisions on consumers; promote to typed when consumer demand is concrete.
|
|
389
|
+
3. **`oneOf` without discriminator.** Phase 1 emits a warning and a fallback; not all backends produce well-discriminated unions.
|
|
390
|
+
4. **Per-call options.** Deferred entirely — no `signal`, `headers`, `timeout`, `meta`. If consumers ask, the additive surface (an optional second parameter to the path builder, or an opaque `RequestOptions` struct) is small enough to add later without breaking existing output.
|
|
391
|
+
5. **Sub-type extraction depth.** `inlineTypes: false` is the default; whether to expose a CLI flag to override is deferred until someone asks.
|
|
392
|
+
|
|
393
|
+
## Risk assessment
|
|
394
|
+
|
|
395
|
+
- **Risk: ajsc emitter output and ts-procedures wrapping drift.** Mitigated by the explicit `EmitResult.rootTypeName` + `extractedTypeNames` contract — no string-parsing of ajsc output — and by the kotlinc/swift-build E2E compile checks.
|
|
396
|
+
- **Risk: scope creep into a full mobile client (adapter, errors registry, hooks).** Mitigated by the explicit non-goals list above. Future work goes through a new spec, not a quiet expansion of this one.
|
|
397
|
+
- **Risk: kotlinx.serialization adoption resistance.** A `serializer: 'none'` mode is **reserved for a future phase** (not part of phase 1) — punting type annotations entirely is a one-flag escape if a consumer team's ecosystem cannot accept the kotlinx Gradle plugin. Phase 1 ships kotlinx-only; broadening is additive.
|
|
398
|
+
|
|
399
|
+
## Summary
|
|
400
|
+
|
|
401
|
+
Add two new emitters to `ajsc` (Kotlin then Swift) and one new `--target` mode to ts-procedures codegen. Generated output is **types + URL/method primitives only** — no runtime, no adapter, no registry. Mobile consumers receive a typed, structured catalog of the server's contract and integrate it into whatever HTTP stack they already use. The non-invasive surface keeps maintenance burden low and lets Android/iOS teams keep their existing networking architecture intact.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-procedures",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.2",
|
|
4
4
|
"description": "A TypeScript RPC framework that creates type-safe, schema-validated procedure calls with a single function definition. Define your procedures once and get full type inference, runtime validation, and framework integration hooks.",
|
|
5
5
|
"main": "build/exports.js",
|
|
6
6
|
"types": "build/exports.d.ts",
|
|
@@ -234,6 +234,8 @@ const docs = builder.docs
|
|
|
234
234
|
|
|
235
235
|
All four builders' `build()` methods are synchronous — they return the framework app instance directly. Don't `await` the call.
|
|
236
236
|
|
|
237
|
+
**Single Hono server across builders:** all three Hono builders (`HonoRPCAppBuilder`, `HonoAPIAppBuilder`, `HonoStreamAppBuilder`) accept an optional `app?: Hono` in their config. Pass the same `new Hono()` instance to mount RPC, API, and Stream routes onto one server — the builders are registration scopes, not separate servers. See **[docs/http-integrations.md § One Hono Server, Multiple Builders](../../../docs/http-integrations.md#one-hono-server-multiple-builders)**.
|
|
238
|
+
|
|
237
239
|
**Key methods:**
|
|
238
240
|
|
|
239
241
|
| Method | Returns | Description |
|
|
@@ -257,8 +259,7 @@ import { DocRegistry } from 'ts-procedures/http-docs'
|
|
|
257
259
|
|
|
258
260
|
const docs = new DocRegistry({
|
|
259
261
|
basePath: '/api',
|
|
260
|
-
|
|
261
|
-
errors: DocRegistry.defaultErrors(),
|
|
262
|
+
errors: appErrors, // your ErrorTaxonomy — framework defaults auto-merged and deduped
|
|
262
263
|
})
|
|
263
264
|
.from(rpcBuilder)
|
|
264
265
|
.from(apiBuilder)
|
|
@@ -269,7 +270,7 @@ app.get('/docs', (c) => c.json(docs.toJSON()))
|
|
|
269
270
|
|
|
270
271
|
- `from()` stores a reference — routes are read lazily at `toJSON()` time
|
|
271
272
|
- `toJSON()` supports optional `filter` and `transform` options
|
|
272
|
-
- `
|
|
273
|
+
- `errors` accepts an `ErrorTaxonomy` or raw `ErrorDoc[]`; framework defaults are auto-merged (opt out via `includeDefaults: false`)
|
|
273
274
|
- All builders satisfy the `DocSource` interface (`{ readonly docs: AnyHttpRouteDoc[] }`)
|
|
274
275
|
|
|
275
276
|
### Client Code Generation
|
|
@@ -3,6 +3,7 @@ import { v } from 'suretype'
|
|
|
3
3
|
import { Procedures } from '../../index.js'
|
|
4
4
|
import { HonoRPCAppBuilder } from './hono-rpc/index.js'
|
|
5
5
|
import { DocRegistry } from './doc-registry.js'
|
|
6
|
+
import { defineErrorTaxonomy } from './error-taxonomy.js'
|
|
6
7
|
import type {
|
|
7
8
|
AnyHttpRouteDoc,
|
|
8
9
|
RPCHttpRouteDoc,
|
|
@@ -11,6 +12,7 @@ import type {
|
|
|
11
12
|
StreamHttpRouteDoc,
|
|
12
13
|
DocSource,
|
|
13
14
|
DocEnvelope,
|
|
15
|
+
ErrorDoc,
|
|
14
16
|
} from '../types.js'
|
|
15
17
|
|
|
16
18
|
// ---------------------------------------------------------------------------
|
|
@@ -61,7 +63,7 @@ describe('DocRegistry', () => {
|
|
|
61
63
|
// --------------------------------------------------------------------------
|
|
62
64
|
describe('constructor', () => {
|
|
63
65
|
test('uses defaults when no config provided', () => {
|
|
64
|
-
const registry = new DocRegistry()
|
|
66
|
+
const registry = new DocRegistry({ includeDefaults: false })
|
|
65
67
|
const out = registry.toJSON()
|
|
66
68
|
expect(out.basePath).toBe('')
|
|
67
69
|
expect(out.headers).toEqual([])
|
|
@@ -69,7 +71,7 @@ describe('DocRegistry', () => {
|
|
|
69
71
|
})
|
|
70
72
|
|
|
71
73
|
test('accepts partial config', () => {
|
|
72
|
-
const registry = new DocRegistry({ basePath: '/v1' })
|
|
74
|
+
const registry = new DocRegistry({ basePath: '/v1', includeDefaults: false })
|
|
73
75
|
const out = registry.toJSON()
|
|
74
76
|
expect(out.basePath).toBe('/v1')
|
|
75
77
|
expect(out.headers).toEqual([])
|
|
@@ -79,7 +81,7 @@ describe('DocRegistry', () => {
|
|
|
79
81
|
test('accepts full config', () => {
|
|
80
82
|
const headers = [{ name: 'Authorization', description: 'Bearer token', required: true }]
|
|
81
83
|
const errors = [{ name: 'Unauthorized', statusCode: 401, description: 'Missing token' }]
|
|
82
|
-
const registry = new DocRegistry({ basePath: '/api', headers, errors })
|
|
84
|
+
const registry = new DocRegistry({ basePath: '/api', headers, errors, includeDefaults: false })
|
|
83
85
|
const out = registry.toJSON()
|
|
84
86
|
expect(out.basePath).toBe('/api')
|
|
85
87
|
expect(out.headers).toEqual(headers)
|
|
@@ -175,7 +177,7 @@ describe('DocRegistry', () => {
|
|
|
175
177
|
test('headers and errors are copies', () => {
|
|
176
178
|
const headers = [{ name: 'X-Custom' }]
|
|
177
179
|
const errors = [{ name: 'E', statusCode: 500, description: 'd' }]
|
|
178
|
-
const registry = new DocRegistry({ headers, errors })
|
|
180
|
+
const registry = new DocRegistry({ headers, errors, includeDefaults: false })
|
|
179
181
|
const out = registry.toJSON()
|
|
180
182
|
expect(out.headers).toEqual(headers)
|
|
181
183
|
expect(out.headers).not.toBe(headers)
|
|
@@ -298,6 +300,153 @@ describe('DocRegistry', () => {
|
|
|
298
300
|
})
|
|
299
301
|
})
|
|
300
302
|
|
|
303
|
+
// --------------------------------------------------------------------------
|
|
304
|
+
// taxonomy input + auto-defaults + dedupe (v6.0.1 simplification)
|
|
305
|
+
// --------------------------------------------------------------------------
|
|
306
|
+
describe('errors: ErrorTaxonomy (polymorphic constructor input)', () => {
|
|
307
|
+
test('accepts an ErrorTaxonomy directly and converts to ErrorDoc[]', () => {
|
|
308
|
+
const taxonomy = defineErrorTaxonomy({
|
|
309
|
+
AuthError: {
|
|
310
|
+
class: class AuthError extends Error {},
|
|
311
|
+
statusCode: 401,
|
|
312
|
+
description: 'unauthenticated',
|
|
313
|
+
},
|
|
314
|
+
})
|
|
315
|
+
const envelope = new DocRegistry({ errors: taxonomy }).toJSON()
|
|
316
|
+
const auth = envelope.errors.find((e) => e.name === 'AuthError')
|
|
317
|
+
expect(auth).toBeDefined()
|
|
318
|
+
expect(auth?.statusCode).toBe(401)
|
|
319
|
+
expect(auth?.description).toBe('unauthenticated')
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
test('auto-includes framework defaults when errors is a taxonomy', () => {
|
|
323
|
+
const taxonomy = defineErrorTaxonomy({
|
|
324
|
+
AuthError: { class: class AuthError extends Error {}, statusCode: 401 },
|
|
325
|
+
})
|
|
326
|
+
const names = new DocRegistry({ errors: taxonomy }).toJSON().errors.map((e) => e.name)
|
|
327
|
+
expect(names).toContain('AuthError')
|
|
328
|
+
expect(names).toContain('ProcedureValidationError')
|
|
329
|
+
expect(names).toContain('ProcedureYieldValidationError')
|
|
330
|
+
expect(names).toContain('ProcedureError')
|
|
331
|
+
expect(names).toContain('ProcedureRegistrationError')
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
test('auto-includes framework defaults when errors is ErrorDoc[]', () => {
|
|
335
|
+
const custom: ErrorDoc = { name: 'CustomThing', statusCode: 418, description: 'teapot' }
|
|
336
|
+
const names = new DocRegistry({ errors: [custom] }).toJSON().errors.map((e) => e.name)
|
|
337
|
+
expect(names).toContain('CustomThing')
|
|
338
|
+
expect(names).toContain('ProcedureValidationError')
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
test('includeDefaults: false omits framework defaults', () => {
|
|
342
|
+
const taxonomy = defineErrorTaxonomy({
|
|
343
|
+
AuthError: { class: class AuthError extends Error {}, statusCode: 401 },
|
|
344
|
+
})
|
|
345
|
+
const names = new DocRegistry({ errors: taxonomy, includeDefaults: false })
|
|
346
|
+
.toJSON()
|
|
347
|
+
.errors.map((e) => e.name)
|
|
348
|
+
expect(names).toEqual(['AuthError'])
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test('user taxonomy entry with same name as default overrides default (dedupe, user wins)', () => {
|
|
352
|
+
const taxonomy = defineErrorTaxonomy({
|
|
353
|
+
ProcedureError: {
|
|
354
|
+
class: Error,
|
|
355
|
+
statusCode: 418,
|
|
356
|
+
description: 'custom override',
|
|
357
|
+
},
|
|
358
|
+
})
|
|
359
|
+
const envelope = new DocRegistry({ errors: taxonomy }).toJSON()
|
|
360
|
+
const proc = envelope.errors.filter((e) => e.name === 'ProcedureError')
|
|
361
|
+
expect(proc).toHaveLength(1)
|
|
362
|
+
expect(proc[0]!.statusCode).toBe(418)
|
|
363
|
+
expect(proc[0]!.description).toBe('custom override')
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
test('user ErrorDoc with same name as default overrides default (dedupe, user wins)', () => {
|
|
367
|
+
const custom: ErrorDoc = {
|
|
368
|
+
name: 'ProcedureError',
|
|
369
|
+
statusCode: 418,
|
|
370
|
+
description: 'custom override',
|
|
371
|
+
}
|
|
372
|
+
const envelope = new DocRegistry({ errors: [custom] }).toJSON()
|
|
373
|
+
const proc = envelope.errors.filter((e) => e.name === 'ProcedureError')
|
|
374
|
+
expect(proc).toHaveLength(1)
|
|
375
|
+
expect(proc[0]!.statusCode).toBe(418)
|
|
376
|
+
expect(proc[0]!.description).toBe('custom override')
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
test('empty errors config still returns framework defaults', () => {
|
|
380
|
+
const names = new DocRegistry().toJSON().errors.map((e) => e.name)
|
|
381
|
+
expect(names).toContain('ProcedureError')
|
|
382
|
+
expect(names).toContain('ProcedureValidationError')
|
|
383
|
+
expect(names).toContain('ProcedureYieldValidationError')
|
|
384
|
+
expect(names).toContain('ProcedureRegistrationError')
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
test('includeDefaults: false with no errors produces empty error list', () => {
|
|
388
|
+
const envelope = new DocRegistry({ includeDefaults: false }).toJSON()
|
|
389
|
+
expect(envelope.errors).toEqual([])
|
|
390
|
+
})
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
// --------------------------------------------------------------------------
|
|
394
|
+
// .documentError() fluent extension
|
|
395
|
+
// --------------------------------------------------------------------------
|
|
396
|
+
describe('.documentError()', () => {
|
|
397
|
+
test('adds a single ErrorDoc to the envelope', () => {
|
|
398
|
+
const registry = new DocRegistry({ includeDefaults: false }).documentError({
|
|
399
|
+
name: 'RateLimitExceeded',
|
|
400
|
+
statusCode: 429,
|
|
401
|
+
description: 'too many requests',
|
|
402
|
+
})
|
|
403
|
+
const names = registry.toJSON().errors.map((e) => e.name)
|
|
404
|
+
expect(names).toEqual(['RateLimitExceeded'])
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
test('accepts multiple docs via variadic args', () => {
|
|
408
|
+
const registry = new DocRegistry({ includeDefaults: false }).documentError(
|
|
409
|
+
{ name: 'A', statusCode: 400, description: 'a' },
|
|
410
|
+
{ name: 'B', statusCode: 500, description: 'b' }
|
|
411
|
+
)
|
|
412
|
+
const names = registry.toJSON().errors.map((e) => e.name)
|
|
413
|
+
expect(names).toEqual(['A', 'B'])
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
test('returns this for chaining', () => {
|
|
417
|
+
const registry = new DocRegistry()
|
|
418
|
+
const returned = registry.documentError({
|
|
419
|
+
name: 'Foo',
|
|
420
|
+
statusCode: 500,
|
|
421
|
+
description: 'x',
|
|
422
|
+
})
|
|
423
|
+
expect(returned).toBe(registry)
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
test('composes with taxonomy input — extends docs without replacing', () => {
|
|
427
|
+
const taxonomy = defineErrorTaxonomy({
|
|
428
|
+
AuthError: { class: class AuthError extends Error {}, statusCode: 401 },
|
|
429
|
+
})
|
|
430
|
+
const envelope = new DocRegistry({ errors: taxonomy })
|
|
431
|
+
.documentError({ name: 'RateLimitExceeded', statusCode: 429, description: 'x' })
|
|
432
|
+
.toJSON()
|
|
433
|
+
const names = envelope.errors.map((e) => e.name)
|
|
434
|
+
expect(names).toContain('AuthError')
|
|
435
|
+
expect(names).toContain('RateLimitExceeded')
|
|
436
|
+
expect(names).toContain('ProcedureValidationError')
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
test('dedupes against existing errors (last write wins)', () => {
|
|
440
|
+
const registry = new DocRegistry({ includeDefaults: false })
|
|
441
|
+
.documentError({ name: 'Foo', statusCode: 400, description: 'first' })
|
|
442
|
+
.documentError({ name: 'Foo', statusCode: 500, description: 'second' })
|
|
443
|
+
const foo = registry.toJSON().errors.filter((e) => e.name === 'Foo')
|
|
444
|
+
expect(foo).toHaveLength(1)
|
|
445
|
+
expect(foo[0]!.statusCode).toBe(500)
|
|
446
|
+
expect(foo[0]!.description).toBe('second')
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
|
|
301
450
|
// --------------------------------------------------------------------------
|
|
302
451
|
// kind discriminant
|
|
303
452
|
// --------------------------------------------------------------------------
|
|
@@ -333,7 +482,6 @@ describe('DocRegistry', () => {
|
|
|
333
482
|
const registry = new DocRegistry({
|
|
334
483
|
basePath: '/api',
|
|
335
484
|
headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
|
|
336
|
-
errors: DocRegistry.defaultErrors(),
|
|
337
485
|
})
|
|
338
486
|
.from(makeSource([rpcDoc]))
|
|
339
487
|
.from(makeSource([apiDoc]))
|
|
@@ -347,10 +495,7 @@ describe('DocRegistry', () => {
|
|
|
347
495
|
})
|
|
348
496
|
|
|
349
497
|
test('filter + transform combined', () => {
|
|
350
|
-
const registry = new DocRegistry({
|
|
351
|
-
basePath: '/api',
|
|
352
|
-
errors: DocRegistry.defaultErrors(),
|
|
353
|
-
})
|
|
498
|
+
const registry = new DocRegistry({ basePath: '/api' })
|
|
354
499
|
.from(makeSource([rpcDoc]))
|
|
355
500
|
.from(makeSource([apiDoc]))
|
|
356
501
|
.from(makeSource([streamDoc]))
|
|
@@ -372,7 +517,6 @@ describe('DocRegistry', () => {
|
|
|
372
517
|
const registry = new DocRegistry({
|
|
373
518
|
basePath: '/api',
|
|
374
519
|
headers: [{ name: 'X-Request-Id', example: 'abc-123' }],
|
|
375
|
-
errors: DocRegistry.defaultErrors(),
|
|
376
520
|
})
|
|
377
521
|
.from(makeSource([rpcDoc]))
|
|
378
522
|
.from(makeSource([apiDoc]))
|