@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.
Files changed (102) hide show
  1. package/LICENSE +17 -10
  2. package/README.md +51 -27
  3. package/dist/chunk-CNktS9qV.js +17 -0
  4. package/dist/defineMacro-BLIR6k-j.d.ts +475 -0
  5. package/dist/defineMacro-BTXvS8nI.js +106 -0
  6. package/dist/defineMacro-BTXvS8nI.js.map +1 -0
  7. package/dist/defineMacro-Bv9R_9a2.cjs +123 -0
  8. package/dist/defineMacro-Bv9R_9a2.cjs.map +1 -0
  9. package/dist/extractStringsFromNodes-Cja-xxx5.js +29 -0
  10. package/dist/extractStringsFromNodes-Cja-xxx5.js.map +1 -0
  11. package/dist/extractStringsFromNodes-DKgDjFO0.cjs +34 -0
  12. package/dist/extractStringsFromNodes-DKgDjFO0.cjs.map +1 -0
  13. package/dist/extractStringsFromNodes-p4mX1TQD.d.ts +14 -0
  14. package/dist/factory-CZNOGI-N.js +283 -0
  15. package/dist/factory-CZNOGI-N.js.map +1 -0
  16. package/dist/factory-DG1CVkEb.cjs +300 -0
  17. package/dist/factory-DG1CVkEb.cjs.map +1 -0
  18. package/dist/factory.cjs +29 -0
  19. package/dist/factory.d.ts +62 -0
  20. package/dist/factory.js +3 -0
  21. package/dist/index-BzjwdK2M.d.ts +2433 -0
  22. package/dist/index.cjs +444 -2180
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.ts +94 -3408
  25. package/dist/index.js +395 -2101
  26. package/dist/index.js.map +1 -1
  27. package/dist/macros.cjs +117 -0
  28. package/dist/macros.cjs.map +1 -0
  29. package/dist/macros.d.ts +59 -0
  30. package/dist/macros.js +115 -0
  31. package/dist/macros.js.map +1 -0
  32. package/dist/operationParams-BZ07xDm0.d.ts +204 -0
  33. package/dist/response-KUdWiDWw.js +658 -0
  34. package/dist/response-KUdWiDWw.js.map +1 -0
  35. package/dist/response-hnSw2NKE.cjs +1027 -0
  36. package/dist/response-hnSw2NKE.cjs.map +1 -0
  37. package/dist/types-DyDzizSf.d.ts +364 -0
  38. package/dist/types.cjs +0 -0
  39. package/dist/types.d.ts +6 -0
  40. package/dist/types.js +1 -0
  41. package/dist/utils-BLJwyza-.cjs +912 -0
  42. package/dist/utils-BLJwyza-.cjs.map +1 -0
  43. package/dist/utils-CF_-Pn_c.js +770 -0
  44. package/dist/utils-CF_-Pn_c.js.map +1 -0
  45. package/dist/utils.cjs +36 -0
  46. package/dist/utils.d.ts +358 -0
  47. package/dist/utils.js +4 -0
  48. package/dist/visitor-DJ6ZEJvq.js +548 -0
  49. package/dist/visitor-DJ6ZEJvq.js.map +1 -0
  50. package/dist/visitor-DpKZ9Tk0.cjs +654 -0
  51. package/dist/visitor-DpKZ9Tk0.cjs.map +1 -0
  52. package/package.json +21 -6
  53. package/src/constants.ts +19 -64
  54. package/src/dedupe.ts +239 -0
  55. package/src/defineMacro.ts +132 -0
  56. package/src/dialect.ts +53 -0
  57. package/src/factory.ts +67 -678
  58. package/src/guards.ts +10 -92
  59. package/src/index.ts +13 -44
  60. package/src/infer.ts +16 -14
  61. package/src/macros/index.ts +3 -0
  62. package/src/macros/macroDiscriminatorEnum.ts +44 -0
  63. package/src/macros/macroEnumName.ts +25 -0
  64. package/src/macros/macroSimplifyUnion.ts +50 -0
  65. package/src/mocks.ts +7 -127
  66. package/src/node.ts +128 -0
  67. package/src/nodes/base.ts +5 -12
  68. package/src/nodes/code.ts +165 -74
  69. package/src/nodes/content.ts +56 -0
  70. package/src/nodes/file.ts +97 -36
  71. package/src/nodes/function.ts +216 -156
  72. package/src/nodes/http.ts +1 -35
  73. package/src/nodes/index.ts +23 -15
  74. package/src/nodes/input.ts +140 -0
  75. package/src/nodes/operation.ts +122 -68
  76. package/src/nodes/output.ts +23 -0
  77. package/src/nodes/parameter.ts +33 -3
  78. package/src/nodes/property.ts +36 -3
  79. package/src/nodes/requestBody.ts +61 -0
  80. package/src/nodes/response.ts +58 -13
  81. package/src/nodes/schema.ts +93 -17
  82. package/src/printer.ts +48 -42
  83. package/src/registry.ts +75 -0
  84. package/src/signature.ts +207 -0
  85. package/src/types.ts +8 -68
  86. package/src/utils/codegen.ts +104 -0
  87. package/src/utils/extractStringsFromNodes.ts +34 -0
  88. package/src/utils/fileMerge.ts +184 -0
  89. package/src/utils/index.ts +11 -0
  90. package/src/utils/operationParams.ts +353 -0
  91. package/src/utils/refs.ts +138 -0
  92. package/src/utils/schemaGraph.ts +169 -0
  93. package/src/utils/schemaMerge.ts +34 -0
  94. package/src/utils/schemaTraversal.ts +86 -0
  95. package/src/utils/strings.ts +139 -0
  96. package/src/visitor.ts +227 -289
  97. package/dist/chunk--u3MIqq1.js +0 -8
  98. package/src/nodes/root.ts +0 -64
  99. package/src/refs.ts +0 -67
  100. package/src/resolvers.ts +0 -45
  101. package/src/transformers.ts +0 -159
  102. package/src/utils.ts +0 -915
package/src/guards.ts CHANGED
@@ -1,18 +1,4 @@
1
- import type {
2
- FunctionParameterNode,
3
- FunctionParametersNode,
4
- InputNode,
5
- Node,
6
- NodeKind,
7
- OperationNode,
8
- OutputNode,
9
- ParameterGroupNode,
10
- ParameterNode,
11
- PropertyNode,
12
- ResponseNode,
13
- SchemaNode,
14
- SchemaNodeByType,
15
- } from './nodes/index.ts'
1
+ import type { HttpOperationNode, OperationNode, SchemaNode, SchemaNodeByType } from './nodes/index.ts'
16
2
 
17
3
  /**
18
4
  * Narrows a `SchemaNode` to the variant that matches `type`.
@@ -20,91 +6,23 @@ import type {
20
6
  * @example
21
7
  * ```ts
22
8
  * const schema = createSchema({ type: 'string' })
23
- * const stringNode = narrowSchema(schema, 'string') // StringSchemaNode | undefined
9
+ * const stringNode = narrowSchema(schema, 'string') // StringSchemaNode | null
24
10
  * ```
25
11
  */
26
- export function narrowSchema<T extends SchemaNode['type']>(node: SchemaNode | undefined, type: T): SchemaNodeByType[T] | undefined {
27
- return node?.type === type ? (node as SchemaNodeByType[T]) : undefined
12
+ export function narrowSchema<T extends SchemaNode['type']>(node: SchemaNode | undefined, type: T): SchemaNodeByType[T] | null {
13
+ return node?.type === type ? (node as SchemaNodeByType[T]) : null
28
14
  }
29
15
 
30
- function isKind<T extends Node>(kind: NodeKind) {
31
- return (node: unknown): node is T => (node as Node).kind === kind
32
- }
33
-
34
- /**
35
- * Returns `true` when the input is an `InputNode`.
36
- *
37
- * @example
38
- * ```ts
39
- * if (isInputNode(node)) {
40
- * console.log(node.schemas.length)
41
- * }
42
- * ```
43
- */
44
- export const isInputNode = isKind<InputNode>('Input')
45
-
46
16
  /**
47
- * Returns `true` when the input is an `OutputNode`.
17
+ * Narrows an `OperationNode` to an `HttpOperationNode` so `method` and `path` are present.
48
18
  *
49
19
  * @example
50
20
  * ```ts
51
- * if (isOutputNode(node)) {
52
- * console.log(node.files.length)
21
+ * if (isHttpOperationNode(node)) {
22
+ * console.log(node.method, node.path)
53
23
  * }
54
24
  * ```
55
25
  */
56
- export const isOutputNode = isKind<OutputNode>('Output')
57
-
58
- /**
59
- * Returns `true` when the input is an `OperationNode`.
60
- *
61
- * @example
62
- * ```ts
63
- * if (isOperationNode(node)) {
64
- * console.log(node.operationId)
65
- * }
66
- * ```
67
- */
68
- export const isOperationNode = isKind<OperationNode>('Operation')
69
-
70
- /**
71
- * Returns `true` when the input is a `SchemaNode`.
72
- *
73
- * @example
74
- * ```ts
75
- * if (isSchemaNode(node)) {
76
- * console.log(node.type)
77
- * }
78
- * ```
79
- */
80
- export const isSchemaNode = isKind<SchemaNode>('Schema')
81
-
82
- /**
83
- * Returns `true` when the input is a `PropertyNode`.
84
- */
85
- export const isPropertyNode = isKind<PropertyNode>('Property')
86
-
87
- /**
88
- * Returns `true` when the input is a `ParameterNode`.
89
- */
90
- export const isParameterNode = isKind<ParameterNode>('Parameter')
91
-
92
- /**
93
- * Returns `true` when the input is a `ResponseNode`.
94
- */
95
- export const isResponseNode = isKind<ResponseNode>('Response')
96
-
97
- /**
98
- * Returns `true` when the input is a `FunctionParameterNode`.
99
- */
100
- export const isFunctionParameterNode = isKind<FunctionParameterNode>('FunctionParameter')
101
-
102
- /**
103
- * Returns `true` when the input is a `ParameterGroupNode`.
104
- */
105
- export const isParameterGroupNode = isKind<ParameterGroupNode>('ParameterGroup')
106
-
107
- /**
108
- * Returns `true` when the input is a `FunctionParametersNode`.
109
- */
110
- export const isFunctionParametersNode = isKind<FunctionParametersNode>('FunctionParameters')
26
+ export function isHttpOperationNode(node: OperationNode): node is HttpOperationNode {
27
+ return node.protocol === 'http' || (node.method !== undefined && node.path !== undefined)
28
+ }
package/src/index.ts CHANGED
@@ -1,46 +1,15 @@
1
- export { httpMethods, isScalarPrimitive, mediaTypes, nodeKinds, schemaTypes } from './constants.ts'
2
- export {
3
- createArrowFunction,
4
- createBreak,
5
- createConst,
6
- createExport,
7
- createFile,
8
- createFunction,
9
- createFunctionParameter,
10
- createFunctionParameters,
11
- createImport,
12
- createInput,
13
- createJsx,
14
- createOperation,
15
- createOutput,
16
- createParameter,
17
- createParameterGroup,
18
- createParamsType,
19
- createProperty,
20
- createResponse,
21
- createSchema,
22
- createSource,
23
- createText,
24
- createType,
25
- syncOptionality,
26
- } from './factory.ts'
27
- export { isInputNode, isOperationNode, isOutputNode, isSchemaNode, narrowSchema } from './guards.ts'
1
+ export { httpMethods, schemaTypes } from './constants.ts'
2
+ export { applyDedupe, buildDedupePlan } from './dedupe.ts'
3
+ export { defineSchemaDialect } from './dialect.ts'
4
+ export * as factory from './factory.ts'
5
+ export { isHttpOperationNode, narrowSchema } from './guards.ts'
6
+ export { applyMacros, composeMacros, defineMacro } from './defineMacro.ts'
7
+ export { defineNode } from './node.ts'
8
+ export { syncOptionality } from './node.ts'
9
+ export * from './registry.ts'
28
10
  export { createPrinterFactory, definePrinter } from './printer.ts'
29
- export { extractRefName } from './refs.ts'
30
- export { childName, collectImports, enumPropName, findDiscriminator } from './resolvers.ts'
31
- export { mergeAdjacentObjects, setDiscriminatorEnum, setEnumName, simplifyUnion } from './transformers.ts'
32
- export type * from './types.ts'
33
- export {
34
- caseParams,
35
- collectReferencedSchemaNames,
36
- collectUsedSchemaNames,
37
- containsCircularRef,
38
- createDiscriminantNode,
39
- createOperationParams,
40
- extractStringsFromNodes,
41
- findCircularSchemas,
42
- isStringType,
43
- resolveRefName,
44
- syncSchemaRef,
45
- } from './utils.ts'
11
+ export { signatureOf } from './signature.ts'
12
+ export { extractStringsFromNodes } from './utils/extractStringsFromNodes.ts'
46
13
  export { collect, transform, walk } from './visitor.ts'
14
+
15
+ export type * from './types.ts'
package/src/infer.ts CHANGED
@@ -20,15 +20,25 @@ import type {
20
20
  */
21
21
  export type ParserOptions = {
22
22
  /**
23
- * How `format: 'date-time'` schemas are represented. `false` falls through to a plain string.
23
+ * How `format: 'date-time'` schemas are represented downstream.
24
+ * - `false` falls through to a plain `string` (no validation).
25
+ * - `'string'` emits a datetime string node.
26
+ * - `'stringOffset'` emits a datetime node with timezone offset.
27
+ * - `'stringLocal'` emits a local datetime node.
28
+ * - `'date'` emits a `date` node (JavaScript `Date` object).
24
29
  */
25
30
  dateType: false | 'string' | 'stringOffset' | 'stringLocal' | 'date'
26
31
  /**
27
- * Whether `type: 'integer'` and `format: 'int64'` produce `number` or `bigint` nodes.
32
+ * How `type: 'integer'` (and `format: 'int64'`) maps to TypeScript.
33
+ * - `'bigint'` is exact for 64-bit IDs, but does not round-trip through JSON.
34
+ * - `'number'` fits most JSON APIs. Loses precision above `Number.MAX_SAFE_INTEGER`.
35
+ *
36
+ * @default 'bigint'
28
37
  */
29
38
  integerType?: 'number' | 'bigint'
30
39
  /**
31
- * AST type used when no schema type can be inferred.
40
+ * AST type used when a schema's type cannot be inferred from the spec
41
+ * (`additionalProperties: true`, missing `type`, ...).
32
42
  */
33
43
  unknownType: 'any' | 'unknown' | 'void'
34
44
  /**
@@ -36,7 +46,8 @@ export type ParserOptions = {
36
46
  */
37
47
  emptySchemaType: 'any' | 'unknown' | 'void'
38
48
  /**
39
- * Suffix appended to derived enum names when building property schema names.
49
+ * Suffix appended to derived enum names when Kubb has to invent one
50
+ * (typically for inline enums on object properties).
40
51
  */
41
52
  enumSuffix: 'enum' | (string & {})
42
53
  }
@@ -66,7 +77,7 @@ type ResolveDateTimeNode<TDateType extends ParserOptions['dateType']> = DateTime
66
77
  type SchemaNodeMap<TDateType extends ParserOptions['dateType'] = 'string'> = [
67
78
  [{ $ref: string }, RefSchemaNode],
68
79
  [{ allOf: ReadonlyArray<unknown>; properties: object }, IntersectionSchemaNode],
69
- [{ allOf: readonly [unknown, unknown, ...unknown[]] }, IntersectionSchemaNode],
80
+ [{ allOf: readonly [unknown, unknown, ...Array<unknown>] }, IntersectionSchemaNode],
70
81
  [{ allOf: ReadonlyArray<unknown> }, SchemaNode],
71
82
  [{ oneOf: ReadonlyArray<unknown> }, UnionSchemaNode],
72
83
  [{ anyOf: ReadonlyArray<unknown> }, UnionSchemaNode],
@@ -119,12 +130,3 @@ export type InferSchemaNode<
119
130
  ? TEntry[1]
120
131
  : InferSchemaNode<TSchema, TDateType, TRest>
121
132
  : SchemaNode
122
-
123
- /**
124
- * Backward-compatible alias for `InferSchemaNode`.
125
- */
126
- export type InferSchema<
127
- TSchema extends object,
128
- TDateType extends ParserOptions['dateType'] = 'string',
129
- TEntries extends ReadonlyArray<[object, SchemaNode]> = SchemaNodeMap<TDateType>,
130
- > = InferSchemaNode<TSchema, TDateType, TEntries>
@@ -0,0 +1,3 @@
1
+ export { macroDiscriminatorEnum } from './macroDiscriminatorEnum.ts'
2
+ export { macroEnumName } from './macroEnumName.ts'
3
+ export { macroSimplifyUnion } from './macroSimplifyUnion.ts'
@@ -0,0 +1,44 @@
1
+ import { defineMacro } from '../defineMacro.ts'
2
+ import { narrowSchema } from '../guards.ts'
3
+ import { createProperty } from '../nodes/property.ts'
4
+ import { createSchema } from '../nodes/schema.ts'
5
+
6
+ /**
7
+ * Builds a macro that replaces a discriminator property's schema with a string enum of the given
8
+ * values. Object schemas that lack the property are returned unchanged.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const macro = macroDiscriminatorEnum({ propertyName: 'type', values: ['dog', 'cat'] })
13
+ * const next = applyMacros(objectSchema, [macro], { depth: 'shallow' })
14
+ * ```
15
+ */
16
+ export function macroDiscriminatorEnum({ propertyName, values, enumName }: { propertyName: string; values: Array<string>; enumName?: string }) {
17
+ return defineMacro({
18
+ name: 'discriminator-enum',
19
+ schema(node) {
20
+ const objectNode = narrowSchema(node, 'object')
21
+ if (!objectNode?.properties?.length) return undefined
22
+ if (!objectNode.properties.some((prop) => prop.name === propertyName)) return undefined
23
+
24
+ return createSchema({
25
+ ...objectNode,
26
+ properties: objectNode.properties.map((prop) => {
27
+ if (prop.name !== propertyName) return prop
28
+
29
+ return createProperty({
30
+ ...prop,
31
+ schema: createSchema({
32
+ type: 'enum',
33
+ primitive: 'string',
34
+ enumValues: values,
35
+ name: enumName,
36
+ readOnly: prop.schema.readOnly,
37
+ writeOnly: prop.schema.writeOnly,
38
+ }),
39
+ })
40
+ }),
41
+ })
42
+ },
43
+ })
44
+ }
@@ -0,0 +1,25 @@
1
+ import { defineMacro } from '../defineMacro.ts'
2
+ import { narrowSchema } from '../guards.ts'
3
+ import { enumPropName } from '../utils/refs.ts'
4
+
5
+ /**
6
+ * Builds a macro that names an inline enum schema from its parent and property name. Boolean enums
7
+ * are left anonymous; non-enum nodes are returned unchanged.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const macro = macroEnumName({ parentName: 'Pet', propName: 'status', enumSuffix: 'enum' })
12
+ * const named = applyMacros(propSchema, [macro], { depth: 'shallow' })
13
+ * ```
14
+ */
15
+ export function macroEnumName({ parentName, propName, enumSuffix }: { parentName: string | null | undefined; propName: string; enumSuffix: string }) {
16
+ return defineMacro({
17
+ name: 'enum-name',
18
+ schema(node) {
19
+ const enumNode = narrowSchema(node, 'enum')
20
+ if (enumNode?.primitive === 'boolean') return { ...node, name: null }
21
+ if (enumNode) return { ...node, name: enumPropName(parentName, propName, enumSuffix) }
22
+ return undefined
23
+ },
24
+ })
25
+ }
@@ -0,0 +1,50 @@
1
+ import { isScalarPrimitive } from '../constants.ts'
2
+ import { defineMacro } from '../defineMacro.ts'
3
+ import { narrowSchema } from '../guards.ts'
4
+ import type { SchemaNode } from '../nodes/schema.ts'
5
+
6
+ /**
7
+ * Removes union members a broader scalar primitive already covers, such as a multi-value string enum
8
+ * sitting next to a plain `string`. Single-value enums are kept.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const next = applyMacros(unionSchema, [macroSimplifyUnion], { depth: 'shallow' })
13
+ * ```
14
+ */
15
+ export const macroSimplifyUnion = defineMacro({
16
+ name: 'simplify-union',
17
+ schema(node) {
18
+ const unionNode = narrowSchema(node, 'union')
19
+ if (!unionNode?.members?.length) return undefined
20
+
21
+ const simplified = simplifyUnionMembers(unionNode.members)
22
+ if (simplified.length === unionNode.members.length) return undefined
23
+
24
+ return { ...unionNode, members: simplified }
25
+ },
26
+ })
27
+
28
+ /**
29
+ * Filters union members, dropping enum members that a broader scalar primitive already covers.
30
+ */
31
+ function simplifyUnionMembers(members: Array<SchemaNode>): Array<SchemaNode> {
32
+ const scalarPrimitives = new Set(members.filter((member) => isScalarPrimitive(member.type)).map((m) => m.type))
33
+ if (!scalarPrimitives.size) return members
34
+
35
+ return members.filter((member) => {
36
+ const enumNode = narrowSchema(member, 'enum')
37
+ if (!enumNode) return true
38
+
39
+ const primitive = enumNode.primitive
40
+ if (!primitive) return true
41
+
42
+ const enumValueCount = enumNode.namedEnumValues?.length ?? enumNode.enumValues?.length ?? 0
43
+ if (enumValueCount <= 1) return true
44
+
45
+ if (scalarPrimitives.has(primitive)) return false
46
+ if ((primitive === 'integer' || primitive === 'number') && (scalarPrimitives.has('integer') || scalarPrimitives.has('number'))) return false
47
+
48
+ return true
49
+ })
50
+ }
package/src/mocks.ts CHANGED
@@ -1,5 +1,10 @@
1
- import { createInput, createOperation, createParameter, createProperty, createResponse, createSchema } from './factory.ts'
2
- import type { InputNode } from './nodes/root.ts'
1
+ import { createInput } from './nodes/input.ts'
2
+ import type { InputNode } from './nodes/input.ts'
3
+ import { createOperation } from './nodes/operation.ts'
4
+ import { createParameter } from './nodes/parameter.ts'
5
+ import { createProperty } from './nodes/property.ts'
6
+ import { createResponse } from './nodes/response.ts'
7
+ import { createSchema } from './nodes/schema.ts'
3
8
 
4
9
  /**
5
10
  * Builds a minimal sample AST with one `Pet` schema and one `getPetById` operation.
@@ -49,128 +54,3 @@ export function buildSampleTree(): InputNode {
49
54
 
50
55
  return createInput({ schemas: [petSchema], operations: [operation] })
51
56
  }
52
-
53
- /**
54
- * Builds a PetStore-like fixture AST with:
55
- * - six named schemas (`Pet`, `NewPet`, `PetList`, `Error`, `PetOrError`, `FullPet`)
56
- * - two operations (`listPets`, `createPet`)
57
- */
58
- export function buildFixture(): InputNode {
59
- const refPet = createSchema({ type: 'ref', ref: 'Pet' })
60
- const refError = createSchema({ type: 'ref', ref: 'Error' })
61
-
62
- return createInput({
63
- schemas: [
64
- createSchema({
65
- name: 'Pet',
66
- type: 'object',
67
- properties: [
68
- createProperty({
69
- name: 'id',
70
- schema: createSchema({ type: 'integer' }),
71
- required: true,
72
- }),
73
- createProperty({
74
- name: 'name',
75
- schema: createSchema({ type: 'string' }),
76
- required: true,
77
- }),
78
- ],
79
- }),
80
- createSchema({
81
- name: 'NewPet',
82
- type: 'object',
83
- properties: [
84
- createProperty({
85
- name: 'name',
86
- schema: createSchema({ type: 'string' }),
87
- required: true,
88
- }),
89
- ],
90
- }),
91
- createSchema({
92
- name: 'PetList',
93
- type: 'array',
94
- items: [refPet],
95
- }),
96
- createSchema({
97
- name: 'Error',
98
- type: 'object',
99
- properties: [
100
- createProperty({
101
- name: 'code',
102
- schema: createSchema({ type: 'integer' }),
103
- required: true,
104
- }),
105
- createProperty({
106
- name: 'message',
107
- schema: createSchema({ type: 'string' }),
108
- required: true,
109
- }),
110
- ],
111
- }),
112
- createSchema({
113
- name: 'PetOrError',
114
- type: 'union',
115
- members: [refPet, refError],
116
- }),
117
- createSchema({
118
- name: 'FullPet',
119
- type: 'intersection',
120
- members: [
121
- refPet,
122
- createSchema({
123
- type: 'object',
124
- properties: [
125
- createProperty({
126
- name: 'createdAt',
127
- schema: createSchema({ type: 'datetime' }),
128
- }),
129
- ],
130
- }),
131
- ],
132
- }),
133
- ],
134
- operations: [
135
- createOperation({
136
- operationId: 'listPets',
137
- method: 'GET',
138
- path: '/pets',
139
- tags: ['pets'],
140
- parameters: [
141
- createParameter({
142
- name: 'limit',
143
- in: 'query',
144
- schema: createSchema({ type: 'integer' }),
145
- }),
146
- ],
147
- responses: [
148
- createResponse({
149
- statusCode: '200',
150
- schema: createSchema({ type: 'ref', ref: 'PetList' }),
151
- mediaType: 'application/json',
152
- }),
153
- createResponse({
154
- statusCode: '400',
155
- schema: refError,
156
- mediaType: 'application/json',
157
- }),
158
- ],
159
- }),
160
- createOperation({
161
- operationId: 'createPet',
162
- method: 'POST',
163
- path: '/pets',
164
- tags: ['pets'],
165
- requestBody: { content: [{ contentType: 'application/json', schema: createSchema({ type: 'ref', ref: 'NewPet' }) }] },
166
- responses: [
167
- createResponse({
168
- statusCode: '201',
169
- schema: refPet,
170
- mediaType: 'application/json',
171
- }),
172
- ],
173
- }),
174
- ],
175
- })
176
- }
package/src/node.ts ADDED
@@ -0,0 +1,128 @@
1
+ import type { BaseNode, NodeKind } from './nodes/base.ts'
2
+ import type { SchemaNode } from './nodes/index.ts'
3
+
4
+ /**
5
+ * Visitor callback names, one per traversable node kind. Kept in sync with the
6
+ * keys of `Visitor` in `visitor.ts`.
7
+ */
8
+ type VisitorKey = 'input' | 'output' | 'operation' | 'schema' | 'property' | 'parameter' | 'response'
9
+
10
+ /**
11
+ * Distributive `Omit` that preserves each member of a union.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * type A = { kind: 'a'; keep: string; drop: number }
16
+ * type B = { kind: 'b'; keep: boolean; drop: number }
17
+ * type Result = DistributiveOmit<A | B, 'drop'>
18
+ * // -> { kind: 'a'; keep: string } | { kind: 'b'; keep: boolean }
19
+ * ```
20
+ */
21
+ export type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never
22
+
23
+ /**
24
+ * Builds a type guard that matches nodes of the given `kind`.
25
+ */
26
+ function isKind<T extends BaseNode>(kind: NodeKind) {
27
+ return (node: unknown): node is T => (node as BaseNode).kind === kind
28
+ }
29
+
30
+ /**
31
+ * Updates a schema's `optional` and `nullish` flags from a parent's `required`
32
+ * value and the schema's own `nullable`. Mirrors how OpenAPI parameters and
33
+ * object properties combine "required" and "nullable" into a single AST.
34
+ *
35
+ * - Non-required + non-nullable → `optional: true`.
36
+ * - Non-required + nullable → `nullish: true`.
37
+ * - Required → both flags cleared.
38
+ */
39
+ export function syncOptionality(schema: SchemaNode, required: boolean): SchemaNode {
40
+ const nullable = schema.nullable ?? false
41
+
42
+ return {
43
+ ...schema,
44
+ optional: !required && !nullable ? true : undefined,
45
+ nullish: !required && nullable ? true : undefined,
46
+ }
47
+ }
48
+
49
+ /**
50
+ * The single definition derived from one {@link defineNode} call: the node's
51
+ * `create` builder, its `is` guard, and the traversal metadata the registry
52
+ * collects into the visitor tables.
53
+ */
54
+ export type NodeDef<TNode extends BaseNode = BaseNode, TInput = never> = {
55
+ /**
56
+ * Node discriminator this definition owns.
57
+ */
58
+ kind: NodeKind
59
+ /**
60
+ * Builds a node from its input, applying `defaults` and the optional `build` hook.
61
+ */
62
+ create: (input: TInput) => TNode
63
+ /**
64
+ * Type guard matching this node kind.
65
+ */
66
+ is: (node: unknown) => node is TNode
67
+ /**
68
+ * Child node fields in traversal order. Feeds `VISITOR_KEYS`.
69
+ */
70
+ children?: ReadonlyArray<string>
71
+ /**
72
+ * Visitor callback name. Feeds `VISITOR_KEY_BY_KIND`.
73
+ */
74
+ visitorKey?: VisitorKey
75
+ /**
76
+ * When `true`, `create` is rerun after children are rebuilt so computed fields
77
+ * stay in sync. Feeds `nodeRebuilders`.
78
+ */
79
+ rebuild?: boolean
80
+ }
81
+
82
+ type DefineNodeConfig<TNode extends BaseNode, TInput, TBuilt extends object> = {
83
+ kind: TNode['kind']
84
+ defaults?: Partial<TNode>
85
+ build?: (input: TInput) => TBuilt
86
+ children?: ReadonlyArray<string>
87
+ visitorKey?: VisitorKey
88
+ rebuild?: boolean
89
+ }
90
+
91
+ /**
92
+ * Defines a node once and derives its `create` builder, `is` guard, and traversal
93
+ * metadata. `create` merges `defaults`, the `build` hook (or the raw input), and the
94
+ * `kind`, so node construction lives in one place without scattered `as` casts.
95
+ *
96
+ * Set `rebuild: true` when the `build` hook derives fields from children. After a
97
+ * transform rewrites those children, the registry reruns `create` so the derived
98
+ * fields stay correct.
99
+ *
100
+ * @example Simple node
101
+ * ```ts
102
+ * const importDef = defineNode<ImportNode>({ kind: 'Import' })
103
+ * const createImport = importDef.create
104
+ * ```
105
+ *
106
+ * @example Node with a build hook that is rerun on transform
107
+ * ```ts
108
+ * const propertyDef = defineNode<PropertyNode, UserPropertyNode>({
109
+ * kind: 'Property',
110
+ * build: (props) => ({ ...props, required: props.required ?? false }),
111
+ * children: ['schema'],
112
+ * visitorKey: 'property',
113
+ * rebuild: true,
114
+ * })
115
+ * ```
116
+ */
117
+ export function defineNode<TNode extends BaseNode, TInput = Omit<TNode, 'kind'>, TBuilt extends object = Omit<TNode, 'kind'>>(
118
+ config: DefineNodeConfig<TNode, TInput, TBuilt>,
119
+ ): NodeDef<TNode, TInput> {
120
+ const { kind, defaults, build, children, visitorKey, rebuild } = config
121
+
122
+ function create(input: TInput): TNode {
123
+ const base = build ? build(input) : input
124
+ return { ...defaults, ...(base as object), kind } as TNode
125
+ }
126
+
127
+ return { kind, create, is: isKind<TNode>(kind), children, visitorKey, rebuild }
128
+ }
package/src/nodes/base.ts CHANGED
@@ -14,11 +14,14 @@ export type NodeKind =
14
14
  | 'Property'
15
15
  | 'Parameter'
16
16
  | 'Response'
17
+ | 'RequestBody'
18
+ | 'Content'
17
19
  | 'FunctionParameter'
18
- | 'ParameterGroup'
19
20
  | 'FunctionParameters'
21
+ | 'TypeLiteral'
22
+ | 'IndexedAccessType'
23
+ | 'ObjectBindingPattern'
20
24
  | 'Type'
21
- | 'ParamsType'
22
25
  | 'File'
23
26
  | 'Import'
24
27
  | 'Export'
@@ -44,13 +47,3 @@ export type BaseNode = {
44
47
  */
45
48
  kind: NodeKind
46
49
  }
47
-
48
- /**
49
- * Minimal node type when only `kind` is needed.
50
- *
51
- * @example
52
- * ```ts
53
- * const node: Node = { kind: 'Operation' }
54
- * ```
55
- */
56
- export type Node = BaseNode