@adhd/apigen-core-client 0.1.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/CHANGELOG.md +31 -0
- package/README.md +238 -0
- package/index.d.ts +14 -0
- package/index.js +1 -0
- package/index.mjs +1618 -0
- package/lib/apigen-core.d.ts +1 -0
- package/lib/compose-schemas.d.ts +31 -0
- package/lib/descriptor.d.ts +220 -0
- package/lib/extract-classes.d.ts +37 -0
- package/lib/extract.d.ts +43 -0
- package/lib/extraction-session.d.ts +97 -0
- package/lib/get-safety.d.ts +14 -0
- package/lib/param-defaults.d.ts +36 -0
- package/lib/plugin.d.ts +446 -0
- package/lib/schema-builders/map-set-tuple.d.ts +19 -0
- package/lib/schema-builders/morph-fallback.d.ts +13 -0
- package/lib/schema-builders/morph-walk.d.ts +59 -0
- package/lib/schema-builders/nominal.d.ts +95 -0
- package/lib/schema-builders/ts-json-schema.d.ts +65 -0
- package/lib/schema-builders/union.d.ts +74 -0
- package/lib/source-language.d.ts +81 -0
- package/lib/types.d.ts +87 -0
- package/package.json +17 -0
package/lib/plugin.d.ts
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { PluginLanguage } from './types';
|
|
2
|
+
import { Operation, JSONSchema } from './descriptor';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The four canonical transport carriers apigen knows about (SPEC §5/§7/§9.1).
|
|
6
|
+
*
|
|
7
|
+
* A `MountedOperation` may opt in to a subset — `transports` omitted → all.
|
|
8
|
+
*/
|
|
9
|
+
export type Transport = 'http' | 'grpc' | 'mcp' | 'cli';
|
|
10
|
+
/**
|
|
11
|
+
* A type-keyed, mutable-during-compose map threaded through every Layer and
|
|
12
|
+
* into the dispatch function as `ctx` (SPEC §8.1).
|
|
13
|
+
*
|
|
14
|
+
* Layers insert typed values with a class or symbol key and downstream layers /
|
|
15
|
+
* the function implementation read them back — the same mental model as
|
|
16
|
+
* `http::Extensions` (Rust) and `AsyncLocalStorage` (Node).
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* // insert (in a Layer):
|
|
21
|
+
* call.ctx.set(Logger, new Logger({ level: 'info' }))
|
|
22
|
+
* // read (in a Layer or dispatch):
|
|
23
|
+
* const log = call.ctx.get(Logger)
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export interface Extensions {
|
|
27
|
+
/**
|
|
28
|
+
* Retrieve the value stored under the given class constructor or symbol key.
|
|
29
|
+
* Returns `undefined` when the key has not been set.
|
|
30
|
+
*/
|
|
31
|
+
get<T>(key: abstract new (...args: never[]) => T): T | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Store a value under the given class constructor or symbol key.
|
|
34
|
+
* Overwrites any existing value for that key.
|
|
35
|
+
*/
|
|
36
|
+
set<T>(key: abstract new (...args: never[]) => T, value: T): void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The inbound call descriptor passed to every layer and ultimately to dispatch
|
|
40
|
+
* (SPEC §7.1 / §8.1).
|
|
41
|
+
*
|
|
42
|
+
* `data` contains the bare domain params (envelope dissolved); `envelope`
|
|
43
|
+
* contains transport-native side-channel fields (session, auth tokens, …).
|
|
44
|
+
* `raw` is an escape hatch for transport-specific adapters that need access to
|
|
45
|
+
* the native request object — ordinary plugins should never need it.
|
|
46
|
+
*/
|
|
47
|
+
export interface Call {
|
|
48
|
+
/** The operation being invoked (from the merged canonical descriptor). */
|
|
49
|
+
operation: Operation;
|
|
50
|
+
/** Bare domain params (the `data`-wrapper is dissolved; ctx excluded). */
|
|
51
|
+
data: Record<string, unknown>;
|
|
52
|
+
/**
|
|
53
|
+
* Side-channel metadata from the transport-native carrier, keyed as
|
|
54
|
+
* `x-<pluginId>-<field>` (SPEC §9 / §9.1).
|
|
55
|
+
*/
|
|
56
|
+
envelope: Record<string, unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* Typed request-extensions map — threaded `mw → mw → fn` (SPEC §8.1).
|
|
59
|
+
* Layers insert context values; downstream layers and the domain function
|
|
60
|
+
* read them back.
|
|
61
|
+
*/
|
|
62
|
+
ctx: Extensions;
|
|
63
|
+
/** Which transport delivered this call. */
|
|
64
|
+
transport: Transport;
|
|
65
|
+
/**
|
|
66
|
+
* Cancellation signal — wired to the transport's native cancellation
|
|
67
|
+
* mechanism (HTTP abort, gRPC cancel, MCP cancel, Ctrl-C for CLI).
|
|
68
|
+
* Layers must propagate it to any async work they initiate (SPEC §11).
|
|
69
|
+
*/
|
|
70
|
+
signal: AbortSignal;
|
|
71
|
+
/**
|
|
72
|
+
* Transport-native request object — escape hatch for adapters that need
|
|
73
|
+
* raw access to e.g. a Fastify `Request` or an MCP `CallToolRequest`.
|
|
74
|
+
* Ordinary plugins must NOT depend on this; it degrades portability.
|
|
75
|
+
*/
|
|
76
|
+
raw?: unknown;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* A single chunk emitted by a streaming operation (SPEC §11).
|
|
80
|
+
*
|
|
81
|
+
* The type is intentionally open (`unknown`) because the per-chunk element
|
|
82
|
+
* type is described by `operation.output` (JSON Schema) — static typing is
|
|
83
|
+
* achieved per-plugin via generics when the element type is known.
|
|
84
|
+
*/
|
|
85
|
+
export type Chunk = unknown;
|
|
86
|
+
/**
|
|
87
|
+
* The non-streaming result of a Layer invocation — the value that flows
|
|
88
|
+
* back out to the transport adapter.
|
|
89
|
+
*/
|
|
90
|
+
export type Result = unknown;
|
|
91
|
+
/**
|
|
92
|
+
* The continuation function passed to a Layer.
|
|
93
|
+
*
|
|
94
|
+
* Calling `next()` invokes the **remaining** layers and ultimately `dispatch`.
|
|
95
|
+
* A Layer may call it at most once per request. Not calling it short-circuits
|
|
96
|
+
* all downstream layers and dispatch (SPEC §8.1 rule 1).
|
|
97
|
+
*
|
|
98
|
+
* The return type is a union to support both unary and streaming operations
|
|
99
|
+
* from a single `LayerCapability.layer` signature (SPEC §11).
|
|
100
|
+
*/
|
|
101
|
+
export type Next = () => Promise<Result> | AsyncIterable<Chunk>;
|
|
102
|
+
/**
|
|
103
|
+
* A single emitted file produced by {@link TargetCapability.generate}.
|
|
104
|
+
*
|
|
105
|
+
* `content` is always a UTF-8 string — plugins emit source code, config,
|
|
106
|
+
* or structured text in any language (TS, Python, proto, YAML, …).
|
|
107
|
+
* Nothing in core restricts the language (SPEC [inv:language-agnostic-output]).
|
|
108
|
+
*/
|
|
109
|
+
export interface File {
|
|
110
|
+
/** Relative or absolute path where the file should be written. */
|
|
111
|
+
path: string;
|
|
112
|
+
/** UTF-8 string content to write. */
|
|
113
|
+
content: string;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Opaque handle returned by {@link TargetCapability.serve}.
|
|
117
|
+
*
|
|
118
|
+
* The minimal contract is `close()` for graceful shutdown. Plugins may
|
|
119
|
+
* extend this with transport-specific members (e.g. `port`, `url`).
|
|
120
|
+
*/
|
|
121
|
+
export interface Server {
|
|
122
|
+
/** Gracefully shut down the server and release all resources. */
|
|
123
|
+
close(): Promise<void>;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The runtime harness injected into {@link TargetCapability.serve}.
|
|
127
|
+
*
|
|
128
|
+
* Provides `invoke` — the composed Layer stack that wraps `dispatch`.
|
|
129
|
+
* Transports call `harness.invoke(op, partialCall)` once per inbound request;
|
|
130
|
+
* the harness threads the active plugins' layers around the operation and
|
|
131
|
+
* returns/streams the result (SPEC §8).
|
|
132
|
+
*/
|
|
133
|
+
export interface Harness {
|
|
134
|
+
/**
|
|
135
|
+
* Invoke the full composed-Layer stack for `op` with the given call context.
|
|
136
|
+
* Partial — the harness fills in `operation`, `ctx`, and wires `signal`.
|
|
137
|
+
*
|
|
138
|
+
* Returns a `Promise<Result>` for unary operations or an
|
|
139
|
+
* `AsyncIterable<Chunk>` for streaming operations (SPEC §11).
|
|
140
|
+
*/
|
|
141
|
+
invoke(op: Operation, call: Omit<Call, 'operation' | 'ctx'>): Promise<Result> | AsyncIterable<Chunk>;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The merged canonical descriptor: the full set of `Operation`s produced by
|
|
145
|
+
* merging one or more per-language extractors (SPEC §4).
|
|
146
|
+
*
|
|
147
|
+
* Plugins receive a `Descriptor` at generate/serve time; they **must not**
|
|
148
|
+
* modify it — it is the single source of truth.
|
|
149
|
+
*/
|
|
150
|
+
export interface Descriptor {
|
|
151
|
+
/** All extracted operations, across all host languages, in insertion order. */
|
|
152
|
+
operations: Operation[];
|
|
153
|
+
/**
|
|
154
|
+
* The host tag of the primary language runtime, e.g. `'ts'`, `'py'`.
|
|
155
|
+
* Plugins may use this to restrict generated code to the primary host.
|
|
156
|
+
*/
|
|
157
|
+
host: string;
|
|
158
|
+
/**
|
|
159
|
+
* Optional namespace segment from `--namespace` or the tsconfig root folder.
|
|
160
|
+
* Typically the npm package name or a short domain slug.
|
|
161
|
+
*/
|
|
162
|
+
namespace?: string;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* **`target`** capability — project the descriptor to a transport or format
|
|
166
|
+
* (SPEC §7.1).
|
|
167
|
+
*
|
|
168
|
+
* A `TargetCapability` is selected by `--type <name>` at CLI time. It
|
|
169
|
+
* optionally both *generates* (emits static files) and *serves* (runs a live
|
|
170
|
+
* server hosting the domain functions in-process).
|
|
171
|
+
*
|
|
172
|
+
* @typeParam Opts - Plugin-specific options, validated via `optionsSchema`.
|
|
173
|
+
*
|
|
174
|
+
* @remarks
|
|
175
|
+
* **v1 compatibility:** The v1 `OutputPlugin` interface (`generate(PluginInput)`)
|
|
176
|
+
* is the direct precursor. v2 `TargetCapability.generate` receives a
|
|
177
|
+
* `Descriptor` instead of `PluginInput` — the richer, host-agnostic contract.
|
|
178
|
+
* v1 plugins migrate by wrapping their `PluginInput` construction in the new
|
|
179
|
+
* `generate` signature. `serve()` is the new addition for live server targets.
|
|
180
|
+
*/
|
|
181
|
+
export interface TargetCapability<Opts = Record<string, unknown>> {
|
|
182
|
+
/**
|
|
183
|
+
* Short identifier for this target — the value the user passes to `--type`.
|
|
184
|
+
* Examples: `'mcp'`, `'http-fastify'`, `'http-express'`, `'cli'`, `'proto'`.
|
|
185
|
+
*/
|
|
186
|
+
name: string;
|
|
187
|
+
/**
|
|
188
|
+
* Project the descriptor to a set of static files (generate mode).
|
|
189
|
+
*
|
|
190
|
+
* Called once per `apigen generate` invocation with the full merged
|
|
191
|
+
* descriptor and the resolved plugin options. The returned `File[]` is
|
|
192
|
+
* written to the output directory by the CLI.
|
|
193
|
+
*
|
|
194
|
+
* @param descriptor - The merged canonical descriptor (read-only).
|
|
195
|
+
* @param opts - Plugin-specific options (already validated).
|
|
196
|
+
* @returns Array of files to emit (may be empty; never `null`).
|
|
197
|
+
*/
|
|
198
|
+
generate(descriptor: Descriptor, opts: Opts): File[] | Promise<File[]>;
|
|
199
|
+
/**
|
|
200
|
+
* Start a live server hosting the domain functions in-process (run mode).
|
|
201
|
+
*
|
|
202
|
+
* Called once per `apigen run` invocation. The transport adapter wires the
|
|
203
|
+
* harness's `invoke()` to the native request/response cycle.
|
|
204
|
+
*
|
|
205
|
+
* Optional — omit for codegen-only plugins (clients, proto, docs).
|
|
206
|
+
*
|
|
207
|
+
* @param descriptor - The merged canonical descriptor (read-only).
|
|
208
|
+
* @param harness - The composed Layer stack; call `harness.invoke()` per request.
|
|
209
|
+
* @param opts - Plugin-specific options (already validated).
|
|
210
|
+
* @returns A {@link Server} handle; the CLI calls `server.close()` on SIGINT/SIGTERM.
|
|
211
|
+
*/
|
|
212
|
+
serve?(descriptor: Descriptor, harness: Harness, opts: Opts): Promise<Server>;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* **`layer`** capability — wrap operations (the onion) (SPEC §7.1 / §8 / §8.1).
|
|
216
|
+
*
|
|
217
|
+
* A `LayerCapability` is loaded via `--use <plugin>` and is composed by the
|
|
218
|
+
* harness around the `dispatch` call. Hook sugar (`onRequest`/`onResponse`/
|
|
219
|
+
* `onError`) compiles to a `LayerCapability` — one execution model (SPEC §7.1).
|
|
220
|
+
*
|
|
221
|
+
* @remarks
|
|
222
|
+
* **Streaming:** `layer` may return an `AsyncIterable<Chunk>` — making it an
|
|
223
|
+
* `async function*` that wraps `next()` with `for await … yield` — to
|
|
224
|
+
* participate in the full per-chunk stream lifecycle (SPEC §11).
|
|
225
|
+
*/
|
|
226
|
+
export interface LayerCapability {
|
|
227
|
+
/**
|
|
228
|
+
* Extra envelope fields this layer needs on the request side — merged into
|
|
229
|
+
* the effective descriptor's envelope schema before serving begins.
|
|
230
|
+
*
|
|
231
|
+
* Keys are bare field names; values are JSON Schema fragments.
|
|
232
|
+
* Example: `{ session: { type: 'string', description: 'session token' } }`.
|
|
233
|
+
*/
|
|
234
|
+
envelopeFields?: Record<string, JSONSchema>;
|
|
235
|
+
/**
|
|
236
|
+
* The layer function — owns the continuation.
|
|
237
|
+
*
|
|
238
|
+
* Call `next()` to invoke the remaining layers and `dispatch`. Not calling
|
|
239
|
+
* `next()` short-circuits all downstream layers (SPEC §8.1 rule 1).
|
|
240
|
+
*
|
|
241
|
+
* For streaming operations, return an `AsyncIterable<Chunk>` wrapping the
|
|
242
|
+
* iterable returned by `next()` (SPEC §11).
|
|
243
|
+
*
|
|
244
|
+
* @param call - The inbound call descriptor.
|
|
245
|
+
* @param next - The continuation — call at most once.
|
|
246
|
+
* @returns `Promise<Result>` for unary operations; `AsyncIterable<Chunk>`
|
|
247
|
+
* for streaming operations.
|
|
248
|
+
*/
|
|
249
|
+
layer(call: Call, next: Next): Promise<Result> | AsyncIterable<Chunk>;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* **`mount`** capability — add synthetic operations to the descriptor
|
|
253
|
+
* (SPEC §7.1 / §7.2b / §7.2c).
|
|
254
|
+
*
|
|
255
|
+
* A `MountCapability` is loaded via `--use <plugin>` and contributes extra
|
|
256
|
+
* `Operation`-like entries (with an in-process `handler`) that flow through the
|
|
257
|
+
* harness and Layer stack exactly like extracted operations. Typical uses:
|
|
258
|
+
* `/meta/openapi`, `/meta/health`, version endpoints.
|
|
259
|
+
*/
|
|
260
|
+
export interface MountCapability {
|
|
261
|
+
/**
|
|
262
|
+
* Return the set of synthetic operations this plugin contributes.
|
|
263
|
+
*
|
|
264
|
+
* `MountedOperation` extends `Operation` with an in-process `handler` and
|
|
265
|
+
* an optional `transports` filter (default: all transports).
|
|
266
|
+
*
|
|
267
|
+
* @param descriptor - The current merged descriptor (read-only).
|
|
268
|
+
* @param opts - Plugin-specific options.
|
|
269
|
+
* @returns Array of `MountedOperation`s; may be empty.
|
|
270
|
+
*/
|
|
271
|
+
operations(descriptor: Descriptor, opts?: Record<string, unknown>): MountedOperation[];
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* A synthetic operation contributed by a {@link MountCapability}.
|
|
275
|
+
*
|
|
276
|
+
* Extends the base {@link Operation} with:
|
|
277
|
+
* - `transports` — optional filter to restrict which transports expose this
|
|
278
|
+
* operation (default: all four, matching the host plugin's transport set).
|
|
279
|
+
* - `handler` — the in-process function called when a request arrives.
|
|
280
|
+
* Called with the same {@link Call} context as extracted operations; the
|
|
281
|
+
* returned value is marshalled by the transport adapter.
|
|
282
|
+
*/
|
|
283
|
+
export type MountedOperation = Operation & {
|
|
284
|
+
/**
|
|
285
|
+
* Optional transport filter. When omitted the operation is exposed on all
|
|
286
|
+
* transports supported by the active target plugin.
|
|
287
|
+
*/
|
|
288
|
+
transports?: Transport[];
|
|
289
|
+
/**
|
|
290
|
+
* The in-process handler for this synthetic operation.
|
|
291
|
+
*
|
|
292
|
+
* Called with the full {@link Call} context (same as extracted operations)
|
|
293
|
+
* after the composed Layer stack has run. The return value is serialised by
|
|
294
|
+
* the transport adapter and may be a `Promise` or an `AsyncIterable` for
|
|
295
|
+
* streaming mounts.
|
|
296
|
+
*/
|
|
297
|
+
handler(call: Call): unknown | Promise<unknown> | AsyncIterable<Chunk>;
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* **`envelope`** capability — declare request/response side-channel fields
|
|
301
|
+
* (SPEC §7.1 / §9 / §9.1).
|
|
302
|
+
*
|
|
303
|
+
* A plugin with an `envelope` capability advertises the transport-agnostic
|
|
304
|
+
* side-channel fields it reads from (request) or writes to (response) without
|
|
305
|
+
* wrapping the operation in a Layer. The harness merges these schemas into the
|
|
306
|
+
* effective descriptor's envelope before serving.
|
|
307
|
+
*
|
|
308
|
+
* Canonical field identity is `(pluginId, field)` (SPEC §9.1); fields declared
|
|
309
|
+
* here are surfaced by each transport adapter per the binding table:
|
|
310
|
+
* - HTTP/gRPC/MCP: `x-<pluginId>-<field>` header/metadata/`_meta` key
|
|
311
|
+
* - CLI: `--<pluginId>-<field>` flag + `APIGEN_<PLUGINID>_<FIELD>` env var
|
|
312
|
+
*/
|
|
313
|
+
export interface EnvelopeCapability {
|
|
314
|
+
/**
|
|
315
|
+
* JSON Schema fragments for fields this plugin reads from the incoming
|
|
316
|
+
* transport-native metadata (HTTP headers, gRPC metadata, MCP `_meta`,
|
|
317
|
+
* CLI flags/env).
|
|
318
|
+
*
|
|
319
|
+
* Keys are bare field names (e.g. `'session'`); the adapter prepends
|
|
320
|
+
* `x-<pluginId>-` when surfacing on k/v carriers.
|
|
321
|
+
*/
|
|
322
|
+
request?: Record<string, JSONSchema>;
|
|
323
|
+
/**
|
|
324
|
+
* JSON Schema fragments for fields this plugin writes to the outgoing
|
|
325
|
+
* transport-native metadata (response headers, trailers, `_meta`, stderr).
|
|
326
|
+
*
|
|
327
|
+
* Keys follow the same `x-<pluginId>-<field>` convention as `request`.
|
|
328
|
+
*/
|
|
329
|
+
response?: Record<string, JSONSchema>;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* The v2 plugin interface (SPEC §7.1).
|
|
333
|
+
*
|
|
334
|
+
* Every apigen plugin is an object that satisfies this interface. A plugin
|
|
335
|
+
* declares one or more **capabilities** — the harness fans them out at compose
|
|
336
|
+
* time. All four capability slots are optional; a minimal "noop" plugin omits
|
|
337
|
+
* all of them (useful as a template).
|
|
338
|
+
*
|
|
339
|
+
* @typeParam Opts - Plugin-specific CLI options, validated against
|
|
340
|
+
* `capabilities.target.optionsSchema` (if present) before being passed to
|
|
341
|
+
* `generate` / `serve` / `mount.operations`.
|
|
342
|
+
*
|
|
343
|
+
* @example Logger layer (SPEC §7.2a)
|
|
344
|
+
* ```ts
|
|
345
|
+
* export default {
|
|
346
|
+
* id: 'logger',
|
|
347
|
+
* capabilities: {
|
|
348
|
+
* layer: {
|
|
349
|
+
* layer: async (call, next) => {
|
|
350
|
+
* const t = Date.now()
|
|
351
|
+
* console.error(`→ ${call.operation.id}`)
|
|
352
|
+
* try { const r = await next(); console.error(`← ${call.operation.id} ${Date.now()-t}ms`); return r }
|
|
353
|
+
* catch (e) { console.error(`✗ ${call.operation.id}`); throw e }
|
|
354
|
+
* },
|
|
355
|
+
* },
|
|
356
|
+
* },
|
|
357
|
+
* } satisfies Plugin
|
|
358
|
+
* ```
|
|
359
|
+
*
|
|
360
|
+
* @example OpenAPI mount (SPEC §7.2b)
|
|
361
|
+
* ```ts
|
|
362
|
+
* import { toOpenApi } from '@adhd/apigen-openapi'
|
|
363
|
+
* export default {
|
|
364
|
+
* id: 'openapi',
|
|
365
|
+
* capabilities: {
|
|
366
|
+
* mount: {
|
|
367
|
+
* operations: (d) => [{
|
|
368
|
+
* ...syntheticOp('_meta/openapi', d),
|
|
369
|
+
* handler: () => toOpenApi(d),
|
|
370
|
+
* }],
|
|
371
|
+
* },
|
|
372
|
+
* },
|
|
373
|
+
* } satisfies Plugin
|
|
374
|
+
* ```
|
|
375
|
+
*/
|
|
376
|
+
export interface Plugin<Opts = Record<string, unknown>> {
|
|
377
|
+
/**
|
|
378
|
+
* Canonical fully-qualified plugin identifier (SPEC §7.1).
|
|
379
|
+
*
|
|
380
|
+
* Use the package name (e.g. `'@adhd/apigen-ts-plugin-logger'`) or a short
|
|
381
|
+
* slug (e.g. `'logger'`). The CLI accepts either as the `--use` / `--type`
|
|
382
|
+
* argument. The id is also used as the `pluginId` in envelope field naming
|
|
383
|
+
* (`x-<id>-<field>`, SPEC §9.1).
|
|
384
|
+
*/
|
|
385
|
+
id: string;
|
|
386
|
+
/**
|
|
387
|
+
* Optional human-readable description shown in `apigen plugins list` output
|
|
388
|
+
* and generated documentation.
|
|
389
|
+
*/
|
|
390
|
+
description?: string;
|
|
391
|
+
/**
|
|
392
|
+
* The source language this plugin consumes.
|
|
393
|
+
*
|
|
394
|
+
* Used by the `serve` command to route each source file to the plugin(s)
|
|
395
|
+
* whose `language` matches its extension (`.ts`/`.tsx`/`.mts`/`.cts` → `'ts'`,
|
|
396
|
+
* `.py` → `'py'`, etc.).
|
|
397
|
+
*
|
|
398
|
+
* Defaults to `'ts'` when omitted for backward-compatibility with plugins
|
|
399
|
+
* authored before this field was introduced. All first-party plugins
|
|
400
|
+
* explicitly declare `language: 'ts'`.
|
|
401
|
+
*/
|
|
402
|
+
language?: PluginLanguage;
|
|
403
|
+
/**
|
|
404
|
+
* Optional JSON Schema for plugin-specific options.
|
|
405
|
+
*
|
|
406
|
+
* When provided, the CLI validates the `--opt` values supplied via
|
|
407
|
+
* `--use <plugin> --opt key=value` before constructing `opts`.
|
|
408
|
+
*/
|
|
409
|
+
optionsSchema?: Record<string, unknown>;
|
|
410
|
+
/**
|
|
411
|
+
* The set of capabilities this plugin contributes. All four are optional.
|
|
412
|
+
*
|
|
413
|
+
* At least one capability is expected in practice; the harness warns
|
|
414
|
+
* (at debug level) when a loaded plugin declares no capabilities.
|
|
415
|
+
*/
|
|
416
|
+
capabilities: {
|
|
417
|
+
/**
|
|
418
|
+
* Target capability — project the descriptor to a transport/format and/or
|
|
419
|
+
* host domain functions in-process (SPEC §7.1 / §5).
|
|
420
|
+
*
|
|
421
|
+
* Selected by `--type <plugin>`.
|
|
422
|
+
*/
|
|
423
|
+
target?: TargetCapability<Opts>;
|
|
424
|
+
/**
|
|
425
|
+
* Layer capability — wrap all operations in the onion (SPEC §7.1 / §8).
|
|
426
|
+
*
|
|
427
|
+
* Loaded by `--use <plugin>` when the plugin declares this capability.
|
|
428
|
+
*/
|
|
429
|
+
layer?: LayerCapability;
|
|
430
|
+
/**
|
|
431
|
+
* Mount capability — add synthetic operations to the descriptor
|
|
432
|
+
* (SPEC §7.1). Typical uses: `/meta/openapi`, `/meta/health`.
|
|
433
|
+
*
|
|
434
|
+
* Loaded by `--use <plugin>`.
|
|
435
|
+
*/
|
|
436
|
+
mount?: MountCapability;
|
|
437
|
+
/**
|
|
438
|
+
* Envelope capability — declare request/response side-channel fields
|
|
439
|
+
* (SPEC §7.1 / §9.1). Loaded by `--use <plugin>`.
|
|
440
|
+
*
|
|
441
|
+
* A plugin may combine `envelope` with `layer` to both *declare* the
|
|
442
|
+
* fields it needs and *read/write* them in its layer function.
|
|
443
|
+
*/
|
|
444
|
+
envelope?: EnvelopeCapability;
|
|
445
|
+
};
|
|
446
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Async element-schema builder — the `buildSchema` entrypoint, injected to avoid a circular import. */
|
|
2
|
+
export type RecurseBuildSchema = (typeText: string) => Promise<Record<string, unknown>>;
|
|
3
|
+
/**
|
|
4
|
+
* If `typeText` is a `Map` / `Set` / tuple, build the canonical
|
|
5
|
+
* array-compatible JSON-Schema fragment (recursing into element types via
|
|
6
|
+
* `recurse`). Returns `undefined` for any other type so the caller can fall
|
|
7
|
+
* through to its normal generator path.
|
|
8
|
+
*
|
|
9
|
+
* Schemas produced:
|
|
10
|
+
* - `Map<K, V>` → `{ type:'array', items:{ type:'array', items:[Kschema, Vschema],
|
|
11
|
+
* minItems:2, maxItems:2 } }`
|
|
12
|
+
* - `Set<T>` → `{ type:'array', items:Tschema, uniqueItems:true }`
|
|
13
|
+
* - `[A, B, C]` → `{ type:'array', items:[Aschema, Bschema, Cschema],
|
|
14
|
+
* minItems:N, maxItems:N }` (positional / "prefixItems" form)
|
|
15
|
+
*
|
|
16
|
+
* The tuple positional `items` array is exactly what Ajv (draft-07) validates
|
|
17
|
+
* positionally, and what the runtime transcoder walks position-by-position.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildMapSetTupleSchema(typeText: string, recurse: RecurseBuildSchema): Promise<Record<string, unknown> | undefined>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursive fallback schema builder for primitive, array, union, and anonymous object types.
|
|
3
|
+
*
|
|
4
|
+
* @param typeText - The TypeScript type text to convert (already alias-normalised by the caller).
|
|
5
|
+
* @param depth - Recursion depth guard (max 6).
|
|
6
|
+
* @param aliases - Optional local-name → canonical-key alias map from `extractScalarAliases`.
|
|
7
|
+
* When provided, bare local names (e.g. `D2`) are resolved to their canonical
|
|
8
|
+
* SCALAR_SCHEMAS key before the lookup. The caller (buildSchema) already
|
|
9
|
+
* applies `applyAliasesToTypeText` to the top-level typeText, so aliases are
|
|
10
|
+
* only needed here as a fallback for any residual non-canonical names that
|
|
11
|
+
* survive deep inside the type text (e.g. union branches).
|
|
12
|
+
*/
|
|
13
|
+
export declare function morphFallback(typeText: string, depth: number, aliases?: ReadonlyMap<string, string>): Record<string, unknown>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Project, SourceFile, Type } from 'ts-morph';
|
|
2
|
+
|
|
3
|
+
/** Async element-schema builder — the shared `buildSchema` entrypoint, injected to avoid a circular import. */
|
|
4
|
+
export type RecurseBuildSchema = (typeText: string) => Promise<Record<string, unknown>>;
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a TypeScript type-text string into a ts-morph {@link Type}, evaluated
|
|
7
|
+
* in the lexical scope of `sf` (so the source file's imports and local
|
|
8
|
+
* declarations — `Decimal`, `Box`, `Point`, … — are all in scope).
|
|
9
|
+
*
|
|
10
|
+
* We add a throwaway `type __ApigenProbe_N = <typeText>` alias to the IN-MEMORY
|
|
11
|
+
* source file, read its `.getType()`, run the supplied visitor while the alias
|
|
12
|
+
* node is still live (ts-morph `Type` objects are only meaningfully walkable
|
|
13
|
+
* while their owning node exists), then remove the alias. The file is never
|
|
14
|
+
* `.save()`d, so the user's source on disk is untouched, and re-running the
|
|
15
|
+
* extractor sees the original file.
|
|
16
|
+
*
|
|
17
|
+
* Returns `undefined` if the alias cannot be created or its type cannot be
|
|
18
|
+
* resolved, so the caller can fall through to its text-based fallback.
|
|
19
|
+
*/
|
|
20
|
+
export declare function withResolvedType<T>(_project: Project, sf: SourceFile, typeText: string, visit: (type: Type) => Promise<T>): Promise<T | undefined>;
|
|
21
|
+
/**
|
|
22
|
+
* Walk a ts-morph {@link Type} and build the canonical JSON-Schema fragment for
|
|
23
|
+
* the *structural* shapes that the scalar / Map-Set-tuple handlers don't own:
|
|
24
|
+
* anonymous objects, index signatures (`Record`), arrays, tuples, and unions.
|
|
25
|
+
*
|
|
26
|
+
* Every nested type is routed back through `recurse` (the shared `buildSchema`)
|
|
27
|
+
* via its type-text so scalar formats, Map/Set/tuple wire, aliases, and
|
|
28
|
+
* readonly arrays continue to flow through their existing handlers — this
|
|
29
|
+
* function never re-implements those rules.
|
|
30
|
+
*
|
|
31
|
+
* @param type The resolved ts-morph Type (live; from {@link withResolvedType}).
|
|
32
|
+
* @param recurse The shared buildSchema entrypoint (resolves nested type-text).
|
|
33
|
+
* @param depth Recursion guard.
|
|
34
|
+
* @returns A JSON-Schema fragment, or `{}` for genuinely opaque types — matching
|
|
35
|
+
* the prior permissive fallback (e.g. an unresolvable generic).
|
|
36
|
+
*/
|
|
37
|
+
export declare function walkType(type: Type, recurse: RecurseBuildSchema, depth: number): Promise<Record<string, unknown>>;
|
|
38
|
+
/** Advisory discriminator metadata attached to a `oneOf` union fragment. */
|
|
39
|
+
export interface InlineDiscriminator {
|
|
40
|
+
/** Name of the property shared by every branch that carries a distinct literal value. */
|
|
41
|
+
propertyName: string;
|
|
42
|
+
/**
|
|
43
|
+
* Literal value → JSON-Pointer into this schema's own `oneOf` array
|
|
44
|
+
* (e.g. `"dog": "#/oneOf/0"`). Inline branches have no `$ref`/`$defs`
|
|
45
|
+
* identity of their own, so — unlike `union.ts`'s $ref-based
|
|
46
|
+
* `buildUnionSchema` — the mapping target is a same-document pointer.
|
|
47
|
+
*/
|
|
48
|
+
mapping: Record<string, string>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Looks for a property name present in EVERY variant's schema whose value is
|
|
52
|
+
* a single-value string/number `enum` (i.e. a literal, such as `kind: 'dog'`)
|
|
53
|
+
* and whose literal values are pairwise distinct across variants. When found,
|
|
54
|
+
* returns the discriminator metadata; otherwise `undefined` — a plain `oneOf`
|
|
55
|
+
* (no discriminator) still correctly models "exactly one of these variants"
|
|
56
|
+
* even when the variants have no shared literal tag (e.g. a domain interface
|
|
57
|
+
* unioned with `Record<string, unknown>`).
|
|
58
|
+
*/
|
|
59
|
+
export declare function detectDiscriminator(variants: ReadonlyArray<Record<string, unknown>>): InlineDiscriminator | undefined;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { X_APIGEN_LOGICAL, X_APIGEN_CODEC, X_APIGEN_CTOR, X_APIGEN_TOJSON } from '@adhd/apigen-base-logical';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Information about one field of a nominal class, already expressed as a JSON
|
|
5
|
+
* Schema fragment (typically produced by `buildSchema` from ts-json-schema.ts).
|
|
6
|
+
*/
|
|
7
|
+
export interface NominalField {
|
|
8
|
+
/** Original field name as declared in source. */
|
|
9
|
+
name: string;
|
|
10
|
+
/** JSON Schema fragment for the field's type. */
|
|
11
|
+
schema: Record<string, unknown>;
|
|
12
|
+
/** True when the field has a `?` modifier or default value. */
|
|
13
|
+
optional?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Extracted class descriptor handed to `buildNominalSchema`.
|
|
17
|
+
*
|
|
18
|
+
* All ts-morph work is done upstream (in `extract-classes.ts` or the
|
|
19
|
+
* caller). This record is the minimal slice needed to emit the schema.
|
|
20
|
+
*/
|
|
21
|
+
export interface NominalClassInfo {
|
|
22
|
+
/** Source-level class name, e.g. `"User"`. */
|
|
23
|
+
className: string;
|
|
24
|
+
/**
|
|
25
|
+
* Namespace qualifier prepended to `className` to form the stable
|
|
26
|
+
* `LogicalTypeId` (e.g. `"cli"` → `"cli.User"`). Pass `""` for
|
|
27
|
+
* top-level / unnamespaced classes.
|
|
28
|
+
*/
|
|
29
|
+
namespace: string;
|
|
30
|
+
/** Ordered list of public serializable fields. */
|
|
31
|
+
fields: NominalField[];
|
|
32
|
+
/**
|
|
33
|
+
* Names of static/instance methods declared on the class (used to derive
|
|
34
|
+
* the optional `x-apigen-ctor` / `x-apigen-tojson` hints).
|
|
35
|
+
*/
|
|
36
|
+
methodNames?: string[];
|
|
37
|
+
}
|
|
38
|
+
/** The JSON Schema for a single field inside a `$def`. */
|
|
39
|
+
export type FieldSchema = Record<string, unknown>;
|
|
40
|
+
/**
|
|
41
|
+
* The `$def` schema for the class — an object schema with `properties`,
|
|
42
|
+
* `required`, and optional `x-apigen-*` hints.
|
|
43
|
+
*/
|
|
44
|
+
export interface NominalDef {
|
|
45
|
+
type: 'object';
|
|
46
|
+
properties: Record<string, FieldSchema>;
|
|
47
|
+
required: string[];
|
|
48
|
+
[X_APIGEN_LOGICAL]: 'nominal';
|
|
49
|
+
[X_APIGEN_CODEC]: string;
|
|
50
|
+
[X_APIGEN_CTOR]?: string;
|
|
51
|
+
[X_APIGEN_TOJSON]?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The full result of `buildNominalSchema`:
|
|
55
|
+
* - `def`: the `$def` object to register under `$defs[className]`.
|
|
56
|
+
* - `ref`: the `$ref` fragment for inline use (`#/$defs/<ClassName>`).
|
|
57
|
+
* - `defKey`: the key to use under `$defs` (the class name).
|
|
58
|
+
* - `codecId`: the qualified `LogicalTypeId` (e.g. `"cli.User"`).
|
|
59
|
+
*/
|
|
60
|
+
export interface NominalSchema {
|
|
61
|
+
/** Key under `$defs` (`className`). */
|
|
62
|
+
defKey: string;
|
|
63
|
+
/** The `$def` schema fragment — register as `$defs[defKey]`. */
|
|
64
|
+
def: NominalDef;
|
|
65
|
+
/** Inline `$ref` schema for positions that hold this nominal type. */
|
|
66
|
+
ref: {
|
|
67
|
+
$ref: string;
|
|
68
|
+
};
|
|
69
|
+
/** Stable, namespace-qualified `LogicalTypeId`. */
|
|
70
|
+
codecId: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Given an extracted nominal class descriptor, emit the canonical descriptor
|
|
74
|
+
* schema fragment: a named `$def` (object schema with fields + `x-apigen-*`
|
|
75
|
+
* hints) and a `$ref` pointing at it.
|
|
76
|
+
*
|
|
77
|
+
* Per invariant `[inv:hints-advisory]` the `x-apigen-*` keys are advisory —
|
|
78
|
+
* see `stripHints` to obtain a pure structural schema.
|
|
79
|
+
*
|
|
80
|
+
* @param info - Class info produced by the extractor.
|
|
81
|
+
* @returns `{ defKey, def, ref, codecId }` ready for insertion into a descriptor.
|
|
82
|
+
*/
|
|
83
|
+
export declare function buildNominalSchema(info: NominalClassInfo): NominalSchema;
|
|
84
|
+
declare const HINT_KEYS: readonly ["x-apigen-logical", "x-apigen-codec", "x-apigen-ctor", "x-apigen-tojson"];
|
|
85
|
+
/**
|
|
86
|
+
* Return a copy of `def` with all `x-apigen-*` advisory keys removed.
|
|
87
|
+
*
|
|
88
|
+
* The result is a plain JSON Schema object schema — valid and structurally
|
|
89
|
+
* authoritative without any apigen extension keys. Proves invariant
|
|
90
|
+
* `[inv:hints-advisory]`: stripping hints MUST leave a structurally-complete
|
|
91
|
+
* schema (type + properties + required) that a standard JSON-Schema validator
|
|
92
|
+
* can consume.
|
|
93
|
+
*/
|
|
94
|
+
export declare function stripHints(def: NominalDef): Omit<NominalDef, (typeof HINT_KEYS)[number]>;
|
|
95
|
+
export {};
|