@kubb/adapter-oas 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 +100 -0
- package/dist/index.cjs +1459 -613
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +821 -63
- package/dist/index.js +1461 -610
- package/dist/index.js.map +1 -1
- package/package.json +12 -8
- package/src/adapter.bench.ts +60 -0
- package/src/adapter.ts +133 -56
- package/src/bundler.ts +71 -0
- package/src/constants.ts +17 -3
- package/src/dialect.ts +38 -0
- package/src/discriminator.ts +68 -44
- package/src/factory.ts +63 -48
- package/src/index.ts +0 -3
- package/src/mime.ts +22 -0
- package/src/operation.ts +194 -0
- package/src/parser.ts +335 -214
- package/src/refs.ts +31 -5
- package/src/resolvers.ts +86 -48
- package/src/schemaDiagnostics.ts +76 -0
- package/src/stream.ts +283 -0
- package/src/types.ts +82 -53
- package/extension.yaml +0 -344
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
package/src/discriminator.ts
CHANGED
|
@@ -1,29 +1,22 @@
|
|
|
1
1
|
import { ast } from '@kubb/core'
|
|
2
|
+
import type { SchemaNodeByType } from '@kubb/ast'
|
|
2
3
|
|
|
3
|
-
type DiscriminatorTarget = {
|
|
4
|
+
export type DiscriminatorTarget = {
|
|
4
5
|
propertyName: string
|
|
5
6
|
enumValues: Array<string | number | boolean>
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
+
* Builds a map of child schema names → discriminator patch data by scanning the given
|
|
11
|
+
* top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* child object schema.
|
|
14
|
-
*
|
|
15
|
-
* Returns a new `InputNode` — the original is never mutated.
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```ts
|
|
19
|
-
* const { root } = parseOas(document, options)
|
|
20
|
-
* const next = applyDiscriminatorInheritance(root)
|
|
21
|
-
* ```
|
|
13
|
+
* The streaming path calls this on a small pre-parsed subset of schemas (only the
|
|
14
|
+
* discriminator parents) rather than on all schemas at once.
|
|
22
15
|
*/
|
|
23
|
-
export function
|
|
16
|
+
export function buildDiscriminatorChildMap(schemas: Array<ast.SchemaNode>): Map<string, DiscriminatorTarget> {
|
|
24
17
|
const childMap = new Map<string, DiscriminatorTarget>()
|
|
25
18
|
|
|
26
|
-
for (const schema of
|
|
19
|
+
for (const schema of schemas) {
|
|
27
20
|
// Case 1: top-level schema is a union (oneOf/anyOf with discriminator)
|
|
28
21
|
// Case 2: top-level schema is an intersection wrapping a union (oneOf/anyOf + shared properties)
|
|
29
22
|
let unionNode = ast.narrowSchema(schema, 'union')
|
|
@@ -50,8 +43,8 @@ export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNod
|
|
|
50
43
|
const intersectionNode = ast.narrowSchema(member, 'intersection')
|
|
51
44
|
if (!intersectionNode?.members) continue
|
|
52
45
|
|
|
53
|
-
let refNode:
|
|
54
|
-
let objNode:
|
|
46
|
+
let refNode: SchemaNodeByType['ref'] | null = null
|
|
47
|
+
let objNode: SchemaNodeByType['object'] | null = null
|
|
55
48
|
|
|
56
49
|
for (const m of intersectionNode.members) {
|
|
57
50
|
refNode ??= ast.narrowSchema(m, 'ref')
|
|
@@ -61,48 +54,79 @@ export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNod
|
|
|
61
54
|
if (!refNode?.name || !objNode) continue
|
|
62
55
|
|
|
63
56
|
const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName)
|
|
64
|
-
const enumNode = prop ? ast.narrowSchema(prop.schema, 'enum') :
|
|
57
|
+
const enumNode = prop ? ast.narrowSchema(prop.schema, 'enum') : null
|
|
65
58
|
if (!enumNode?.enumValues?.length) continue
|
|
66
59
|
|
|
67
60
|
const enumValues = enumNode.enumValues.filter((v): v is string | number | boolean => v !== null)
|
|
68
61
|
if (!enumValues.length) continue
|
|
69
62
|
|
|
70
63
|
const existing = childMap.get(refNode.name)
|
|
71
|
-
if (existing) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
childMap.set(refNode.name, {
|
|
75
|
-
propertyName: discriminatorPropertyName,
|
|
76
|
-
enumValues: [...enumValues],
|
|
77
|
-
})
|
|
64
|
+
if (!existing) {
|
|
65
|
+
childMap.set(refNode.name, { propertyName: discriminatorPropertyName, enumValues: [...enumValues] })
|
|
66
|
+
continue
|
|
78
67
|
}
|
|
68
|
+
existing.enumValues.push(...enumValues)
|
|
79
69
|
}
|
|
80
70
|
}
|
|
81
71
|
|
|
82
|
-
|
|
72
|
+
return childMap
|
|
73
|
+
}
|
|
83
74
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Patches a single top-level `SchemaNode` with its discriminator entry (adds or replaces
|
|
77
|
+
* the discriminant property). Used by the streaming path to apply patches inline per yield
|
|
78
|
+
* without buffering all schemas.
|
|
79
|
+
*/
|
|
80
|
+
export function patchDiscriminatorNode(node: ast.SchemaNode, entry: { propertyName: string; enumValues: Array<string | number | boolean> }): ast.SchemaNode {
|
|
81
|
+
const objectNode = ast.narrowSchema(node, 'object')
|
|
82
|
+
if (!objectNode) return node
|
|
83
|
+
|
|
84
|
+
const { propertyName, enumValues } = entry
|
|
85
|
+
const enumSchema = ast.factory.createSchema({ type: 'enum', enumValues })
|
|
86
|
+
const newProp = ast.factory.createProperty({ name: propertyName, required: true, schema: enumSchema })
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName)
|
|
89
|
+
const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => (i === existingIdx ? newProp : p)) : [...objectNode.properties, newProp]
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
return { ...objectNode, properties: newProperties }
|
|
92
|
+
}
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Creates a single-property object schema used as a discriminator literal.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* createDiscriminantNode({ propertyName: 'type', value: 'dog' })
|
|
100
|
+
* // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export function createDiscriminantNode({ propertyName, value }: { propertyName: string; value: string }): ast.SchemaNode {
|
|
104
|
+
return ast.factory.createSchema({
|
|
105
|
+
type: 'object',
|
|
106
|
+
primitive: 'object',
|
|
107
|
+
properties: [
|
|
108
|
+
ast.factory.createProperty({
|
|
97
109
|
name: propertyName,
|
|
110
|
+
schema: ast.factory.createSchema({
|
|
111
|
+
type: 'enum',
|
|
112
|
+
primitive: 'string',
|
|
113
|
+
enumValues: [value],
|
|
114
|
+
}),
|
|
98
115
|
required: true,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName)
|
|
103
|
-
const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => (i === existingIdx ? newProp : p)) : [...objectNode.properties, newProp]
|
|
104
|
-
|
|
105
|
-
return { ...objectNode, properties: newProperties }
|
|
106
|
-
},
|
|
116
|
+
}),
|
|
117
|
+
],
|
|
107
118
|
})
|
|
108
119
|
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export function findDiscriminator(mapping: Record<string, string> | undefined, ref: string | undefined): string | null {
|
|
130
|
+
if (!mapping || !ref) return null
|
|
131
|
+
return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null
|
|
132
|
+
}
|
package/src/factory.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import { mergeDeep,
|
|
2
|
+
import { exists, mergeDeep, Url } from '@internals/utils'
|
|
3
|
+
import { Diagnostics } from '@kubb/core'
|
|
3
4
|
import type { AdapterSource } from '@kubb/core'
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
import { compileErrors, validate } from '@readme/openapi-parser'
|
|
6
|
+
import { parse } from 'yaml'
|
|
7
|
+
import { bundleDocument } from './bundler.ts'
|
|
7
8
|
import { MERGE_DEFAULT_TITLE, MERGE_DEFAULT_VERSION, MERGE_OPENAPI_VERSION } from './constants.ts'
|
|
8
9
|
import { isOpenApiV2Document } from './guards.ts'
|
|
9
10
|
import type { Document } from './types.ts'
|
|
10
11
|
|
|
11
12
|
export type ParseOptions = {
|
|
12
13
|
canBundle?: boolean
|
|
13
|
-
enablePaths?: boolean
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export type ValidateDocumentOptions = {
|
|
@@ -20,9 +20,10 @@ export type ValidateDocumentOptions = {
|
|
|
20
20
|
/**
|
|
21
21
|
* Loads and dereferences an OpenAPI document, returning the raw `Document`.
|
|
22
22
|
*
|
|
23
|
-
* Accepts a file path string or an already-parsed document object. File paths
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* Accepts a file path string or an already-parsed document object. File paths and URLs are
|
|
24
|
+
* bundled via `api-ref-bundler`, hoisting external file schemas into named `components.schemas`
|
|
25
|
+
* entries so generators can emit named types and imports. Swagger 2.0 documents are
|
|
26
|
+
* automatically up-converted to OpenAPI 3.0 via `swagger2openapi`.
|
|
26
27
|
*
|
|
27
28
|
* @example
|
|
28
29
|
* ```ts
|
|
@@ -30,28 +31,19 @@ export type ValidateDocumentOptions = {
|
|
|
30
31
|
* const document = await parse(rawDocumentObject, { canBundle: false })
|
|
31
32
|
* ```
|
|
32
33
|
*/
|
|
33
|
-
export async function parseDocument(pathOrApi: string | Document, { canBundle = true
|
|
34
|
+
export async function parseDocument(pathOrApi: string | Document, { canBundle = true }: ParseOptions = {}): Promise<Document> {
|
|
34
35
|
if (typeof pathOrApi === 'string' && canBundle) {
|
|
35
|
-
const
|
|
36
|
-
const bundleResults = await bundle({
|
|
37
|
-
ref: pathOrApi,
|
|
38
|
-
config,
|
|
39
|
-
base: pathOrApi,
|
|
40
|
-
})
|
|
36
|
+
const bundled = await bundleDocument(pathOrApi)
|
|
41
37
|
|
|
42
|
-
return parseDocument(
|
|
43
|
-
canBundle,
|
|
44
|
-
enablePaths,
|
|
45
|
-
})
|
|
38
|
+
return parseDocument(bundled, { canBundle: false })
|
|
46
39
|
}
|
|
47
40
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
})
|
|
52
|
-
const document = (await oasNormalize.load()) as Document
|
|
41
|
+
// A string here is always inline YAML/JSON content: file paths and URLs are read and parsed by
|
|
42
|
+
// `bundleDocument` first. `yaml.parse` also parses JSON, since JSON is a subset of YAML.
|
|
43
|
+
const document = (typeof pathOrApi === 'string' ? parse(pathOrApi) : pathOrApi) as Document
|
|
53
44
|
|
|
54
45
|
if (isOpenApiV2Document(document)) {
|
|
46
|
+
const { default: swagger2openapi } = await import('swagger2openapi')
|
|
55
47
|
const { openapi } = await swagger2openapi.convertObj(document, {
|
|
56
48
|
anchors: true,
|
|
57
49
|
})
|
|
@@ -65,7 +57,7 @@ export async function parseDocument(pathOrApi: string | Document, { canBundle =
|
|
|
65
57
|
/**
|
|
66
58
|
* Deep-merges multiple OpenAPI documents into a single `Document`.
|
|
67
59
|
*
|
|
68
|
-
* Each document is parsed independently then
|
|
60
|
+
* Each document is parsed independently, then deep-merged into one in array order.
|
|
69
61
|
* Throws when the input array is empty.
|
|
70
62
|
*
|
|
71
63
|
* @example
|
|
@@ -74,13 +66,16 @@ export async function parseDocument(pathOrApi: string | Document, { canBundle =
|
|
|
74
66
|
* ```
|
|
75
67
|
*/
|
|
76
68
|
export async function mergeDocuments(pathOrApi: Array<string | Document>): Promise<Document> {
|
|
77
|
-
const documents:
|
|
78
|
-
for (const p of pathOrApi) {
|
|
79
|
-
documents.push(await parseDocument(p, { enablePaths: false, canBundle: false }))
|
|
80
|
-
}
|
|
69
|
+
const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, { canBundle: false })))
|
|
81
70
|
|
|
82
71
|
if (documents.length === 0) {
|
|
83
|
-
throw new Error(
|
|
72
|
+
throw new Diagnostics.Error({
|
|
73
|
+
code: Diagnostics.code.inputRequired,
|
|
74
|
+
severity: 'error',
|
|
75
|
+
message: 'No OAS documents were provided for merging.',
|
|
76
|
+
help: 'Pass at least one path or document to `input.path`.',
|
|
77
|
+
location: { kind: 'config' },
|
|
78
|
+
})
|
|
84
79
|
}
|
|
85
80
|
|
|
86
81
|
const seed: Document = {
|
|
@@ -102,9 +97,9 @@ export async function mergeDocuments(pathOrApi: Array<string | Document>): Promi
|
|
|
102
97
|
* Creates a `Document` from an `AdapterSource`.
|
|
103
98
|
*
|
|
104
99
|
* Handles all three source types:
|
|
105
|
-
* - `{ type: 'path' }`
|
|
106
|
-
* - `{ type: 'paths' }`
|
|
107
|
-
* - `{ type: 'data' }`
|
|
100
|
+
* - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
|
|
101
|
+
* - `{ type: 'paths' }` merges multiple file paths into a single document.
|
|
102
|
+
* - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
|
|
108
103
|
*
|
|
109
104
|
* @example
|
|
110
105
|
* ```ts
|
|
@@ -112,7 +107,7 @@ export async function mergeDocuments(pathOrApi: Array<string | Document>): Promi
|
|
|
112
107
|
* const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
|
|
113
108
|
* ```
|
|
114
109
|
*/
|
|
115
|
-
export function parseFromConfig(source: AdapterSource): Promise<Document> {
|
|
110
|
+
export async function parseFromConfig(source: AdapterSource): Promise<Document> {
|
|
116
111
|
if (source.type === 'data') {
|
|
117
112
|
if (typeof source.data === 'object') {
|
|
118
113
|
return parseDocument(structuredClone(source.data) as Document)
|
|
@@ -126,15 +121,37 @@ export function parseFromConfig(source: AdapterSource): Promise<Document> {
|
|
|
126
121
|
}
|
|
127
122
|
|
|
128
123
|
// type === 'path'
|
|
129
|
-
if (
|
|
124
|
+
if (Url.canParse(source.path)) {
|
|
130
125
|
return parseDocument(source.path)
|
|
131
126
|
}
|
|
132
127
|
|
|
133
|
-
|
|
128
|
+
const resolved = path.resolve(path.dirname(source.path), source.path)
|
|
129
|
+
await assertInputExists(resolved)
|
|
130
|
+
return parseDocument(resolved)
|
|
134
131
|
}
|
|
135
132
|
|
|
136
133
|
/**
|
|
137
|
-
*
|
|
134
|
+
* Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
|
|
135
|
+
* URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
|
|
136
|
+
* its parse error instead.
|
|
137
|
+
*/
|
|
138
|
+
export async function assertInputExists(input: string): Promise<void> {
|
|
139
|
+
if (Url.canParse(input)) {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
if (!(await exists(input))) {
|
|
143
|
+
throw new Diagnostics.Error({
|
|
144
|
+
code: Diagnostics.code.inputNotFound,
|
|
145
|
+
severity: 'error',
|
|
146
|
+
message: `Cannot read the file set in \`input.path\` (or via \`kubb generate PATH\`): ${input}`,
|
|
147
|
+
help: 'Check that the path exists and is readable, then set it in `input.path` or pass it as `kubb generate PATH`.',
|
|
148
|
+
location: { kind: 'config' },
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Validates an OpenAPI document using `@readme/openapi-parser` with colorized error output.
|
|
138
155
|
*
|
|
139
156
|
* @example
|
|
140
157
|
* ```ts
|
|
@@ -143,23 +160,21 @@ export function parseFromConfig(source: AdapterSource): Promise<Document> {
|
|
|
143
160
|
*/
|
|
144
161
|
export async function validateDocument(document: Document, { throwOnError = false }: ValidateDocumentOptions = {}): Promise<void> {
|
|
145
162
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
await oasNormalize.validate({
|
|
152
|
-
parser: {
|
|
153
|
-
validate: {
|
|
154
|
-
errors: { colorize: true },
|
|
155
|
-
},
|
|
163
|
+
// `validate` dereferences its input in place, so clone to keep the cached document intact.
|
|
164
|
+
const result = await validate(structuredClone(document), {
|
|
165
|
+
validate: {
|
|
166
|
+
errors: { colorize: true },
|
|
156
167
|
},
|
|
157
168
|
})
|
|
169
|
+
|
|
170
|
+
if (!result.valid) {
|
|
171
|
+
throw new Error(compileErrors(result))
|
|
172
|
+
}
|
|
158
173
|
} catch (error) {
|
|
159
174
|
if (throwOnError) {
|
|
160
175
|
throw error
|
|
161
176
|
}
|
|
162
177
|
|
|
163
|
-
// Validation failures are non-fatal
|
|
178
|
+
// Validation failures are non-fatal, mirror plugin-oas behavior
|
|
164
179
|
}
|
|
165
180
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
export { adapterOas, adapterOasName } from './adapter.ts'
|
|
2
|
-
export type { ValidateDocumentOptions } from './factory.ts'
|
|
3
|
-
export { mergeDocuments } from './factory.ts'
|
|
4
2
|
export type {
|
|
5
3
|
AdapterOas,
|
|
6
4
|
AdapterOasOptions,
|
|
@@ -15,4 +13,3 @@ export type {
|
|
|
15
13
|
ResponseObject,
|
|
16
14
|
SchemaObject,
|
|
17
15
|
} from './types.ts'
|
|
18
|
-
export { HttpMethods } from './types.ts'
|
package/src/mime.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MIME type fragments that mark a media type as JSON-like.
|
|
3
|
+
*
|
|
4
|
+
* Matches the substrings used by the `oas` library's `matchesMimeType.json`, so a content type is
|
|
5
|
+
* JSON when it contains any of these. The `+json` entry catches structured-syntax suffixes such as
|
|
6
|
+
* `application/vnd.api+json`.
|
|
7
|
+
*/
|
|
8
|
+
const jsonMimeFragments = ['application/json', 'application/x-json', 'text/json', 'text/x-json', '+json'] as const
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns `true` when a media type string is JSON-like.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* isJsonMimeType('application/json') // true
|
|
16
|
+
* isJsonMimeType('application/vnd.api+json') // true
|
|
17
|
+
* isJsonMimeType('multipart/form-data') // false
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function isJsonMimeType(mimeType: string): boolean {
|
|
21
|
+
return jsonMimeFragments.some((fragment) => mimeType.includes(fragment))
|
|
22
|
+
}
|
package/src/operation.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { SUPPORTED_METHODS } from './constants.ts'
|
|
2
|
+
import { isReference } from './guards.ts'
|
|
3
|
+
import { isJsonMimeType } from './mime.ts'
|
|
4
|
+
import { resolveRef } from './refs.ts'
|
|
5
|
+
import type { Document, MediaTypeObject, OperationObject, PathItemObject, ReferenceObject, RequestBodyObject, ResponseObject } from './types.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A single OpenAPI operation: its URL path, HTTP method, and the raw operation object.
|
|
9
|
+
*
|
|
10
|
+
* `schema` is a live reference into the document, so any in-place `$ref` resolution the resolvers
|
|
11
|
+
* perform is visible here too.
|
|
12
|
+
*/
|
|
13
|
+
export type Operation = {
|
|
14
|
+
path: string
|
|
15
|
+
method: string
|
|
16
|
+
schema: OperationObject
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The document plus the operation being read. Shared by the request/response accessors so they can
|
|
21
|
+
* resolve `$ref`s against the document.
|
|
22
|
+
*/
|
|
23
|
+
type OperationContext = {
|
|
24
|
+
document: Document
|
|
25
|
+
operation: Operation
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
|
|
30
|
+
* with no leading or trailing dash.
|
|
31
|
+
*/
|
|
32
|
+
function slugify(value: string): string {
|
|
33
|
+
return value
|
|
34
|
+
.replace(/[^a-zA-Z0-9]/g, '-')
|
|
35
|
+
.replace(/-{2,}/g, '-')
|
|
36
|
+
.replace(/^-|-$/g, '')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
|
|
41
|
+
*/
|
|
42
|
+
export function getOperationId({ path, method, schema }: Operation): string {
|
|
43
|
+
const { operationId } = schema
|
|
44
|
+
if (typeof operationId === 'string' && operationId.length > 0) {
|
|
45
|
+
return operationId
|
|
46
|
+
}
|
|
47
|
+
return `${method}_${slugify(path).toLowerCase()}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Returns the declared response status codes, skipping `x-` extensions and non-object entries.
|
|
52
|
+
*/
|
|
53
|
+
export function getResponseStatusCodes({ schema }: Operation): Array<string> {
|
|
54
|
+
const responses = schema.responses as Record<string, unknown> | undefined
|
|
55
|
+
if (!responses || isReference(responses)) {
|
|
56
|
+
return []
|
|
57
|
+
}
|
|
58
|
+
return Object.keys(responses).filter((key) => !key.startsWith('x-') && !!responses[key] && typeof responses[key] === 'object')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns the response object for a status code, resolving a `$ref` in place. `false` when absent.
|
|
63
|
+
*/
|
|
64
|
+
export function getResponseByStatusCode({ document, operation, statusCode }: OperationContext & { statusCode: string | number }): ResponseObject | false {
|
|
65
|
+
const responses = operation.schema.responses as Record<string, ResponseObject | ReferenceObject> | undefined
|
|
66
|
+
if (!responses || isReference(responses)) {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
const response = responses[statusCode]
|
|
70
|
+
if (!response) {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
if (isReference(response)) {
|
|
74
|
+
const resolved = resolveRef<ResponseObject>(document, response.$ref)
|
|
75
|
+
responses[statusCode] = resolved as ResponseObject
|
|
76
|
+
if (!resolved || isReference(resolved)) {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
return resolved
|
|
80
|
+
}
|
|
81
|
+
return response
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolves the request body (dereferencing a `$ref` in place) and returns its content map, or
|
|
86
|
+
* `undefined` when the operation has no request body.
|
|
87
|
+
*/
|
|
88
|
+
function getRequestBodyContent({ document, operation }: OperationContext): Record<string, MediaTypeObject> | undefined {
|
|
89
|
+
const { schema } = operation
|
|
90
|
+
let requestBody = schema.requestBody as RequestBodyObject | ReferenceObject | undefined
|
|
91
|
+
if (!requestBody) {
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
if (isReference(requestBody)) {
|
|
95
|
+
const resolved = resolveRef<RequestBodyObject>(document, requestBody.$ref)
|
|
96
|
+
;(schema as { requestBody?: unknown }).requestBody = resolved
|
|
97
|
+
if (!resolved || isReference(resolved)) {
|
|
98
|
+
return undefined
|
|
99
|
+
}
|
|
100
|
+
requestBody = resolved
|
|
101
|
+
}
|
|
102
|
+
return requestBody.content
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Returns the request body media type. With `mediaType` set, returns that entry or `false`.
|
|
107
|
+
* Otherwise picks the first JSON-like media type, then the first declared one, as a
|
|
108
|
+
* `[mediaType, object]` tuple.
|
|
109
|
+
*/
|
|
110
|
+
export function getRequestContent({
|
|
111
|
+
document,
|
|
112
|
+
operation,
|
|
113
|
+
mediaType,
|
|
114
|
+
}: OperationContext & { mediaType?: string }): MediaTypeObject | false | [string, MediaTypeObject] {
|
|
115
|
+
const content = getRequestBodyContent({ document, operation })
|
|
116
|
+
if (!content) {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
if (mediaType) {
|
|
120
|
+
return mediaType in content ? content[mediaType]! : false
|
|
121
|
+
}
|
|
122
|
+
const mediaTypes = Object.keys(content)
|
|
123
|
+
const available = mediaTypes.find((mt) => isJsonMimeType(mt)) ?? mediaTypes[0]
|
|
124
|
+
return available ? [available, content[available]!] : false
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Returns the primary request content type. Prefers a JSON-like media type (the last one wins,
|
|
129
|
+
* matching the previous behavior), then the first declared one, defaulting to `'application/json'`.
|
|
130
|
+
*/
|
|
131
|
+
export function getRequestContentType({ document, operation }: OperationContext): string {
|
|
132
|
+
const content = getRequestBodyContent({ document, operation })
|
|
133
|
+
const mediaTypes = content ? Object.keys(content) : []
|
|
134
|
+
|
|
135
|
+
let result = mediaTypes[0] ?? 'application/json'
|
|
136
|
+
for (const mt of mediaTypes) {
|
|
137
|
+
if (isJsonMimeType(mt)) {
|
|
138
|
+
result = mt
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return result
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Builds an `Operation` for every supported HTTP method on every path, in document order.
|
|
146
|
+
* `x-` path keys and unresolvable path-item `$ref`s are skipped.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```ts
|
|
150
|
+
* for (const operation of getOperations(document)) {
|
|
151
|
+
* parseOperation(options, operation)
|
|
152
|
+
* }
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export function getOperations(document: Document): Array<Operation> {
|
|
156
|
+
const operations: Array<Operation> = []
|
|
157
|
+
const paths = document.paths
|
|
158
|
+
if (!paths) {
|
|
159
|
+
return operations
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const path of Object.keys(paths)) {
|
|
163
|
+
if (path.startsWith('x-')) {
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let pathItem = paths[path] as PathItemObject | ReferenceObject | undefined
|
|
168
|
+
if (!pathItem) {
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
if (isReference(pathItem)) {
|
|
172
|
+
const resolved = resolveRef<PathItemObject>(document, pathItem.$ref)
|
|
173
|
+
;(paths as Record<string, unknown>)[path] = resolved
|
|
174
|
+
if (!resolved || isReference(resolved)) {
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
pathItem = resolved
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const item = pathItem as Record<string, unknown>
|
|
181
|
+
for (const method of Object.keys(item)) {
|
|
182
|
+
if (!SUPPORTED_METHODS.has(method)) {
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
const schema = item[method]
|
|
186
|
+
if (!schema || typeof schema !== 'object') {
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
operations.push({ path, method, schema: schema as OperationObject })
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return operations
|
|
194
|
+
}
|