@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/adapter-oas",
3
- "version": "5.0.0-alpha.43",
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": "^31.1.2",
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"
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 = `#/components/schemas/${name}`
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: requestBodyDescription,
738
- schema: ast.syncOptionality(requestBodySchemaNode, requestBodyRequired),
739
- keysToOmit: requestBodyKeysToOmit?.length ? requestBodyKeysToOmit : undefined,
740
- required: requestBodyRequired || undefined,
741
- contentType: requestBodyContentType,
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 = typeof responseObj === 'object' && responseObj !== null && !Array.isArray(responseObj) ? responseObj.description : undefined
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: keysToOmit?.length ? keysToOmit : undefined,
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
- contentTypes.forEach((mt: string) => {
132
- if (!availableContentType && matchesMimeType.json(mt)) {
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
- contentTypes.forEach((mt: string) => {
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
- Object.keys(operation.schema.responses).forEach((key) => {
164
- const schema = operation.schema.responses![key]
165
- const $ref = isReference(schema) ? schema.$ref : undefined
166
-
167
- if (schema && $ref) {
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 isPlainFragment = (item: SchemaObject) => !Object.keys(item).some((key) => structuralKeys.has(key as 'properties'))
262
- if (!schema.allOf.every((item) => isPlainFragment(item as SchemaObject))) {
263
- return schema
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 schema.allOf as SchemaObject[]) {
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 [key, value] of Object.entries(schema)) {
323
+ for (const key in schema) {
324
+ const value = (schema as Record<string, unknown>)[key]
315
325
  if (key === '$ref' && typeof value === 'string') {
316
- const match = value.match(/^#\/components\/schemas\/(.+)$/)
317
- if (match) refs.add(match[1]!)
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 = items.length === 1 ? '' : multipleSources(items) ? getSemanticSuffix(item.source) : index === 0 ? '' : String(index + 1)
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)