@kubb/ast 5.0.0-beta.6 → 5.0.0-beta.61
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/LICENSE +17 -10
- package/README.md +51 -27
- package/dist/chunk-CNktS9qV.js +17 -0
- package/dist/defineMacro-BLIR6k-j.d.ts +475 -0
- package/dist/defineMacro-BTXvS8nI.js +106 -0
- package/dist/defineMacro-BTXvS8nI.js.map +1 -0
- package/dist/defineMacro-Bv9R_9a2.cjs +123 -0
- package/dist/defineMacro-Bv9R_9a2.cjs.map +1 -0
- package/dist/extractStringsFromNodes-Cja-xxx5.js +29 -0
- package/dist/extractStringsFromNodes-Cja-xxx5.js.map +1 -0
- package/dist/extractStringsFromNodes-DKgDjFO0.cjs +34 -0
- package/dist/extractStringsFromNodes-DKgDjFO0.cjs.map +1 -0
- package/dist/extractStringsFromNodes-p4mX1TQD.d.ts +14 -0
- package/dist/factory-CZNOGI-N.js +283 -0
- package/dist/factory-CZNOGI-N.js.map +1 -0
- package/dist/factory-DG1CVkEb.cjs +300 -0
- package/dist/factory-DG1CVkEb.cjs.map +1 -0
- package/dist/factory.cjs +29 -0
- package/dist/factory.d.ts +62 -0
- package/dist/factory.js +3 -0
- package/dist/index-BzjwdK2M.d.ts +2433 -0
- package/dist/index.cjs +444 -2180
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +94 -3408
- package/dist/index.js +395 -2101
- package/dist/index.js.map +1 -1
- package/dist/macros.cjs +117 -0
- package/dist/macros.cjs.map +1 -0
- package/dist/macros.d.ts +59 -0
- package/dist/macros.js +115 -0
- package/dist/macros.js.map +1 -0
- package/dist/operationParams-BZ07xDm0.d.ts +204 -0
- package/dist/response-KUdWiDWw.js +658 -0
- package/dist/response-KUdWiDWw.js.map +1 -0
- package/dist/response-hnSw2NKE.cjs +1027 -0
- package/dist/response-hnSw2NKE.cjs.map +1 -0
- package/dist/types-DyDzizSf.d.ts +364 -0
- package/dist/types.cjs +0 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +1 -0
- package/dist/utils-BLJwyza-.cjs +912 -0
- package/dist/utils-BLJwyza-.cjs.map +1 -0
- package/dist/utils-CF_-Pn_c.js +770 -0
- package/dist/utils-CF_-Pn_c.js.map +1 -0
- package/dist/utils.cjs +36 -0
- package/dist/utils.d.ts +358 -0
- package/dist/utils.js +4 -0
- package/dist/visitor-DJ6ZEJvq.js +548 -0
- package/dist/visitor-DJ6ZEJvq.js.map +1 -0
- package/dist/visitor-DpKZ9Tk0.cjs +654 -0
- package/dist/visitor-DpKZ9Tk0.cjs.map +1 -0
- package/package.json +21 -6
- package/src/constants.ts +19 -64
- package/src/dedupe.ts +239 -0
- package/src/defineMacro.ts +132 -0
- package/src/dialect.ts +53 -0
- package/src/factory.ts +67 -678
- package/src/guards.ts +10 -92
- package/src/index.ts +13 -44
- package/src/infer.ts +16 -14
- package/src/macros/index.ts +3 -0
- package/src/macros/macroDiscriminatorEnum.ts +44 -0
- package/src/macros/macroEnumName.ts +25 -0
- package/src/macros/macroSimplifyUnion.ts +50 -0
- package/src/mocks.ts +7 -127
- package/src/node.ts +128 -0
- package/src/nodes/base.ts +5 -12
- package/src/nodes/code.ts +165 -74
- package/src/nodes/content.ts +56 -0
- package/src/nodes/file.ts +97 -36
- package/src/nodes/function.ts +216 -156
- package/src/nodes/http.ts +1 -35
- package/src/nodes/index.ts +23 -15
- package/src/nodes/input.ts +140 -0
- package/src/nodes/operation.ts +122 -68
- package/src/nodes/output.ts +23 -0
- package/src/nodes/parameter.ts +33 -3
- package/src/nodes/property.ts +36 -3
- package/src/nodes/requestBody.ts +61 -0
- package/src/nodes/response.ts +58 -13
- package/src/nodes/schema.ts +93 -17
- package/src/printer.ts +48 -42
- package/src/registry.ts +75 -0
- package/src/signature.ts +207 -0
- package/src/types.ts +8 -68
- package/src/utils/codegen.ts +104 -0
- package/src/utils/extractStringsFromNodes.ts +34 -0
- package/src/utils/fileMerge.ts +184 -0
- package/src/utils/index.ts +11 -0
- package/src/utils/operationParams.ts +353 -0
- package/src/utils/refs.ts +138 -0
- package/src/utils/schemaGraph.ts +169 -0
- package/src/utils/schemaMerge.ts +34 -0
- package/src/utils/schemaTraversal.ts +86 -0
- package/src/utils/strings.ts +139 -0
- package/src/visitor.ts +227 -289
- package/dist/chunk--u3MIqq1.js +0 -8
- package/src/nodes/root.ts +0 -64
- package/src/refs.ts +0 -67
- package/src/resolvers.ts +0 -45
- package/src/transformers.ts +0 -159
- package/src/utils.ts +0 -915
package/src/nodes/schema.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { InferSchemaNode } from '../infer.ts'
|
|
2
|
+
import { defineNode, type DistributiveOmit } from '../node.ts'
|
|
1
3
|
import type { BaseNode } from './base.ts'
|
|
2
4
|
import type { PropertyNode } from './property.ts'
|
|
3
5
|
|
|
@@ -58,12 +60,12 @@ export type PrimitiveSchemaType =
|
|
|
58
60
|
/**
|
|
59
61
|
* Composite schema types.
|
|
60
62
|
*/
|
|
61
|
-
|
|
63
|
+
type ComplexSchemaType = 'tuple' | 'union' | 'intersection' | 'enum'
|
|
62
64
|
|
|
63
65
|
/**
|
|
64
66
|
* Schema types that need special handling in generators.
|
|
65
67
|
*/
|
|
66
|
-
|
|
68
|
+
type SpecialSchemaType = 'ref' | 'datetime' | 'time' | 'uuid' | 'email' | 'url' | 'ipv4' | 'ipv6' | 'blob'
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
71
|
* All schema type strings.
|
|
@@ -154,6 +156,10 @@ type SchemaNodeBase = BaseNode & {
|
|
|
154
156
|
* For example, this is `'string'` for a `uuid` schema.
|
|
155
157
|
*/
|
|
156
158
|
primitive?: PrimitiveSchemaType
|
|
159
|
+
/**
|
|
160
|
+
* Schema `format` value.
|
|
161
|
+
*/
|
|
162
|
+
format?: string
|
|
157
163
|
}
|
|
158
164
|
|
|
159
165
|
/**
|
|
@@ -174,7 +180,7 @@ export type ObjectSchemaNode = SchemaNodeBase & {
|
|
|
174
180
|
*/
|
|
175
181
|
type: 'object'
|
|
176
182
|
/**
|
|
177
|
-
* Primitive type
|
|
183
|
+
* Primitive type, always `'object'` for object schemas.
|
|
178
184
|
*/
|
|
179
185
|
primitive: 'object'
|
|
180
186
|
/**
|
|
@@ -274,8 +280,9 @@ export type UnionSchemaNode = CompositeSchemaNodeBase & {
|
|
|
274
280
|
*/
|
|
275
281
|
discriminatorPropertyName?: string
|
|
276
282
|
/**
|
|
277
|
-
*
|
|
278
|
-
* '
|
|
283
|
+
* How many union members must be valid.
|
|
284
|
+
* - `'one'`: exactly one member, from `oneOf`
|
|
285
|
+
* - `'any'`: any number of members, from `anyOf`
|
|
279
286
|
*/
|
|
280
287
|
strategy?: 'one' | 'any'
|
|
281
288
|
}
|
|
@@ -302,7 +309,7 @@ export type IntersectionSchemaNode = CompositeSchemaNodeBase & {
|
|
|
302
309
|
/**
|
|
303
310
|
* One named enum item.
|
|
304
311
|
*/
|
|
305
|
-
|
|
312
|
+
type EnumValueNode = {
|
|
306
313
|
/**
|
|
307
314
|
* Enum item name.
|
|
308
315
|
*/
|
|
@@ -364,8 +371,9 @@ export type RefSchemaNode = SchemaNodeBase & {
|
|
|
364
371
|
type: 'ref'
|
|
365
372
|
/**
|
|
366
373
|
* Referenced schema name.
|
|
374
|
+
* `null` means Kubb has processed this and determined there is no applicable name.
|
|
367
375
|
*/
|
|
368
|
-
name?: string
|
|
376
|
+
name?: string | null
|
|
369
377
|
/**
|
|
370
378
|
* Original `$ref` path, for example, `#/components/schemas/Order`.
|
|
371
379
|
* Used to resolve names later.
|
|
@@ -376,14 +384,12 @@ export type RefSchemaNode = SchemaNodeBase & {
|
|
|
376
384
|
*/
|
|
377
385
|
pattern?: string
|
|
378
386
|
/**
|
|
379
|
-
* The fully-parsed schema
|
|
380
|
-
* Populated during OAS parsing when the
|
|
381
|
-
* `
|
|
382
|
-
*
|
|
383
|
-
* Useful for inspecting the referenced schema's structure (e.g. `primitive`, `properties`)
|
|
384
|
-
* without following the reference manually.
|
|
387
|
+
* The fully-parsed schema this ref resolves to, so its structure (`primitive`, `properties`)
|
|
388
|
+
* can be read without following the reference. Populated during OAS parsing when the
|
|
389
|
+
* definition resolves, `null` when it can't or the ref is circular, and `undefined` when
|
|
390
|
+
* resolution has not been attempted.
|
|
385
391
|
*/
|
|
386
|
-
schema?: SchemaNode
|
|
392
|
+
schema?: SchemaNode | null
|
|
387
393
|
}
|
|
388
394
|
|
|
389
395
|
/**
|
|
@@ -556,7 +562,7 @@ export type UrlSchemaNode = SchemaNodeBase & {
|
|
|
556
562
|
* const uuidSchema: FormatStringSchemaNode = { kind: 'Schema', type: 'uuid', min: 36, max: 36 }
|
|
557
563
|
* ```
|
|
558
564
|
*/
|
|
559
|
-
|
|
565
|
+
type FormatStringSchemaNode = SchemaNodeBase & {
|
|
560
566
|
/**
|
|
561
567
|
* Schema type discriminator.
|
|
562
568
|
*/
|
|
@@ -579,7 +585,7 @@ export type FormatStringSchemaNode = SchemaNodeBase & {
|
|
|
579
585
|
* const ipv4Schema: Ipv4SchemaNode = { kind: 'Schema', type: 'ipv4' }
|
|
580
586
|
* ```
|
|
581
587
|
*/
|
|
582
|
-
|
|
588
|
+
type Ipv4SchemaNode = SchemaNodeBase & {
|
|
583
589
|
/**
|
|
584
590
|
* Schema type discriminator.
|
|
585
591
|
*/
|
|
@@ -594,7 +600,7 @@ export type Ipv4SchemaNode = SchemaNodeBase & {
|
|
|
594
600
|
* const ipv6Schema: Ipv6SchemaNode = { kind: 'Schema', type: 'ipv6' }
|
|
595
601
|
* ```
|
|
596
602
|
*/
|
|
597
|
-
|
|
603
|
+
type Ipv6SchemaNode = SchemaNodeBase & {
|
|
598
604
|
/**
|
|
599
605
|
* Schema type discriminator.
|
|
600
606
|
*/
|
|
@@ -654,3 +660,73 @@ export type SchemaNode =
|
|
|
654
660
|
| Ipv4SchemaNode
|
|
655
661
|
| Ipv6SchemaNode
|
|
656
662
|
| ScalarSchemaNode
|
|
663
|
+
|
|
664
|
+
type CreateSchemaObjectInput = Omit<ObjectSchemaNode, 'kind' | 'properties' | 'primitive'> & { properties?: Array<PropertyNode>; primitive?: 'object' }
|
|
665
|
+
type CreateSchemaInput = CreateSchemaObjectInput | DistributiveOmit<Exclude<SchemaNode, ObjectSchemaNode>, 'kind'>
|
|
666
|
+
type CreateSchemaOutput<T extends CreateSchemaInput> = InferSchemaNode<T> & {
|
|
667
|
+
kind: 'Schema'
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Maps schema `type` to its underlying `primitive`.
|
|
672
|
+
* Primitive types map to themselves. Special string formats map to `'string'`.
|
|
673
|
+
* Complex types (`ref`, `enum`, `union`, `intersection`, `tuple`, `blob`) are left unset.
|
|
674
|
+
*/
|
|
675
|
+
const TYPE_TO_PRIMITIVE: Partial<Record<SchemaNode['type'], PrimitiveSchemaType>> = {
|
|
676
|
+
string: 'string',
|
|
677
|
+
number: 'number',
|
|
678
|
+
integer: 'integer',
|
|
679
|
+
bigint: 'bigint',
|
|
680
|
+
boolean: 'boolean',
|
|
681
|
+
null: 'null',
|
|
682
|
+
any: 'any',
|
|
683
|
+
unknown: 'unknown',
|
|
684
|
+
void: 'void',
|
|
685
|
+
never: 'never',
|
|
686
|
+
object: 'object',
|
|
687
|
+
array: 'array',
|
|
688
|
+
date: 'date',
|
|
689
|
+
uuid: 'string',
|
|
690
|
+
email: 'string',
|
|
691
|
+
url: 'string',
|
|
692
|
+
datetime: 'string',
|
|
693
|
+
time: 'string',
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Definition for the {@link SchemaNode}. Object schemas default `properties` to an
|
|
698
|
+
* empty array, and `primitive` is inferred from `type` when not explicitly provided.
|
|
699
|
+
*/
|
|
700
|
+
export const schemaDef = defineNode<SchemaNode, CreateSchemaInput>({
|
|
701
|
+
kind: 'Schema',
|
|
702
|
+
build: (props) => {
|
|
703
|
+
if (props.type === 'object') {
|
|
704
|
+
return { properties: [], primitive: 'object' as const, ...props }
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return { primitive: TYPE_TO_PRIMITIVE[props.type as keyof typeof TYPE_TO_PRIMITIVE], ...props }
|
|
708
|
+
},
|
|
709
|
+
children: ['properties', 'items', 'members', 'additionalProperties'],
|
|
710
|
+
visitorKey: 'schema',
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Creates a `SchemaNode`, narrowed to the variant of `props.type`.
|
|
715
|
+
*
|
|
716
|
+
* @example
|
|
717
|
+
* ```ts
|
|
718
|
+
* const scalar = createSchema({ type: 'string' })
|
|
719
|
+
* // { kind: 'Schema', type: 'string', primitive: 'string' }
|
|
720
|
+
* ```
|
|
721
|
+
*
|
|
722
|
+
* @example
|
|
723
|
+
* ```ts
|
|
724
|
+
* const object = createSchema({ type: 'object' })
|
|
725
|
+
* // { kind: 'Schema', type: 'object', primitive: 'object', properties: [] }
|
|
726
|
+
* ```
|
|
727
|
+
*/
|
|
728
|
+
export function createSchema<T extends CreateSchemaInput>(props: T): CreateSchemaOutput<T>
|
|
729
|
+
export function createSchema(props: CreateSchemaInput): SchemaNode
|
|
730
|
+
export function createSchema(props: CreateSchemaInput): SchemaNode {
|
|
731
|
+
return schemaDef.create(props)
|
|
732
|
+
}
|
package/src/printer.ts
CHANGED
|
@@ -13,12 +13,12 @@ import type { SchemaNode, SchemaNodeByType, SchemaType } from './nodes/index.ts'
|
|
|
13
13
|
* }
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
type PrinterHandlerContext<TOutput, TOptions extends object> = {
|
|
17
17
|
/**
|
|
18
18
|
* Recursively transform a nested `SchemaNode` to `TOutput` using the node-level handlers.
|
|
19
19
|
* Use `this.transform` inside `nodes` handlers and inside the `print` override.
|
|
20
20
|
*/
|
|
21
|
-
transform: (node: SchemaNode) => TOutput | null
|
|
21
|
+
transform: (node: SchemaNode) => TOutput | null
|
|
22
22
|
/**
|
|
23
23
|
* Options for this printer instance.
|
|
24
24
|
*/
|
|
@@ -37,16 +37,16 @@ export type PrinterHandlerContext<TOutput, TOptions extends object> = {
|
|
|
37
37
|
* }
|
|
38
38
|
* ```
|
|
39
39
|
*/
|
|
40
|
-
|
|
40
|
+
type PrinterHandler<TOutput, TOptions extends object, T extends SchemaType = SchemaType> = (
|
|
41
41
|
this: PrinterHandlerContext<TOutput, TOptions>,
|
|
42
42
|
node: SchemaNodeByType[T],
|
|
43
|
-
) => TOutput | null
|
|
43
|
+
) => TOutput | null
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Partial map of per-node-type handler overrides for a printer.
|
|
47
47
|
*
|
|
48
48
|
* Each key is a `SchemaType` string (e.g. `'date'`, `'string'`).
|
|
49
|
-
* Supply only the handlers you want to replace
|
|
49
|
+
* Supply only the handlers you want to replace. The printer's built-in
|
|
50
50
|
* defaults fill in the rest.
|
|
51
51
|
*
|
|
52
52
|
* @example
|
|
@@ -69,10 +69,10 @@ export type PrinterPartial<TOutput, TOptions extends object> = Partial<{
|
|
|
69
69
|
/**
|
|
70
70
|
* Generic shape used by `definePrinter`.
|
|
71
71
|
*
|
|
72
|
-
* - `TName`
|
|
73
|
-
* - `TOptions`
|
|
74
|
-
* - `TOutput`
|
|
75
|
-
* - `TPrintOutput`
|
|
72
|
+
* - `TName` unique string identifier (e.g. `'zod'`, `'ts'`)
|
|
73
|
+
* - `TOptions` options passed to and stored on the printer instance
|
|
74
|
+
* - `TOutput` the type emitted by node handlers
|
|
75
|
+
* - `TPrintOutput` type returned by public `print` (defaults to `TOutput`)
|
|
76
76
|
*
|
|
77
77
|
* @example
|
|
78
78
|
* ```ts
|
|
@@ -104,17 +104,17 @@ export type Printer<T extends PrinterFactoryOptions = PrinterFactoryOptions> = {
|
|
|
104
104
|
*/
|
|
105
105
|
options: T['options']
|
|
106
106
|
/**
|
|
107
|
-
* Node-level dispatcher
|
|
108
|
-
* Always dispatches through the `nodes` map
|
|
109
|
-
*
|
|
107
|
+
* Node-level dispatcher, converts a `SchemaNode` directly to `TOutput` using the `nodes` handlers.
|
|
108
|
+
* Always dispatches through the `nodes` map. Never calls the `print` override.
|
|
109
|
+
* Reach for it when you need the raw output (e.g. `ts.TypeNode`) without declaration wrapping.
|
|
110
110
|
*/
|
|
111
|
-
transform: (node: SchemaNode) => T['output'] | null
|
|
111
|
+
transform: (node: SchemaNode) => T['output'] | null
|
|
112
112
|
/**
|
|
113
113
|
* Public printer. If the builder provides a root-level `print`, this calls that
|
|
114
114
|
* higher-level function (which may produce full declarations).
|
|
115
115
|
* Otherwise, falls back to the node-level dispatcher.
|
|
116
116
|
*/
|
|
117
|
-
print: (node: SchemaNode) => T['printOutput'] | null
|
|
117
|
+
print: (node: SchemaNode) => T['printOutput'] | null
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/**
|
|
@@ -143,28 +143,32 @@ type PrinterBuilder<T extends PrinterFactoryOptions> = (options: T['options']) =
|
|
|
143
143
|
/**
|
|
144
144
|
* Optional root-level print override. When provided, becomes the public `printer.print`.
|
|
145
145
|
* Use `this.transform(node)` inside this function to dispatch to the node-level handlers (`nodes`),
|
|
146
|
-
* not the override itself
|
|
146
|
+
* not the override itself, so recursion is safe.
|
|
147
147
|
*/
|
|
148
148
|
print?: (this: PrinterHandlerContext<T['output'], T['options']>, node: SchemaNode) => T['printOutput'] | null
|
|
149
149
|
}
|
|
150
|
-
|
|
151
150
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
151
|
+
* Defines a schema printer: a function that takes a `SchemaNode` and emits
|
|
152
|
+
* code in your target language. Each plugin that produces code from schemas
|
|
153
|
+
* (TypeScript types, Zod schemas, Faker factories) ships a printer built
|
|
154
|
+
* with this helper.
|
|
155
155
|
*
|
|
156
156
|
* The builder receives resolved options and returns:
|
|
157
|
-
* - `name` — a unique identifier for the printer
|
|
158
|
-
* - `options` — options stored on the returned printer instance
|
|
159
|
-
* - `nodes` — a map of `SchemaType` → handler functions that convert a `SchemaNode` to `TOutput`
|
|
160
|
-
* - `print` _(optional)_ — top-level override exposed as `printer.print`
|
|
161
|
-
* - Inside this function, use `this.transform(node)` to dispatch to the `nodes` map
|
|
162
|
-
* - This keeps recursion safe and avoids self-calls
|
|
163
157
|
*
|
|
164
|
-
*
|
|
158
|
+
* - `name` unique identifier for the printer.
|
|
159
|
+
* - `options` stored on the returned printer instance.
|
|
160
|
+
* - `nodes` map of `SchemaType` → handler. Handlers return the rendered
|
|
161
|
+
* output (a string, a TypeScript AST node, ...) for that schema type.
|
|
162
|
+
* - `print` (optional), top-level override exposed as `printer.print`.
|
|
163
|
+
* Use `this.transform(node)` inside it to dispatch to `nodes` recursively.
|
|
164
|
+
*
|
|
165
|
+
* Without a `print` override, `printer.print` falls back to `printer.transform`
|
|
166
|
+
* (the node-level dispatcher).
|
|
165
167
|
*
|
|
166
|
-
* @example
|
|
168
|
+
* @example Tiny Zod printer
|
|
167
169
|
* ```ts
|
|
170
|
+
* import { definePrinter, type PrinterFactoryOptions } from '@kubb/ast'
|
|
171
|
+
*
|
|
168
172
|
* type PrinterZod = PrinterFactoryOptions<'zod', { strict?: boolean }, string>
|
|
169
173
|
*
|
|
170
174
|
* export const zodPrinter = definePrinter<PrinterZod>((options) => ({
|
|
@@ -173,7 +177,9 @@ type PrinterBuilder<T extends PrinterFactoryOptions> = (options: T['options']) =
|
|
|
173
177
|
* nodes: {
|
|
174
178
|
* string: () => 'z.string()',
|
|
175
179
|
* object(node) {
|
|
176
|
-
* const props = node.properties
|
|
180
|
+
* const props = node.properties
|
|
181
|
+
* .map((p) => `${p.name}: ${this.transform(p.schema)}`)
|
|
182
|
+
* .join(', ')
|
|
177
183
|
* return `z.object({ ${props} })`
|
|
178
184
|
* },
|
|
179
185
|
* },
|
|
@@ -186,15 +192,15 @@ export function definePrinter<T extends PrinterFactoryOptions = PrinterFactoryOp
|
|
|
186
192
|
|
|
187
193
|
/**
|
|
188
194
|
* Generic printer-factory function used by `definePrinter` and `defineFunctionPrinter`.
|
|
189
|
-
|
|
195
|
+
*
|
|
190
196
|
* @example
|
|
191
197
|
* ```ts
|
|
192
|
-
* export const defineFunctionPrinter = createPrinterFactory<
|
|
193
|
-
* (node) =>
|
|
198
|
+
* export const defineFunctionPrinter = createPrinterFactory<FunctionParamNode, FunctionParamKind, Partial<Record<FunctionParamKind, FunctionParamNode>>>(
|
|
199
|
+
* (node) => node.kind,
|
|
194
200
|
* )
|
|
195
201
|
* ```
|
|
196
202
|
*/
|
|
197
|
-
export function createPrinterFactory<TNode, TKey extends string, TNodeByKey extends Partial<Record<TKey, TNode>>>(getKey: (node: TNode) => TKey |
|
|
203
|
+
export function createPrinterFactory<TNode, TKey extends string, TNodeByKey extends Partial<Record<TKey, TNode>>>(getKey: (node: TNode) => TKey | null) {
|
|
198
204
|
return function <T extends PrinterFactoryOptions>(
|
|
199
205
|
build: (options: T['options']) => {
|
|
200
206
|
name: T['name']
|
|
@@ -202,40 +208,40 @@ export function createPrinterFactory<TNode, TKey extends string, TNodeByKey exte
|
|
|
202
208
|
nodes: Partial<{
|
|
203
209
|
[K in TKey]: (
|
|
204
210
|
this: {
|
|
205
|
-
transform: (node: TNode) => T['output'] | null
|
|
211
|
+
transform: (node: TNode) => T['output'] | null
|
|
206
212
|
options: T['options']
|
|
207
213
|
},
|
|
208
214
|
node: TNodeByKey[K],
|
|
209
|
-
) => T['output'] | null
|
|
215
|
+
) => T['output'] | null
|
|
210
216
|
}>
|
|
211
217
|
print?: (
|
|
212
218
|
this: {
|
|
213
|
-
transform: (node: TNode) => T['output'] | null
|
|
219
|
+
transform: (node: TNode) => T['output'] | null
|
|
214
220
|
options: T['options']
|
|
215
221
|
},
|
|
216
222
|
node: TNode,
|
|
217
|
-
) => T['printOutput'] | null
|
|
223
|
+
) => T['printOutput'] | null
|
|
218
224
|
},
|
|
219
225
|
): (options?: T['options']) => {
|
|
220
226
|
name: T['name']
|
|
221
227
|
options: T['options']
|
|
222
|
-
transform: (node: TNode) => T['output'] | null
|
|
223
|
-
print: (node: TNode) => T['printOutput'] | null
|
|
228
|
+
transform: (node: TNode) => T['output'] | null
|
|
229
|
+
print: (node: TNode) => T['printOutput'] | null
|
|
224
230
|
} {
|
|
225
231
|
return (options) => {
|
|
226
232
|
const { name, options: resolvedOptions, nodes, print: printOverride } = build(options ?? ({} as T['options']))
|
|
227
233
|
|
|
228
234
|
const context = {
|
|
229
235
|
options: resolvedOptions,
|
|
230
|
-
transform: (node: TNode): T['output'] | null
|
|
236
|
+
transform: (node: TNode): T['output'] | null => {
|
|
231
237
|
const key = getKey(node)
|
|
232
|
-
if (key ===
|
|
238
|
+
if (key === null) return null
|
|
233
239
|
|
|
234
240
|
const handler = nodes[key]
|
|
235
241
|
|
|
236
242
|
if (!handler) return null
|
|
237
243
|
|
|
238
|
-
return (handler as (this: typeof context, node: TNode) => T['output'] | null
|
|
244
|
+
return (handler as (this: typeof context, node: TNode) => T['output'] | null).call(context, node)
|
|
239
245
|
},
|
|
240
246
|
}
|
|
241
247
|
|
|
@@ -243,7 +249,7 @@ export function createPrinterFactory<TNode, TKey extends string, TNodeByKey exte
|
|
|
243
249
|
name,
|
|
244
250
|
options: resolvedOptions,
|
|
245
251
|
transform: context.transform,
|
|
246
|
-
print: (printOverride ? printOverride.bind(context) : context.transform) as (node: TNode) => T['printOutput'] | null
|
|
252
|
+
print: (printOverride ? printOverride.bind(context) : context.transform) as (node: TNode) => T['printOutput'] | null,
|
|
247
253
|
}
|
|
248
254
|
}
|
|
249
255
|
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { NodeDef } from './node.ts'
|
|
2
|
+
import { arrowFunctionDef, breakDef, constDef, functionDef, jsxDef, textDef, typeDef } from './nodes/code.ts'
|
|
3
|
+
import { contentDef } from './nodes/content.ts'
|
|
4
|
+
import { exportDef, fileDef, importDef, sourceDef } from './nodes/file.ts'
|
|
5
|
+
import { functionParameterDef, functionParametersDef, indexedAccessTypeDef, objectBindingPatternDef, typeLiteralDef } from './nodes/function.ts'
|
|
6
|
+
import { inputDef } from './nodes/input.ts'
|
|
7
|
+
import { operationDef } from './nodes/operation.ts'
|
|
8
|
+
import { outputDef } from './nodes/output.ts'
|
|
9
|
+
import { parameterDef } from './nodes/parameter.ts'
|
|
10
|
+
import { propertyDef } from './nodes/property.ts'
|
|
11
|
+
import { requestBodyDef } from './nodes/requestBody.ts'
|
|
12
|
+
import { responseDef } from './nodes/response.ts'
|
|
13
|
+
import { schemaDef } from './nodes/schema.ts'
|
|
14
|
+
|
|
15
|
+
// Surface every def from one place so the package barrel re-exports them with `export * from './registry.ts'`.
|
|
16
|
+
// Adding a node means adding its `defineNode` to a `nodes/*.ts` file and listing it in `nodeDefs` below, nothing else.
|
|
17
|
+
export {
|
|
18
|
+
arrowFunctionDef,
|
|
19
|
+
breakDef,
|
|
20
|
+
constDef,
|
|
21
|
+
contentDef,
|
|
22
|
+
exportDef,
|
|
23
|
+
fileDef,
|
|
24
|
+
functionDef,
|
|
25
|
+
functionParameterDef,
|
|
26
|
+
functionParametersDef,
|
|
27
|
+
importDef,
|
|
28
|
+
indexedAccessTypeDef,
|
|
29
|
+
inputDef,
|
|
30
|
+
jsxDef,
|
|
31
|
+
objectBindingPatternDef,
|
|
32
|
+
operationDef,
|
|
33
|
+
outputDef,
|
|
34
|
+
parameterDef,
|
|
35
|
+
propertyDef,
|
|
36
|
+
requestBodyDef,
|
|
37
|
+
responseDef,
|
|
38
|
+
schemaDef,
|
|
39
|
+
sourceDef,
|
|
40
|
+
textDef,
|
|
41
|
+
typeDef,
|
|
42
|
+
typeLiteralDef,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Every node definition. Adding a node means adding its `defineNode` to one
|
|
47
|
+
* `nodes/*.ts` file and listing it here. The visitor tables in `visitor.ts` derive from it.
|
|
48
|
+
*/
|
|
49
|
+
export const nodeDefs = [
|
|
50
|
+
inputDef,
|
|
51
|
+
outputDef,
|
|
52
|
+
operationDef,
|
|
53
|
+
requestBodyDef,
|
|
54
|
+
contentDef,
|
|
55
|
+
responseDef,
|
|
56
|
+
schemaDef,
|
|
57
|
+
propertyDef,
|
|
58
|
+
parameterDef,
|
|
59
|
+
functionParameterDef,
|
|
60
|
+
functionParametersDef,
|
|
61
|
+
typeLiteralDef,
|
|
62
|
+
indexedAccessTypeDef,
|
|
63
|
+
objectBindingPatternDef,
|
|
64
|
+
constDef,
|
|
65
|
+
typeDef,
|
|
66
|
+
functionDef,
|
|
67
|
+
arrowFunctionDef,
|
|
68
|
+
textDef,
|
|
69
|
+
breakDef,
|
|
70
|
+
jsxDef,
|
|
71
|
+
importDef,
|
|
72
|
+
exportDef,
|
|
73
|
+
sourceDef,
|
|
74
|
+
fileDef,
|
|
75
|
+
] satisfies ReadonlyArray<NodeDef>
|
package/src/signature.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { hash } from 'node:crypto'
|
|
2
|
+
import type { SchemaNode } from './nodes/index.ts'
|
|
3
|
+
import { extractRefName } from './utils/index.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The flags shared by every node kind that affect its type: `primitive`, `format`, `nullable`.
|
|
7
|
+
*/
|
|
8
|
+
function flagsDescriptor(node: SchemaNode): string {
|
|
9
|
+
return `${node.primitive ?? ''};${node.format ?? ''};${node.nullable ? 1 : 0}`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function refTargetName(node: Extract<SchemaNode, { type: 'ref' }>): string {
|
|
13
|
+
if (node.ref) return extractRefName(node.ref)
|
|
14
|
+
return node.name ?? ''
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type ScalarField = { kind: 'scalar'; key: string; prefix: string }
|
|
18
|
+
type BoolField = { kind: 'bool'; key: string; prefix: string }
|
|
19
|
+
type ChildField = { kind: 'child'; key: string; prefix: string }
|
|
20
|
+
type ChildrenField = { kind: 'children'; key: string; prefix: string }
|
|
21
|
+
type ObjectPropsField = { kind: 'objectProps' }
|
|
22
|
+
type AdditionalPropsField = { kind: 'additionalProps' }
|
|
23
|
+
type PatternPropsField = { kind: 'patternProps' }
|
|
24
|
+
type EnumValuesField = { kind: 'enumValues' }
|
|
25
|
+
type RefTargetField = { kind: 'refTarget' }
|
|
26
|
+
|
|
27
|
+
type ShapeField =
|
|
28
|
+
| ScalarField
|
|
29
|
+
| BoolField
|
|
30
|
+
| ChildField
|
|
31
|
+
| ChildrenField
|
|
32
|
+
| ObjectPropsField
|
|
33
|
+
| AdditionalPropsField
|
|
34
|
+
| PatternPropsField
|
|
35
|
+
| EnumValuesField
|
|
36
|
+
| RefTargetField
|
|
37
|
+
|
|
38
|
+
const arrayTupleFields: ReadonlyArray<ShapeField> = [
|
|
39
|
+
{ kind: 'children', key: 'items', prefix: 'i' },
|
|
40
|
+
{ kind: 'child', key: 'rest', prefix: 'r' },
|
|
41
|
+
{ kind: 'scalar', key: 'min', prefix: 'mn' },
|
|
42
|
+
{ kind: 'scalar', key: 'max', prefix: 'mx' },
|
|
43
|
+
{ kind: 'bool', key: 'unique', prefix: 'u' },
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
const numericFields: ReadonlyArray<ShapeField> = [
|
|
47
|
+
{ kind: 'scalar', key: 'min', prefix: 'mn' },
|
|
48
|
+
{ kind: 'scalar', key: 'max', prefix: 'mx' },
|
|
49
|
+
{ kind: 'scalar', key: 'exclusiveMinimum', prefix: 'emn' },
|
|
50
|
+
{ kind: 'scalar', key: 'exclusiveMaximum', prefix: 'emx' },
|
|
51
|
+
{ kind: 'scalar', key: 'multipleOf', prefix: 'mo' },
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const rangeFields: ReadonlyArray<ShapeField> = [
|
|
55
|
+
{ kind: 'scalar', key: 'min', prefix: 'mn' },
|
|
56
|
+
{ kind: 'scalar', key: 'max', prefix: 'mx' },
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Maps each node `type` to its ordered shape-contributing fields. Types absent from the map
|
|
61
|
+
* (boolean, null, any, and other scalars) fall back to `${type}|${flags}`.
|
|
62
|
+
*/
|
|
63
|
+
const SHAPE_KEYS: Partial<Record<SchemaNode['type'], ReadonlyArray<ShapeField>>> = {
|
|
64
|
+
object: [
|
|
65
|
+
{ kind: 'objectProps' },
|
|
66
|
+
{ kind: 'additionalProps' },
|
|
67
|
+
{ kind: 'patternProps' },
|
|
68
|
+
{ kind: 'scalar', key: 'minProperties', prefix: 'mn' },
|
|
69
|
+
{ kind: 'scalar', key: 'maxProperties', prefix: 'mx' },
|
|
70
|
+
],
|
|
71
|
+
array: arrayTupleFields,
|
|
72
|
+
tuple: arrayTupleFields,
|
|
73
|
+
union: [
|
|
74
|
+
{ kind: 'scalar', key: 'strategy', prefix: 's' },
|
|
75
|
+
{ kind: 'scalar', key: 'discriminatorPropertyName', prefix: 'd' },
|
|
76
|
+
{ kind: 'children', key: 'members', prefix: 'm' },
|
|
77
|
+
],
|
|
78
|
+
intersection: [{ kind: 'children', key: 'members', prefix: 'm' }],
|
|
79
|
+
enum: [{ kind: 'enumValues' }],
|
|
80
|
+
ref: [{ kind: 'refTarget' }],
|
|
81
|
+
string: [
|
|
82
|
+
{ kind: 'scalar', key: 'min', prefix: 'mn' },
|
|
83
|
+
{ kind: 'scalar', key: 'max', prefix: 'mx' },
|
|
84
|
+
{ kind: 'scalar', key: 'pattern', prefix: 'pt' },
|
|
85
|
+
],
|
|
86
|
+
number: numericFields,
|
|
87
|
+
integer: numericFields,
|
|
88
|
+
bigint: numericFields,
|
|
89
|
+
url: [
|
|
90
|
+
{ kind: 'scalar', key: 'path', prefix: 'path' },
|
|
91
|
+
{ kind: 'scalar', key: 'min', prefix: 'mn' },
|
|
92
|
+
{ kind: 'scalar', key: 'max', prefix: 'mx' },
|
|
93
|
+
],
|
|
94
|
+
uuid: rangeFields,
|
|
95
|
+
email: rangeFields,
|
|
96
|
+
datetime: [
|
|
97
|
+
{ kind: 'bool', key: 'offset', prefix: 'o' },
|
|
98
|
+
{ kind: 'bool', key: 'local', prefix: 'l' },
|
|
99
|
+
],
|
|
100
|
+
date: [{ kind: 'scalar', key: 'representation', prefix: 'rep' }],
|
|
101
|
+
time: [{ kind: 'scalar', key: 'representation', prefix: 'rep' }],
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function serializeShapeField(field: ShapeField, node: SchemaNode, record: Record<string, unknown>): string {
|
|
105
|
+
switch (field.kind) {
|
|
106
|
+
case 'scalar':
|
|
107
|
+
return `${field.prefix}:${record[field.key] ?? ''}`
|
|
108
|
+
case 'bool':
|
|
109
|
+
return `${field.prefix}:${record[field.key] ? 1 : 0}`
|
|
110
|
+
case 'child': {
|
|
111
|
+
const child = record[field.key] as SchemaNode | undefined
|
|
112
|
+
return `${field.prefix}:${child ? signatureOf(child) : ''}`
|
|
113
|
+
}
|
|
114
|
+
case 'children': {
|
|
115
|
+
const children = (record[field.key] as Array<SchemaNode> | undefined) ?? []
|
|
116
|
+
return `${field.prefix}[${children.map((c) => signatureOf(c)).join(',')}]`
|
|
117
|
+
}
|
|
118
|
+
case 'objectProps': {
|
|
119
|
+
const obj = node as Extract<SchemaNode, { type: 'object' }>
|
|
120
|
+
const props = (obj.properties ?? []).map((prop) => `${prop.name}${prop.required ? '!' : '?'}${signatureOf(prop.schema)}`).join(',')
|
|
121
|
+
return `p[${props}]`
|
|
122
|
+
}
|
|
123
|
+
case 'additionalProps': {
|
|
124
|
+
const obj = node as Extract<SchemaNode, { type: 'object' }>
|
|
125
|
+
if (typeof obj.additionalProperties === 'boolean') return `ab:${obj.additionalProperties}`
|
|
126
|
+
if (obj.additionalProperties) return `as:${signatureOf(obj.additionalProperties)}`
|
|
127
|
+
return ''
|
|
128
|
+
}
|
|
129
|
+
case 'patternProps': {
|
|
130
|
+
const obj = node as Extract<SchemaNode, { type: 'object' }>
|
|
131
|
+
const pattern = obj.patternProperties
|
|
132
|
+
? Object.keys(obj.patternProperties)
|
|
133
|
+
.sort()
|
|
134
|
+
.map((key) => `${key}=${signatureOf(obj.patternProperties![key]!)}`)
|
|
135
|
+
.join(',')
|
|
136
|
+
: ''
|
|
137
|
+
return `pp[${pattern}]`
|
|
138
|
+
}
|
|
139
|
+
case 'enumValues': {
|
|
140
|
+
const en = node as Extract<SchemaNode, { type: 'enum' }>
|
|
141
|
+
let values = ''
|
|
142
|
+
if (en.namedEnumValues?.length) {
|
|
143
|
+
values = en.namedEnumValues.map((entry) => `${entry.name}=${entry.primitive}:${String(entry.value)}`).join(',')
|
|
144
|
+
} else if (en.enumValues?.length) {
|
|
145
|
+
values = en.enumValues.map((value) => `${value === null ? 'null' : typeof value}:${String(value)}`).join(',')
|
|
146
|
+
}
|
|
147
|
+
return `v[${values}]`
|
|
148
|
+
}
|
|
149
|
+
case 'refTarget': {
|
|
150
|
+
return `->${refTargetName(node as Extract<SchemaNode, { type: 'ref' }>)}`
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Builds the local shape descriptor that {@link signatureOf} hashes: the node's kind, flags,
|
|
157
|
+
* constraints, and its children's signatures.
|
|
158
|
+
*/
|
|
159
|
+
function describeShape(node: SchemaNode): string {
|
|
160
|
+
const flags = flagsDescriptor(node)
|
|
161
|
+
const fields = SHAPE_KEYS[node.type]
|
|
162
|
+
if (!fields) return `${node.type}|${flags}`
|
|
163
|
+
|
|
164
|
+
const record = node as unknown as Record<string, unknown>
|
|
165
|
+
const parts: Array<string> = [`${node.type}|${flags}`]
|
|
166
|
+
for (const field of fields) {
|
|
167
|
+
parts.push(serializeShapeField(field, node, record))
|
|
168
|
+
}
|
|
169
|
+
return parts.join('|')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Caches the digest per node, keyed by identity. A `WeakMap` so entries die with the node, and so
|
|
174
|
+
* a tree hashed during dedupe planning is not walked again when it is rewritten during streaming.
|
|
175
|
+
* Reuse is safe because a signature depends only on content, and nodes are immutable once created.
|
|
176
|
+
*/
|
|
177
|
+
const signatureCache = new WeakMap<SchemaNode, string>()
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Computes a deterministic, shape-only signature (a content hash) for a schema node. Two schemas
|
|
181
|
+
* share a signature when they are structurally identical, ignoring documentation (`name`, `title`,
|
|
182
|
+
* `description`, `example`, `default`, `deprecated`) and usage-slot flags (`optional`, `nullish`,
|
|
183
|
+
* `readOnly`, `writeOnly`). `nullable` is kept because it changes the produced type, and `ref`
|
|
184
|
+
* nodes compare by target name, which also terminates on circular shapes.
|
|
185
|
+
*
|
|
186
|
+
* @example Two enums with different descriptions share a signature
|
|
187
|
+
* ```ts
|
|
188
|
+
* signatureOf(createSchema({ type: 'enum', primitive: 'string', enumValues: ['a', 'b'], description: 'x' })) ===
|
|
189
|
+
* signatureOf(createSchema({ type: 'enum', primitive: 'string', enumValues: ['a', 'b'] }))
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
192
|
+
export function signatureOf(node: SchemaNode): string {
|
|
193
|
+
const cached = signatureCache.get(node)
|
|
194
|
+
if (cached !== undefined) return cached
|
|
195
|
+
const signature = hash('sha256', describeShape(node), 'hex')
|
|
196
|
+
signatureCache.set(node, signature)
|
|
197
|
+
|
|
198
|
+
return signature
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Returns `true` when two schema nodes are structurally identical under shape-only equality,
|
|
203
|
+
* meaning they produce the same TypeScript type.
|
|
204
|
+
*/
|
|
205
|
+
export function isSchemaEqual(a: SchemaNode, b: SchemaNode): boolean {
|
|
206
|
+
return signatureOf(a) === signatureOf(b)
|
|
207
|
+
}
|