@kubb/adapter-oas 5.0.0-beta.62 → 5.0.0-beta.64
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/dist/index.cjs +266 -175
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +267 -176
- package/dist/index.js.map +1 -1
- package/package.json +4 -6
- package/src/adapter.bench.ts +0 -60
- package/src/adapter.ts +0 -207
- package/src/bundler.ts +0 -71
- package/src/constants.ts +0 -136
- package/src/dialect.ts +0 -60
- package/src/discriminator.ts +0 -132
- package/src/factory.ts +0 -180
- package/src/guards.ts +0 -68
- package/src/index.ts +0 -15
- package/src/mime.ts +0 -22
- package/src/operation.ts +0 -194
- package/src/parser.ts +0 -1135
- package/src/refs.ts +0 -85
- package/src/resolvers.ts +0 -582
- package/src/schemaDiagnostics.ts +0 -76
- package/src/stream.ts +0 -264
- package/src/types.ts +0 -235
- /package/dist/{chunk-C0LytTxp.js → rolldown-runtime-C0LytTxp.js} +0 -0
package/src/discriminator.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { ast } from '@kubb/core'
|
|
2
|
-
import type { SchemaNodeByType } from '@kubb/ast'
|
|
3
|
-
|
|
4
|
-
export type DiscriminatorTarget = {
|
|
5
|
-
propertyName: string
|
|
6
|
-
enumValues: Array<string | number | boolean>
|
|
7
|
-
}
|
|
8
|
-
|
|
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`.
|
|
12
|
-
*
|
|
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.
|
|
15
|
-
*/
|
|
16
|
-
export function buildDiscriminatorChildMap(schemas: Array<ast.SchemaNode>): Map<string, DiscriminatorTarget> {
|
|
17
|
-
const childMap = new Map<string, DiscriminatorTarget>()
|
|
18
|
-
|
|
19
|
-
for (const schema of schemas) {
|
|
20
|
-
// Case 1: top-level schema is a union (oneOf/anyOf with discriminator)
|
|
21
|
-
// Case 2: top-level schema is an intersection wrapping a union (oneOf/anyOf + shared properties)
|
|
22
|
-
let unionNode = ast.narrowSchema(schema, 'union')
|
|
23
|
-
|
|
24
|
-
if (!unionNode) {
|
|
25
|
-
const intersectionMembers = ast.narrowSchema(schema, 'intersection')?.members
|
|
26
|
-
if (intersectionMembers) {
|
|
27
|
-
for (const m of intersectionMembers) {
|
|
28
|
-
const u = ast.narrowSchema(m, 'union')
|
|
29
|
-
if (u) {
|
|
30
|
-
unionNode = u
|
|
31
|
-
break
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (!unionNode?.discriminatorPropertyName || !unionNode.members) continue
|
|
38
|
-
|
|
39
|
-
const { discriminatorPropertyName, members } = unionNode
|
|
40
|
-
|
|
41
|
-
for (const member of members) {
|
|
42
|
-
// Members with a discriminant value are intersections: [RefSchemaNode, ObjectSchemaNode]
|
|
43
|
-
const intersectionNode = ast.narrowSchema(member, 'intersection')
|
|
44
|
-
if (!intersectionNode?.members) continue
|
|
45
|
-
|
|
46
|
-
let refNode: SchemaNodeByType['ref'] | null = null
|
|
47
|
-
let objNode: SchemaNodeByType['object'] | null = null
|
|
48
|
-
|
|
49
|
-
for (const m of intersectionNode.members) {
|
|
50
|
-
refNode ??= ast.narrowSchema(m, 'ref')
|
|
51
|
-
objNode ??= ast.narrowSchema(m, 'object')
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (!refNode?.name || !objNode) continue
|
|
55
|
-
|
|
56
|
-
const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName)
|
|
57
|
-
const enumNode = prop ? ast.narrowSchema(prop.schema, 'enum') : null
|
|
58
|
-
if (!enumNode?.enumValues?.length) continue
|
|
59
|
-
|
|
60
|
-
const enumValues = enumNode.enumValues.filter((v): v is string | number | boolean => v !== null)
|
|
61
|
-
if (!enumValues.length) continue
|
|
62
|
-
|
|
63
|
-
const existing = childMap.get(refNode.name)
|
|
64
|
-
if (!existing) {
|
|
65
|
-
childMap.set(refNode.name, { propertyName: discriminatorPropertyName, enumValues: [...enumValues] })
|
|
66
|
-
continue
|
|
67
|
-
}
|
|
68
|
-
existing.enumValues.push(...enumValues)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return childMap
|
|
73
|
-
}
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
return { ...objectNode, properties: newProperties }
|
|
92
|
-
}
|
|
93
|
-
|
|
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({
|
|
109
|
-
name: propertyName,
|
|
110
|
-
schema: ast.factory.createSchema({
|
|
111
|
-
type: 'enum',
|
|
112
|
-
primitive: 'string',
|
|
113
|
-
enumValues: [value],
|
|
114
|
-
}),
|
|
115
|
-
required: true,
|
|
116
|
-
}),
|
|
117
|
-
],
|
|
118
|
-
})
|
|
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
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
|
-
import { exists, mergeDeep, Url } from '@internals/utils'
|
|
3
|
-
import { Diagnostics } from '@kubb/core'
|
|
4
|
-
import type { AdapterSource } from '@kubb/core'
|
|
5
|
-
import { compileErrors, validate } from '@readme/openapi-parser'
|
|
6
|
-
import { parse } from 'yaml'
|
|
7
|
-
import { bundleDocument } from './bundler.ts'
|
|
8
|
-
import { MERGE_DEFAULT_TITLE, MERGE_DEFAULT_VERSION, MERGE_OPENAPI_VERSION } from './constants.ts'
|
|
9
|
-
import { isOpenApiV2Document } from './guards.ts'
|
|
10
|
-
import type { Document } from './types.ts'
|
|
11
|
-
|
|
12
|
-
export type ParseOptions = {
|
|
13
|
-
canBundle?: boolean
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type ValidateDocumentOptions = {
|
|
17
|
-
throwOnError?: boolean
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Loads and dereferences an OpenAPI document, returning the raw `Document`.
|
|
22
|
-
*
|
|
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`.
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```ts
|
|
30
|
-
* const document = await parseDocument('./openapi.yaml')
|
|
31
|
-
* const document = await parse(rawDocumentObject, { canBundle: false })
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
34
|
-
export async function parseDocument(pathOrApi: string | Document, { canBundle = true }: ParseOptions = {}): Promise<Document> {
|
|
35
|
-
if (typeof pathOrApi === 'string' && canBundle) {
|
|
36
|
-
const bundled = await bundleDocument(pathOrApi)
|
|
37
|
-
|
|
38
|
-
return parseDocument(bundled, { canBundle: false })
|
|
39
|
-
}
|
|
40
|
-
|
|
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
|
|
44
|
-
|
|
45
|
-
if (isOpenApiV2Document(document)) {
|
|
46
|
-
const { default: swagger2openapi } = await import('swagger2openapi')
|
|
47
|
-
const { openapi } = await swagger2openapi.convertObj(document, {
|
|
48
|
-
anchors: true,
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
return openapi as Document
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return document
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Deep-merges multiple OpenAPI documents into a single `Document`.
|
|
59
|
-
*
|
|
60
|
-
* Each document is parsed independently, then deep-merged into one in array order.
|
|
61
|
-
* Throws when the input array is empty.
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* ```ts
|
|
65
|
-
* const document = await mergeDocuments(['./pets.yaml', './orders.yaml'])
|
|
66
|
-
* ```
|
|
67
|
-
*/
|
|
68
|
-
export async function mergeDocuments(pathOrApi: Array<string | Document>): Promise<Document> {
|
|
69
|
-
const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, { canBundle: false })))
|
|
70
|
-
|
|
71
|
-
if (documents.length === 0) {
|
|
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
|
-
})
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const seed: Document = {
|
|
82
|
-
openapi: MERGE_OPENAPI_VERSION,
|
|
83
|
-
info: { title: MERGE_DEFAULT_TITLE, version: MERGE_DEFAULT_VERSION },
|
|
84
|
-
paths: {},
|
|
85
|
-
components: { schemas: {} },
|
|
86
|
-
} as Document
|
|
87
|
-
|
|
88
|
-
const merged = documents.reduce(
|
|
89
|
-
(acc, current) => mergeDeep(acc as Record<string, unknown>, current as Record<string, unknown>),
|
|
90
|
-
seed as Record<string, unknown>,
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
return parseDocument(merged as Document)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Creates a `Document` from an `AdapterSource`.
|
|
98
|
-
*
|
|
99
|
-
* Handles all three source types:
|
|
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.
|
|
103
|
-
*
|
|
104
|
-
* @example
|
|
105
|
-
* ```ts
|
|
106
|
-
* const document = await parseFromConfig({ type: 'path', path: './openapi.yaml' })
|
|
107
|
-
* const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
|
|
108
|
-
* ```
|
|
109
|
-
*/
|
|
110
|
-
export async function parseFromConfig(source: AdapterSource): Promise<Document> {
|
|
111
|
-
if (source.type === 'data') {
|
|
112
|
-
if (typeof source.data === 'object') {
|
|
113
|
-
return parseDocument(structuredClone(source.data) as Document)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return parseDocument(source.data as string, { canBundle: false })
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (source.type === 'paths') {
|
|
120
|
-
return mergeDocuments(source.paths)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// type === 'path'
|
|
124
|
-
if (Url.canParse(source.path)) {
|
|
125
|
-
return parseDocument(source.path)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const resolved = path.resolve(path.dirname(source.path), source.path)
|
|
129
|
-
await assertInputExists(resolved)
|
|
130
|
-
return parseDocument(resolved)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
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.
|
|
155
|
-
*
|
|
156
|
-
* @example
|
|
157
|
-
* ```ts
|
|
158
|
-
* await validateDocument(document)
|
|
159
|
-
* ```
|
|
160
|
-
*/
|
|
161
|
-
export async function validateDocument(document: Document, { throwOnError = false }: ValidateDocumentOptions = {}): Promise<void> {
|
|
162
|
-
try {
|
|
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 },
|
|
167
|
-
},
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
if (!result.valid) {
|
|
171
|
-
throw new Error(compileErrors(result))
|
|
172
|
-
}
|
|
173
|
-
} catch (error) {
|
|
174
|
-
if (throwOnError) {
|
|
175
|
-
throw error
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Validation failures are non-fatal, mirror plugin-oas behavior
|
|
179
|
-
}
|
|
180
|
-
}
|
package/src/guards.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { isPlainObject } from '@internals/utils'
|
|
2
|
-
import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
|
|
3
|
-
import type { DiscriminatorObject, SchemaObject } from './types.ts'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Returns `true` when `doc` is a Swagger 2.0 document (no `openapi` key).
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```ts
|
|
10
|
-
* if (isOpenApiV2Document(doc)) {
|
|
11
|
-
* // doc is OpenAPIV2.Document
|
|
12
|
-
* }
|
|
13
|
-
* ```
|
|
14
|
-
*/
|
|
15
|
-
export function isOpenApiV2Document(doc: unknown): doc is OpenAPIV2.Document {
|
|
16
|
-
return !!doc && isPlainObject(doc) && !('openapi' in doc)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Returns `true` when a schema should be treated as nullable.
|
|
21
|
-
*
|
|
22
|
-
* Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
|
|
23
|
-
* `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* ```ts
|
|
27
|
-
* isNullable({ type: 'string', nullable: true }) // true
|
|
28
|
-
* isNullable({ type: ['string', 'null'] }) // true
|
|
29
|
-
* isNullable({ type: 'string' }) // false
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
|
-
export function isNullable(schema?: SchemaObject & { 'x-nullable'?: boolean }): boolean {
|
|
33
|
-
const explicitNullable = schema?.nullable ?? schema?.['x-nullable']
|
|
34
|
-
if (explicitNullable === true) return true
|
|
35
|
-
|
|
36
|
-
const schemaType = schema?.type
|
|
37
|
-
if (schemaType === 'null') return true
|
|
38
|
-
if (Array.isArray(schemaType)) return schemaType.includes('null')
|
|
39
|
-
|
|
40
|
-
return false
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* ```ts
|
|
48
|
-
* isReference({ $ref: '#/components/schemas/Pet' }) // true
|
|
49
|
-
* isReference({ type: 'string' }) // false
|
|
50
|
-
* ```
|
|
51
|
-
*/
|
|
52
|
-
export function isReference(obj?: unknown): obj is OpenAPIV3.ReferenceObject | OpenAPIV3_1.ReferenceObject {
|
|
53
|
-
return !!obj && typeof obj === 'object' && '$ref' in obj
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object.
|
|
58
|
-
*
|
|
59
|
-
* @example
|
|
60
|
-
* ```ts
|
|
61
|
-
* isDiscriminator({ discriminator: { propertyName: 'type', mapping: {} } }) // true
|
|
62
|
-
* isDiscriminator({ discriminator: 'type' }) // false (Swagger 2 string form)
|
|
63
|
-
* ```
|
|
64
|
-
*/
|
|
65
|
-
export function isDiscriminator(obj?: unknown): obj is SchemaObject & { discriminator: DiscriminatorObject } {
|
|
66
|
-
const record = obj as Record<string, unknown>
|
|
67
|
-
return !!obj && !!record['discriminator'] && typeof record['discriminator'] !== 'string'
|
|
68
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export { adapterOas, adapterOasName } from './adapter.ts'
|
|
2
|
-
export type {
|
|
3
|
-
AdapterOas,
|
|
4
|
-
AdapterOasOptions,
|
|
5
|
-
AdapterOasResolvedOptions,
|
|
6
|
-
ContentType,
|
|
7
|
-
DiscriminatorObject,
|
|
8
|
-
Document,
|
|
9
|
-
HttpMethod,
|
|
10
|
-
MediaTypeObject,
|
|
11
|
-
Operation,
|
|
12
|
-
ReferenceObject,
|
|
13
|
-
ResponseObject,
|
|
14
|
-
SchemaObject,
|
|
15
|
-
} from './types.ts'
|
package/src/mime.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
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
|
-
}
|