@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
@@ -0,0 +1,140 @@
1
+ import type { Streamable } from '@internals/utils'
2
+ import { defineNode } from '../node.ts'
3
+ import type { BaseNode } from './base.ts'
4
+ import type { OperationNode } from './operation.ts'
5
+ import type { SchemaNode } from './schema.ts'
6
+
7
+ /**
8
+ * Metadata for an API document, populated by the adapter and available to every generator.
9
+ *
10
+ * All fields are plain JSON-serializable values, no `Set`, no `Map`, no class instances.
11
+ * Computed fields (`circularNames`, `enumNames`) are pre-calculated once during the adapter
12
+ * pre-scan so generators never need to iterate the full schema list themselves.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const meta: InputMeta = { title: 'Pet Store', version: '1.0.0', baseURL: 'https://petstore.swagger.io/v2', circularNames: [], enumNames: [] }
17
+ * ```
18
+ */
19
+ export type InputMeta = {
20
+ /**
21
+ * API title from `info.title` in the source document.
22
+ */
23
+ title?: string
24
+ /**
25
+ * API description from `info.description` in the source document.
26
+ */
27
+ description?: string
28
+ /**
29
+ * API version string from `info.version` in the source document.
30
+ */
31
+ version?: string
32
+ /**
33
+ * Resolved base URL from the first matching server entry in the source document.
34
+ */
35
+ baseURL?: string | null
36
+ /**
37
+ * Names of schemas that participate in a circular reference chain.
38
+ * Computed once during the adapter pre-scan, so a generator never has to
39
+ * call `findCircularSchemas` itself.
40
+ *
41
+ * Convert to a `Set` once at the start of a generator, not per-schema,
42
+ * so lookups stay O(1) without repeated allocations.
43
+ *
44
+ * @example Wrap a circular schema in z.lazy()
45
+ * ```ts
46
+ * const circular = new Set(meta.circularNames)
47
+ * if (circular.has(schema.name)) { ... }
48
+ * ```
49
+ */
50
+ circularNames: ReadonlyArray<string>
51
+ /**
52
+ * Names of schemas whose type is `enum`.
53
+ * Computed once during the adapter pre-scan, so a generator never has to
54
+ * filter the schema list itself.
55
+ *
56
+ * Convert to a `Set` once at the start of a generator when you need repeated
57
+ * membership checks, so each check stays O(1) instead of an array scan.
58
+ *
59
+ * @example Check if a referenced schema is an enum
60
+ * `const enums = new Set(meta.enumNames)`
61
+ * `const isEnum = enums.has(schemaName)`
62
+ */
63
+ enumNames: ReadonlyArray<string>
64
+ }
65
+
66
+ /**
67
+ * Input AST node that contains all schemas and operations for one API document.
68
+ * Produced by the adapter and consumed by all Kubb plugins.
69
+ *
70
+ * `Stream` switches `schemas` and `operations` between eager `Array`s (the default) and lazy
71
+ * `AsyncIterable`s. The streaming variant `InputNode<true>` yields nodes one at a time and makes
72
+ * `meta` optional, since the adapter can emit metadata before the first node is parsed.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * const input: InputNode = {
77
+ * kind: 'Input',
78
+ * schemas: [],
79
+ * operations: [],
80
+ * meta: { circularNames: [], enumNames: [] },
81
+ * }
82
+ * ```
83
+ *
84
+ * @example Streaming variant for large specs
85
+ * ```ts
86
+ * for await (const schema of inputNode.schemas) {
87
+ * // only this one SchemaNode is live here. Previous ones are GC-eligible
88
+ * }
89
+ * ```
90
+ */
91
+ export type InputNode<Stream extends boolean = false> = BaseNode & {
92
+ /**
93
+ * Node kind.
94
+ */
95
+ kind: 'Input'
96
+ /**
97
+ * All schema nodes in the document.
98
+ */
99
+ schemas: Streamable<SchemaNode, Stream>
100
+ /**
101
+ * All operation nodes in the document.
102
+ */
103
+ operations: Streamable<OperationNode, Stream>
104
+ } & (Stream extends true ? { meta?: InputMeta } : { meta: InputMeta })
105
+
106
+ /**
107
+ * Definition for the {@link InputNode}.
108
+ */
109
+ export const inputDef = defineNode<InputNode, Partial<Omit<InputNode, 'kind'>>>({
110
+ kind: 'Input',
111
+ defaults: { schemas: [], operations: [], meta: { circularNames: [], enumNames: [] } },
112
+ children: ['schemas', 'operations'],
113
+ visitorKey: 'input',
114
+ })
115
+
116
+ /**
117
+ * Creates an `InputNode`. Pass `stream: true` for the streaming variant whose `schemas` and
118
+ * `operations` are `AsyncIterable` sources and whose `meta` is optional. Otherwise it builds the
119
+ * eager variant with array `schemas`/`operations` and the defaulted `meta`.
120
+ *
121
+ * @example Eager
122
+ * ```ts
123
+ * const input = createInput()
124
+ * // { kind: 'Input', schemas: [], operations: [] }
125
+ * ```
126
+ *
127
+ * @example Streaming
128
+ * ```ts
129
+ * const node = createInput({ stream: true, schemas: schemasIterable, operations: operationsIterable, meta: { title: 'My API' } })
130
+ * ```
131
+ */
132
+ export function createInput<Stream extends boolean = false>(options: Partial<Omit<InputNode<Stream>, 'kind'>> & { stream?: Stream } = {}): InputNode<Stream> {
133
+ const { stream, ...overrides } = options
134
+ // Streaming inputs carry AsyncIterable sources, so skip the array/meta defaults that
135
+ // inputDef.create applies for the eager variant.
136
+ if (stream) {
137
+ return { kind: 'Input', ...overrides } as InputNode<Stream>
138
+ }
139
+ return inputDef.create(overrides as Partial<Omit<InputNode, 'kind'>>) as InputNode<Stream>
140
+ }
@@ -1,27 +1,20 @@
1
+ import { defineNode } from '../node.ts'
1
2
  import type { BaseNode } from './base.ts'
2
3
  import type { ParameterNode } from './parameter.ts'
4
+ import { createRequestBody, type RequestBodyNode, type UserRequestBody } from './requestBody.ts'
3
5
  import type { ResponseNode } from './response.ts'
4
- import type { SchemaNode } from './schema.ts'
5
6
 
6
7
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'TRACE'
7
8
 
8
9
  /**
9
- * AST node representing one API operation.
10
- *
11
- * @example
12
- * ```ts
13
- * const operation: OperationNode = {
14
- * kind: 'Operation',
15
- * operationId: 'listPets',
16
- * method: 'GET',
17
- * path: '/pets',
18
- * tags: [],
19
- * parameters: [],
20
- * responses: [],
21
- * }
22
- * ```
10
+ * Transport an operation belongs to.
23
11
  */
24
- export type OperationNode = BaseNode & {
12
+ type OperationProtocol = 'http'
13
+
14
+ /**
15
+ * Fields shared by every operation, regardless of transport.
16
+ */
17
+ type OperationNodeBase = BaseNode & {
25
18
  /**
26
19
  * Node kind.
27
20
  */
@@ -30,15 +23,6 @@ export type OperationNode = BaseNode & {
30
23
  * Operation identifier, usually from OpenAPI `operationId`.
31
24
  */
32
25
  operationId: string
33
- /**
34
- * HTTP Method like 'GET'
35
- */
36
- method: HttpMethod
37
- /**
38
- * OpenAPI-style path string, for example `/pets/{petId}`.
39
- * Path parameters retain the `{param}` notation from the original spec.
40
- */
41
- path: string
42
26
  /**
43
27
  * Group labels for the operation.
44
28
  * Usually copied from OpenAPI `tags`.
@@ -57,55 +41,125 @@ export type OperationNode = BaseNode & {
57
41
  */
58
42
  deprecated?: boolean
59
43
  /**
60
- * Parameters that could be used, we have QueryParams, PathParams, HeaderParams and CookieParams
44
+ * Query, path, header, and cookie parameters for the operation.
61
45
  */
62
46
  parameters: Array<ParameterNode>
63
47
  /**
64
- * Request body metadata for the operation.
48
+ * Request body for the operation.
65
49
  */
66
- requestBody?: {
67
- /**
68
- * Human-readable request body description.
69
- */
70
- description?: string
71
- /**
72
- * Whether the request body is required (`requestBody.required: true` in the spec).
73
- * When `false` or absent, the generated `data` parameter should be optional.
74
- */
75
- required?: boolean
76
- /**
77
- * All available content type entries for this request body.
78
- *
79
- * When the adapter `contentType` option is set, this array contains exactly one entry for
80
- * that content type. Otherwise it contains one entry per content type declared in the spec,
81
- * so that plugins can generate code for every variant (e.g. separate hooks for
82
- * `application/json` and `multipart/form-data`).
83
- *
84
- * @example
85
- * ```ts
86
- * // spec has both application/json and multipart/form-data
87
- * requestBody.content[0].contentType // 'application/json'
88
- * requestBody.content[1].contentType // 'multipart/form-data'
89
- * ```
90
- */
91
- content?: Array<{
92
- /**
93
- * The content type for this entry (e.g. `'application/json'`).
94
- */
95
- contentType: string
96
- /**
97
- * Request body schema for this content type.
98
- */
99
- schema?: SchemaNode
100
- /**
101
- * Property keys to exclude from the generated request body type via `Omit<Type, Keys>`.
102
- * Set when a referenced schema has `readOnly` fields that should be omitted in request types.
103
- */
104
- keysToOmit?: Array<string>
105
- }>
106
- }
50
+ requestBody?: RequestBodyNode
107
51
  /**
108
52
  * Operation responses.
109
53
  */
110
54
  responses: Array<ResponseNode>
111
55
  }
56
+
57
+ /**
58
+ * Operation served over HTTP/REST (OpenAPI). `method` and `path` are guaranteed.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * const operation: HttpOperationNode = {
63
+ * kind: 'Operation',
64
+ * operationId: 'listPets',
65
+ * protocol: 'http',
66
+ * method: 'GET',
67
+ * path: '/pets',
68
+ * tags: [],
69
+ * parameters: [],
70
+ * responses: [],
71
+ * }
72
+ * ```
73
+ */
74
+ export type HttpOperationNode = OperationNodeBase & {
75
+ /**
76
+ * Transport the operation belongs to.
77
+ */
78
+ protocol?: 'http'
79
+ /**
80
+ * HTTP method like `'GET'`.
81
+ */
82
+ method: HttpMethod
83
+ /**
84
+ * OpenAPI-style path string, for example `/pets/{petId}`, with `{param}` notation preserved.
85
+ */
86
+ path: string
87
+ }
88
+
89
+ /**
90
+ * Operation for a non-HTTP transport. HTTP-only fields are forbidden.
91
+ */
92
+ export type GenericOperationNode = OperationNodeBase & {
93
+ /**
94
+ * Transport the operation belongs to.
95
+ */
96
+ protocol?: Exclude<OperationProtocol, 'http'>
97
+ method?: never
98
+ path?: never
99
+ }
100
+
101
+ /**
102
+ * AST node representing one API operation.
103
+ *
104
+ * Discriminated on `protocol`: an {@link HttpOperationNode} (`protocol: 'http'`) guarantees
105
+ * `method` and `path`, while a {@link GenericOperationNode} omits them. Narrow with
106
+ * `isHttpOperationNode(node)` or `node.protocol === 'http'` before reading `method`/`path`.
107
+ */
108
+ export type OperationNode = HttpOperationNode | GenericOperationNode
109
+
110
+ type OperationInput = {
111
+ operationId: string
112
+ method?: HttpOperationNode['method']
113
+ path?: HttpOperationNode['path']
114
+ requestBody?: UserRequestBody
115
+ [key: string]: unknown
116
+ }
117
+
118
+ /**
119
+ * Definition for the {@link OperationNode}. HTTP operations (those carrying both
120
+ * `method` and `path`) are tagged with `protocol: 'http'`, and the request body is
121
+ * normalized into a `RequestBodyNode`.
122
+ */
123
+ export const operationDef = defineNode<OperationNode, OperationInput>({
124
+ kind: 'Operation',
125
+ build: (props) => {
126
+ const { requestBody, ...rest } = props
127
+ const isHttp = rest.method !== undefined && rest.path !== undefined
128
+
129
+ return {
130
+ tags: [],
131
+ parameters: [],
132
+ responses: [],
133
+ ...rest,
134
+ ...(isHttp ? { protocol: 'http' as const } : {}),
135
+ requestBody: requestBody ? createRequestBody(requestBody) : undefined,
136
+ }
137
+ },
138
+ children: ['parameters', 'requestBody', 'responses'],
139
+ visitorKey: 'operation',
140
+ })
141
+
142
+ /**
143
+ * Creates an `OperationNode` with default empty arrays for `tags`, `parameters`, and `responses`.
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * const operation = createOperation({ operationId: 'getPetById', method: 'GET', path: '/pet/{petId}' })
148
+ * // tags, parameters, and responses are []
149
+ * ```
150
+ */
151
+ export function createOperation(
152
+ props: Pick<HttpOperationNode, 'operationId' | 'method' | 'path'> &
153
+ Partial<Omit<HttpOperationNode, 'kind' | 'operationId' | 'method' | 'path' | 'requestBody'>> & {
154
+ requestBody?: UserRequestBody
155
+ },
156
+ ): HttpOperationNode
157
+ export function createOperation(
158
+ props: Pick<GenericOperationNode, 'operationId'> &
159
+ Partial<Omit<GenericOperationNode, 'kind' | 'operationId' | 'requestBody'>> & {
160
+ requestBody?: UserRequestBody
161
+ },
162
+ ): GenericOperationNode
163
+ export function createOperation(props: OperationInput): OperationNode {
164
+ return operationDef.create(props)
165
+ }
@@ -1,3 +1,4 @@
1
+ import { defineNode } from '../node.ts'
1
2
  import type { BaseNode } from './base.ts'
2
3
  import type { FileNode } from './file.ts'
3
4
 
@@ -24,3 +25,25 @@ export type OutputNode = BaseNode & {
24
25
  */
25
26
  files: Array<FileNode>
26
27
  }
28
+
29
+ /**
30
+ * Definition for the {@link OutputNode}.
31
+ */
32
+ export const outputDef = defineNode<OutputNode, Partial<Omit<OutputNode, 'kind'>>>({
33
+ kind: 'Output',
34
+ defaults: { files: [] },
35
+ visitorKey: 'output',
36
+ })
37
+
38
+ /**
39
+ * Creates an `OutputNode` with a stable default for `files`.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const output = createOutput()
44
+ * // { kind: 'Output', files: [] }
45
+ * ```
46
+ */
47
+ export function createOutput(overrides: Partial<Omit<OutputNode, 'kind'>> = {}): OutputNode {
48
+ return outputDef.create(overrides)
49
+ }
@@ -1,3 +1,4 @@
1
+ import { defineNode, syncOptionality } from '../node.ts'
1
2
  import type { BaseNode } from './base.ts'
2
3
  import type { SchemaNode } from './schema.ts'
3
4
 
@@ -18,9 +19,6 @@ export type ParameterLocation = 'path' | 'query' | 'header' | 'cookie'
18
19
  * ```
19
20
  */
20
21
  export type ParameterNode = BaseNode & {
21
- /**
22
- * Node kind.
23
- */
24
22
  kind: 'Parameter'
25
23
  /**
26
24
  * Parameter name.
@@ -39,3 +37,35 @@ export type ParameterNode = BaseNode & {
39
37
  */
40
38
  required: boolean
41
39
  }
40
+
41
+ type UserParameterNode = Pick<ParameterNode, 'name' | 'in' | 'schema'> & Partial<Omit<ParameterNode, 'kind' | 'name' | 'in' | 'schema'>>
42
+
43
+ /**
44
+ * Definition for the {@link ParameterNode}. `required` defaults to `false` and the
45
+ * schema's `optional`/`nullish` flags are kept in sync with it.
46
+ */
47
+ export const parameterDef = defineNode<ParameterNode, UserParameterNode>({
48
+ kind: 'Parameter',
49
+ build: (props) => {
50
+ const required = props.required ?? false
51
+ return { ...props, required, schema: syncOptionality(props.schema, required) }
52
+ },
53
+ children: ['schema'],
54
+ visitorKey: 'parameter',
55
+ rebuild: true,
56
+ })
57
+
58
+ /**
59
+ * Creates a `ParameterNode`.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const param = createParameter({
64
+ * name: 'petId',
65
+ * in: 'path',
66
+ * required: true,
67
+ * schema: createSchema({ type: 'string' }),
68
+ * })
69
+ * ```
70
+ */
71
+ export const createParameter = parameterDef.create
@@ -1,3 +1,4 @@
1
+ import { defineNode, syncOptionality } from '../node.ts'
1
2
  import type { BaseNode } from './base.ts'
2
3
  import type { SchemaNode } from './schema.ts'
3
4
 
@@ -15,9 +16,6 @@ import type { SchemaNode } from './schema.ts'
15
16
  * ```
16
17
  */
17
18
  export type PropertyNode = BaseNode & {
18
- /**
19
- * Node kind.
20
- */
21
19
  kind: 'Property'
22
20
  /**
23
21
  * Property key.
@@ -32,3 +30,38 @@ export type PropertyNode = BaseNode & {
32
30
  */
33
31
  required: boolean
34
32
  }
33
+
34
+ /**
35
+ * Loosely-typed property accepted by `createProperty`, with `required` optional.
36
+ */
37
+ export type UserPropertyNode = Pick<PropertyNode, 'name' | 'schema'> & Partial<Omit<PropertyNode, 'kind' | 'name' | 'schema'>>
38
+
39
+ /**
40
+ * Definition for the {@link PropertyNode}. `required` defaults to `false` and the
41
+ * schema's `optional`/`nullish` flags are kept in sync with it.
42
+ */
43
+ export const propertyDef = defineNode<PropertyNode, UserPropertyNode>({
44
+ kind: 'Property',
45
+ build: (props) => {
46
+ const required = props.required ?? false
47
+ return { ...props, required, schema: syncOptionality(props.schema, required) }
48
+ },
49
+ children: ['schema'],
50
+ visitorKey: 'property',
51
+ rebuild: true,
52
+ })
53
+
54
+ /**
55
+ * Creates a `PropertyNode`.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * const property = createProperty({
60
+ * name: 'status',
61
+ * required: true,
62
+ * schema: createSchema({ type: 'string', nullable: true }),
63
+ * })
64
+ * // required=true, no optional/nullish
65
+ * ```
66
+ */
67
+ export const createProperty = propertyDef.create
@@ -0,0 +1,61 @@
1
+ import { defineNode } from '../node.ts'
2
+ import type { BaseNode } from './base.ts'
3
+ import { type ContentNode, createContent, type UserContent } from './content.ts'
4
+
5
+ /**
6
+ * AST node representing an operation request body.
7
+ *
8
+ * Body schemas live exclusively inside the `content` array (one entry per content type),
9
+ * mirroring {@link ResponseNode}.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const requestBody: RequestBodyNode = {
14
+ * kind: 'RequestBody',
15
+ * required: true,
16
+ * content: [{ kind: 'Content', contentType: 'application/json', schema: createSchema({ type: 'string' }) }],
17
+ * }
18
+ * ```
19
+ */
20
+ export type RequestBodyNode = BaseNode & {
21
+ kind: 'RequestBody'
22
+ /**
23
+ * Request body description carried over from the spec.
24
+ */
25
+ description?: string
26
+ /**
27
+ * Whether the request body is required (`requestBody.required: true` in the spec).
28
+ * When `false` or absent, the generated `data` parameter should be optional.
29
+ */
30
+ required?: boolean
31
+ /**
32
+ * Content type entries for this request body.
33
+ *
34
+ * When the adapter `contentType` option is set, this array contains exactly one entry for
35
+ * that content type. Otherwise it contains one entry per content type declared in the spec,
36
+ * so plugins can generate code for every variant (for example, separate hooks for
37
+ * `application/json` and `multipart/form-data`).
38
+ */
39
+ content?: Array<ContentNode>
40
+ }
41
+
42
+ /**
43
+ * Loosely-typed request body accepted by `createOperation`, normalized into a {@link RequestBodyNode}.
44
+ */
45
+ export type UserRequestBody = Omit<RequestBodyNode, 'kind' | 'content'> & {
46
+ content?: Array<UserContent>
47
+ }
48
+
49
+ /**
50
+ * Definition for the {@link RequestBodyNode}, normalizing each content entry into a `ContentNode`.
51
+ */
52
+ export const requestBodyDef = defineNode<RequestBodyNode, UserRequestBody>({
53
+ kind: 'RequestBody',
54
+ build: (props) => ({ ...props, content: props.content?.map(createContent) }),
55
+ children: ['content'],
56
+ })
57
+
58
+ /**
59
+ * Creates a `RequestBodyNode`, normalizing each content entry into a `ContentNode`.
60
+ */
61
+ export const createRequestBody = requestBodyDef.create
@@ -1,16 +1,22 @@
1
+ import { defineNode } from '../node.ts'
1
2
  import type { BaseNode } from './base.ts'
2
- import type { MediaType, StatusCode } from './http.ts'
3
+ import { type ContentNode, createContent, type UserContent } from './content.ts'
4
+ import type { StatusCode } from './http.ts'
3
5
  import type { SchemaNode } from './schema.ts'
4
6
 
5
7
  /**
6
8
  * AST node representing one operation response variant.
7
9
  *
10
+ * Mirrors {@link OperationNode.requestBody}: the response body schemas live exclusively inside
11
+ * the `content` array (one entry per content type), so the same schema is never duplicated at the
12
+ * node root and inside `content`.
13
+ *
8
14
  * @example
9
15
  * ```ts
10
16
  * const response: ResponseNode = {
11
17
  * kind: 'Response',
12
18
  * statusCode: '200',
13
- * schema: createSchema({ type: 'string' }),
19
+ * content: [{ contentType: 'application/json', schema: createSchema({ type: 'string' }) }],
14
20
  * }
15
21
  * ```
16
22
  */
@@ -28,16 +34,55 @@ export type ResponseNode = BaseNode & {
28
34
  */
29
35
  description?: string
30
36
  /**
31
- * Response body schema.
32
- */
33
- schema: SchemaNode
34
- /**
35
- * Response media type.
36
- */
37
- mediaType?: MediaType | null
38
- /**
39
- * Property keys to exclude from the generated type via `Omit<Type, Keys>`.
40
- * Set when a referenced schema has `writeOnly` fields that should not appear in response types.
37
+ * All available content type entries for this response.
38
+ *
39
+ * When the adapter `contentType` option is set, this array contains exactly one entry for that
40
+ * content type. Otherwise it contains one entry per content type declared in the spec, so that
41
+ * plugins can generate a union of response types (e.g. `application/json` and `application/xml`).
42
+ * Body-less responses keep a single entry whose `schema` is the empty/`void` placeholder.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * // spec response declares both application/json and application/xml
47
+ * response.content[0].contentType // 'application/json'
48
+ * response.content[1].contentType // 'application/xml'
49
+ * ```
41
50
  */
42
- keysToOmit?: Array<string>
51
+ content?: Array<ContentNode>
43
52
  }
53
+
54
+ type ResponseInput = Pick<ResponseNode, 'statusCode'> &
55
+ Partial<Omit<ResponseNode, 'kind' | 'statusCode' | 'content'>> & {
56
+ content?: Array<UserContent>
57
+ schema?: SchemaNode
58
+ mediaType?: string | null
59
+ keysToOmit?: Array<string> | null
60
+ }
61
+
62
+ /**
63
+ * Definition for the {@link ResponseNode}. A single legacy `schema` (with optional
64
+ * `mediaType`/`keysToOmit`) is normalized into one `content` entry.
65
+ */
66
+ export const responseDef = defineNode<ResponseNode, ResponseInput>({
67
+ kind: 'Response',
68
+ build: (props) => {
69
+ const { schema, mediaType, keysToOmit, content, ...rest } = props
70
+ const entries = content ?? (schema ? [{ contentType: mediaType ?? 'application/json', schema, keysToOmit: keysToOmit ?? null }] : undefined)
71
+ return { ...rest, content: entries?.map(createContent) }
72
+ },
73
+ children: ['content'],
74
+ visitorKey: 'response',
75
+ })
76
+
77
+ /**
78
+ * Creates a `ResponseNode`.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * const response = createResponse({
83
+ * statusCode: '200',
84
+ * content: [{ contentType: 'application/json', schema: createSchema({ type: 'object', properties: [] }) }],
85
+ * })
86
+ * ```
87
+ */
88
+ export const createResponse = responseDef.create