@kubb/adapter-oas 5.0.0-alpha.43 → 5.0.0-alpha.45
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 +111 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +111 -45
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/constants.ts +12 -0
- package/src/parser.ts +54 -46
- package/src/resolvers.ts +48 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/adapter-oas",
|
|
3
|
-
"version": "5.0.0-alpha.
|
|
3
|
+
"version": "5.0.0-alpha.45",
|
|
4
4
|
"description": "OpenAPI / Swagger adapter for Kubb converts OAS input into a @kubb/ast RootNode.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openapi",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@redocly/openapi-core": "^2.28.1",
|
|
40
|
-
"oas": "^
|
|
40
|
+
"oas": "^32.1.14",
|
|
41
41
|
"oas-normalize": "^16.0.4",
|
|
42
42
|
"swagger2openapi": "^7.0.8",
|
|
43
|
-
"@kubb/core": "5.0.0-alpha.
|
|
43
|
+
"@kubb/core": "5.0.0-alpha.45"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/swagger2openapi": "^7.0.4",
|
package/src/constants.ts
CHANGED
|
@@ -19,6 +19,18 @@ export const DEFAULT_PARSER_OPTIONS = {
|
|
|
19
19
|
enumSuffix: 'enum',
|
|
20
20
|
} as const satisfies ast.ParserOptions
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* JSON-Pointer prefix for schemas declared under `components.schemas` in an OpenAPI document.
|
|
24
|
+
*
|
|
25
|
+
* Used when building or parsing `$ref` strings.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* `${SCHEMA_REF_PREFIX}Pet` // '#/components/schemas/Pet'
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export const SCHEMA_REF_PREFIX = '#/components/schemas/' as const
|
|
33
|
+
|
|
22
34
|
/**
|
|
23
35
|
* OpenAPI version string written into the stub document created during multi-spec merges.
|
|
24
36
|
*/
|
package/src/parser.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { URLPath } from '@internals/utils'
|
|
2
2
|
import { ast } from '@kubb/core'
|
|
3
3
|
import BaseOas from 'oas'
|
|
4
|
-
import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, typeOptionMap } from './constants.ts'
|
|
4
|
+
import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, SCHEMA_REF_PREFIX, typeOptionMap } from './constants.ts'
|
|
5
5
|
import { isDiscriminator, isNullable, isReference } from './guards.ts'
|
|
6
6
|
import { resolveRef } from './refs.ts'
|
|
7
7
|
import {
|
|
@@ -148,7 +148,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
148
148
|
if (!deref || !isDiscriminator(deref)) return true
|
|
149
149
|
const parentUnion = deref.oneOf ?? deref.anyOf
|
|
150
150
|
if (!parentUnion) return true
|
|
151
|
-
const childRef =
|
|
151
|
+
const childRef = `${SCHEMA_REF_PREFIX}${name}`
|
|
152
152
|
const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef)
|
|
153
153
|
const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef)
|
|
154
154
|
if (inOneOf || inMapping) {
|
|
@@ -697,6 +697,49 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
697
697
|
})
|
|
698
698
|
}
|
|
699
699
|
|
|
700
|
+
/**
|
|
701
|
+
* Reads the inline `requestBody` metadata (description / required / contentType) that OAS exposes
|
|
702
|
+
* outside the schema itself. Returns an empty object when the request body is missing or a `$ref`.
|
|
703
|
+
*/
|
|
704
|
+
function getRequestBodyMeta(operation: Operation): { description?: string; required: boolean; contentType?: string } {
|
|
705
|
+
const body = operation.schema.requestBody
|
|
706
|
+
if (!body || isReference(body)) return { required: false }
|
|
707
|
+
|
|
708
|
+
const inline = body as { description?: string; required?: boolean; content?: Record<string, unknown> }
|
|
709
|
+
return {
|
|
710
|
+
description: inline.description,
|
|
711
|
+
required: inline.required === true,
|
|
712
|
+
contentType: inline.content ? Object.keys(inline.content)[0] : undefined,
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Reads the inline response object (not a `$ref`) and returns its description plus its `content` map.
|
|
718
|
+
*/
|
|
719
|
+
function getResponseMeta(responseObj: unknown): { description?: string; content?: Record<string, unknown> } {
|
|
720
|
+
if (typeof responseObj !== 'object' || responseObj === null || Array.isArray(responseObj)) return {}
|
|
721
|
+
|
|
722
|
+
const inline = responseObj as { description?: string; content?: Record<string, unknown> }
|
|
723
|
+
return { description: inline.description, content: inline.content }
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Collects property names whose schema has a truthy boolean flag (`readOnly` or `writeOnly`).
|
|
728
|
+
* `$ref` entries are skipped since their flags live on the dereferenced target.
|
|
729
|
+
*/
|
|
730
|
+
function collectPropertyKeysByFlag(schema: SchemaObject | null, flag: 'readOnly' | 'writeOnly'): string[] | undefined {
|
|
731
|
+
if (!schema?.properties) return undefined
|
|
732
|
+
|
|
733
|
+
const keys: string[] = []
|
|
734
|
+
for (const key in schema.properties) {
|
|
735
|
+
const prop = schema.properties[key]
|
|
736
|
+
if (prop && !isReference(prop) && (prop as Record<string, unknown>)[flag]) {
|
|
737
|
+
keys.push(key)
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return keys.length ? keys : undefined
|
|
741
|
+
}
|
|
742
|
+
|
|
700
743
|
/**
|
|
701
744
|
* Converts an OAS `Operation` into an `OperationNode`.
|
|
702
745
|
*/
|
|
@@ -707,38 +750,15 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
707
750
|
|
|
708
751
|
const requestBodySchema = getRequestSchema(document, operation, { contentType: ctx.contentType })
|
|
709
752
|
const requestBodySchemaNode = requestBodySchema ? parseSchema({ schema: requestBodySchema }, options) : undefined
|
|
710
|
-
|
|
711
|
-
const requestBodyDescription =
|
|
712
|
-
operation.schema.requestBody && !isReference(operation.schema.requestBody)
|
|
713
|
-
? (operation.schema.requestBody as { description?: string }).description
|
|
714
|
-
: undefined
|
|
715
|
-
|
|
716
|
-
const requestBodyKeysToOmit = requestBodySchema?.properties
|
|
717
|
-
? Object.entries(requestBodySchema.properties)
|
|
718
|
-
.filter(([, prop]) => !isReference(prop) && (prop as { readOnly?: boolean }).readOnly)
|
|
719
|
-
.map(([key]) => key)
|
|
720
|
-
: undefined
|
|
721
|
-
|
|
722
|
-
const requestBodyRequired =
|
|
723
|
-
operation.schema.requestBody && !isReference(operation.schema.requestBody)
|
|
724
|
-
? (operation.schema.requestBody as { required?: boolean }).required === true
|
|
725
|
-
: false
|
|
726
|
-
|
|
727
|
-
const requestBodyContentType = (() => {
|
|
728
|
-
if (!operation.schema.requestBody || isReference(operation.schema.requestBody)) {
|
|
729
|
-
return undefined
|
|
730
|
-
}
|
|
731
|
-
const content = (operation.schema.requestBody as { content?: Record<string, unknown> }).content
|
|
732
|
-
return content ? Object.keys(content)[0] : undefined
|
|
733
|
-
})()
|
|
753
|
+
const requestBodyMeta = getRequestBodyMeta(operation)
|
|
734
754
|
|
|
735
755
|
const requestBody = requestBodySchemaNode
|
|
736
756
|
? {
|
|
737
|
-
description:
|
|
738
|
-
schema: ast.syncOptionality(requestBodySchemaNode,
|
|
739
|
-
keysToOmit:
|
|
740
|
-
required:
|
|
741
|
-
contentType:
|
|
757
|
+
description: requestBodyMeta.description,
|
|
758
|
+
schema: ast.syncOptionality(requestBodySchemaNode, requestBodyMeta.required),
|
|
759
|
+
keysToOmit: collectPropertyKeysByFlag(requestBodySchema, 'readOnly'),
|
|
760
|
+
required: requestBodyMeta.required || undefined,
|
|
761
|
+
contentType: requestBodyMeta.contentType,
|
|
742
762
|
}
|
|
743
763
|
: undefined
|
|
744
764
|
|
|
@@ -751,27 +771,15 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
751
771
|
? parseSchema({ schema: responseSchema }, options)
|
|
752
772
|
: ast.createSchema({ type: typeOptionMap.get(options.emptySchemaType)! })
|
|
753
773
|
|
|
754
|
-
const description
|
|
755
|
-
|
|
756
|
-
const rawContent =
|
|
757
|
-
typeof responseObj === 'object' && responseObj !== null && !Array.isArray(responseObj)
|
|
758
|
-
? (responseObj as { content?: Record<string, unknown> }).content
|
|
759
|
-
: undefined
|
|
760
|
-
|
|
761
|
-
const mediaType = rawContent ? getMediaType(Object.keys(rawContent)[0] ?? '') : getMediaType(operation.contentType ?? '')
|
|
762
|
-
|
|
763
|
-
const keysToOmit = responseSchema?.properties
|
|
764
|
-
? Object.entries(responseSchema.properties)
|
|
765
|
-
.filter(([, prop]) => !isReference(prop) && (prop as { writeOnly?: boolean }).writeOnly)
|
|
766
|
-
.map(([key]) => key)
|
|
767
|
-
: undefined
|
|
774
|
+
const { description, content } = getResponseMeta(responseObj)
|
|
775
|
+
const mediaType = content ? getMediaType(Object.keys(content)[0] ?? '') : getMediaType(operation.contentType ?? '')
|
|
768
776
|
|
|
769
777
|
return ast.createResponse({
|
|
770
778
|
statusCode: statusCode as ast.StatusCode,
|
|
771
779
|
description,
|
|
772
780
|
schema,
|
|
773
781
|
mediaType,
|
|
774
|
-
keysToOmit:
|
|
782
|
+
keysToOmit: collectPropertyKeysByFlag(responseSchema, 'writeOnly'),
|
|
775
783
|
})
|
|
776
784
|
})
|
|
777
785
|
|
package/src/resolvers.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { ast } from '@kubb/core'
|
|
|
3
3
|
import type { ParameterObject, ServerObject } from 'oas/types'
|
|
4
4
|
import { isRef } from 'oas/types'
|
|
5
5
|
import { matchesMimeType } from 'oas/utils'
|
|
6
|
-
import { formatMap, structuralKeys } from './constants.ts'
|
|
6
|
+
import { formatMap, SCHEMA_REF_PREFIX, structuralKeys } from './constants.ts'
|
|
7
7
|
import { isReference } from './guards.ts'
|
|
8
8
|
import { dereferenceWithRef, resolveRef } from './refs.ts'
|
|
9
9
|
import type { ContentType, Document, MediaTypeObject, Operation, ResponseObject, SchemaObject } from './types.ts'
|
|
@@ -128,16 +128,15 @@ function getResponseBody(responseBody: boolean | ResponseObject, contentType?: s
|
|
|
128
128
|
|
|
129
129
|
let availableContentType: string | undefined
|
|
130
130
|
const contentTypes = Object.keys(body.content)
|
|
131
|
-
|
|
132
|
-
if (
|
|
131
|
+
for (const mt of contentTypes) {
|
|
132
|
+
if (matchesMimeType.json(mt)) {
|
|
133
133
|
availableContentType = mt
|
|
134
|
+
break
|
|
134
135
|
}
|
|
135
|
-
}
|
|
136
|
+
}
|
|
136
137
|
|
|
137
138
|
if (!availableContentType) {
|
|
138
|
-
|
|
139
|
-
if (!availableContentType) availableContentType = mt
|
|
140
|
-
})
|
|
139
|
+
availableContentType = contentTypes[0]
|
|
141
140
|
}
|
|
142
141
|
|
|
143
142
|
if (availableContentType) {
|
|
@@ -160,14 +159,13 @@ function getResponseBody(responseBody: boolean | ResponseObject, contentType?: s
|
|
|
160
159
|
*/
|
|
161
160
|
export function getResponseSchema(document: Document, operation: Operation, statusCode: string | number, options: OperationsOptions = {}): SchemaObject {
|
|
162
161
|
if (operation.schema.responses) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
operation.schema.responses![key] = resolveRef<any>(document, $ref)
|
|
162
|
+
const responses = operation.schema.responses
|
|
163
|
+
for (const key in responses) {
|
|
164
|
+
const schema = responses[key]
|
|
165
|
+
if (schema && isReference(schema)) {
|
|
166
|
+
responses[key] = resolveRef<any>(document, schema.$ref)
|
|
169
167
|
}
|
|
170
|
-
}
|
|
168
|
+
}
|
|
171
169
|
}
|
|
172
170
|
|
|
173
171
|
const responseBody = getResponseBody(operation.getResponseByStatusCode(statusCode), options.contentType)
|
|
@@ -254,19 +252,30 @@ export type GetSchemasResult = {
|
|
|
254
252
|
* // returned unchanged — contains a $ref
|
|
255
253
|
* ```
|
|
256
254
|
*/
|
|
255
|
+
/**
|
|
256
|
+
* Returns `true` when `fragment` carries any JSON Schema keyword that makes it
|
|
257
|
+
* structurally significant on its own (see `structuralKeys`).
|
|
258
|
+
*
|
|
259
|
+
* A fragment with a structural keyword can't be safely merged into a parent schema.
|
|
260
|
+
*/
|
|
261
|
+
function hasStructuralKeywords(fragment: SchemaObject): boolean {
|
|
262
|
+
for (const key in fragment) {
|
|
263
|
+
if (structuralKeys.has(key as 'properties')) return true
|
|
264
|
+
}
|
|
265
|
+
return false
|
|
266
|
+
}
|
|
267
|
+
|
|
257
268
|
export function flattenSchema(schema: SchemaObject | null): SchemaObject | null {
|
|
258
269
|
if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null
|
|
259
|
-
if (schema.allOf.some((item) => isRef(item))) return schema
|
|
260
270
|
|
|
261
|
-
const
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
}
|
|
271
|
+
const allOfFragments = schema.allOf as SchemaObject[]
|
|
272
|
+
if (allOfFragments.some((item) => isRef(item))) return schema
|
|
273
|
+
if (allOfFragments.some(hasStructuralKeywords)) return schema
|
|
265
274
|
|
|
266
275
|
const merged: SchemaObject = { ...schema }
|
|
267
276
|
delete merged.allOf
|
|
268
277
|
|
|
269
|
-
for (const fragment of
|
|
278
|
+
for (const fragment of allOfFragments) {
|
|
270
279
|
for (const [key, value] of Object.entries(fragment)) {
|
|
271
280
|
if (merged[key as keyof typeof merged] === undefined) {
|
|
272
281
|
merged[key as keyof typeof merged] = value
|
|
@@ -311,10 +320,13 @@ function collectRefs(schema: unknown, refs = new Set<string>()): Set<string> {
|
|
|
311
320
|
}
|
|
312
321
|
|
|
313
322
|
if (schema && typeof schema === 'object') {
|
|
314
|
-
for (const
|
|
323
|
+
for (const key in schema) {
|
|
324
|
+
const value = (schema as Record<string, unknown>)[key]
|
|
315
325
|
if (key === '$ref' && typeof value === 'string') {
|
|
316
|
-
|
|
317
|
-
|
|
326
|
+
if (value.startsWith(SCHEMA_REF_PREFIX)) {
|
|
327
|
+
const name = value.slice(SCHEMA_REF_PREFIX.length)
|
|
328
|
+
if (name) refs.add(name)
|
|
329
|
+
}
|
|
318
330
|
} else {
|
|
319
331
|
collectRefs(value, refs)
|
|
320
332
|
}
|
|
@@ -424,11 +436,22 @@ export function getSchemas(document: Document, { contentType }: GetSchemasOption
|
|
|
424
436
|
|
|
425
437
|
const schemas: Record<string, SchemaObject> = {}
|
|
426
438
|
const nameMapping = new Map<string, string>()
|
|
427
|
-
const multipleSources = (items: SchemaWithMetadata[]) => new Set(items.map((i) => i.source)).size > 1
|
|
428
439
|
|
|
429
440
|
for (const [, items] of normalizedNames) {
|
|
441
|
+
const isSingle = items.length === 1
|
|
442
|
+
let hasMultipleSources = false
|
|
443
|
+
if (!isSingle) {
|
|
444
|
+
const firstSource = items[0]!.source
|
|
445
|
+
for (let i = 1; i < items.length; i++) {
|
|
446
|
+
if (items[i]!.source !== firstSource) {
|
|
447
|
+
hasMultipleSources = true
|
|
448
|
+
break
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
430
453
|
items.forEach((item, index) => {
|
|
431
|
-
const suffix =
|
|
454
|
+
const suffix = isSingle ? '' : hasMultipleSources ? getSemanticSuffix(item.source) : index === 0 ? '' : String(index + 1)
|
|
432
455
|
const uniqueName = item.originalName + suffix
|
|
433
456
|
schemas[uniqueName] = item.schema
|
|
434
457
|
nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName)
|