@kubb/adapter-oas 5.0.0-alpha.42 → 5.0.0-alpha.44

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.42",
3
+ "version": "5.0.0-alpha.44",
4
4
  "description": "OpenAPI / Swagger adapter for Kubb converts OAS input into a @kubb/ast RootNode.",
5
5
  "keywords": [
6
6
  "openapi",
@@ -37,18 +37,14 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "@redocly/openapi-core": "^2.28.1",
40
- "@stoplight/yaml": "^4.3.0",
41
- "fflate": "^0.8.2",
42
- "jsonpointer": "^5.0.1",
43
40
  "oas": "^31.1.2",
44
41
  "oas-normalize": "^16.0.4",
45
- "openapi-types": "^12.1.3",
46
- "remeda": "^2.33.7",
47
42
  "swagger2openapi": "^7.0.8",
48
- "@kubb/core": "5.0.0-alpha.42"
43
+ "@kubb/core": "5.0.0-alpha.44"
49
44
  },
50
45
  "devDependencies": {
51
46
  "@types/swagger2openapi": "^7.0.4",
47
+ "openapi-types": "^12.1.3",
52
48
  "@internals/utils": "0.0.0"
53
49
  },
54
50
  "engines": {
@@ -60,6 +56,9 @@
60
56
  },
61
57
  "main": "./dist/index.cjs",
62
58
  "module": "./dist/index.js",
59
+ "inlinedDependencies": {
60
+ "openapi-types": "12.1.3"
61
+ },
63
62
  "scripts": {
64
63
  "build": "tsdown",
65
64
  "clean": "npx rimraf ./dist",
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/factory.ts CHANGED
@@ -1,10 +1,8 @@
1
1
  import path from 'node:path'
2
- import { URLPath } from '@internals/utils'
2
+ import { mergeDeep, URLPath } from '@internals/utils'
3
3
  import type { AdapterSource } from '@kubb/core'
4
4
  import { bundle, loadConfig } from '@redocly/openapi-core'
5
- import yaml from '@stoplight/yaml'
6
5
  import OASNormalize from 'oas-normalize'
7
- import { mergeDeep } from 'remeda'
8
6
  import swagger2openapi from 'swagger2openapi'
9
7
  import { MERGE_DEFAULT_TITLE, MERGE_DEFAULT_VERSION, MERGE_OPENAPI_VERSION } from './constants.ts'
10
8
  import { isOpenApiV2Document } from './guards.ts'
@@ -60,7 +58,7 @@ export async function parseDocument(pathOrApi: string | Document, { canBundle =
60
58
  /**
61
59
  * Deep-merges multiple OpenAPI documents into a single `Document`.
62
60
  *
63
- * Each document is parsed independently then recursively merged with `remeda`'s `mergeDeep`.
61
+ * Each document is parsed independently then recursively merged via `mergeDeep` from `@internals/utils`.
64
62
  * Throws when the input array is empty.
65
63
  *
66
64
  * @example
@@ -85,9 +83,12 @@ export async function mergeDocuments(pathOrApi: Array<string | Document>): Promi
85
83
  components: { schemas: {} },
86
84
  } as Document
87
85
 
88
- const merged = documents.reduce((acc, current) => mergeDeep(acc, current as Document), seed)
86
+ const merged = documents.reduce(
87
+ (acc, current) => mergeDeep(acc as Record<string, unknown>, current as Record<string, unknown>),
88
+ seed as Record<string, unknown>,
89
+ )
89
90
 
90
- return parseDocument(merged)
91
+ return parseDocument(merged as Document)
91
92
  }
92
93
 
93
94
  /**
@@ -110,12 +111,7 @@ export function parseFromConfig(source: AdapterSource): Promise<Document> {
110
111
  return parseDocument(structuredClone(source.data) as Document)
111
112
  }
112
113
 
113
- try {
114
- const api: string = yaml.parse(source.data as string)
115
- return parseDocument(api)
116
- } catch {
117
- return parseDocument(source.data as string)
118
- }
114
+ return parseDocument(source.data as string, { canBundle: false })
119
115
  }
120
116
 
121
117
  if (source.type === 'paths') {
package/src/guards.ts CHANGED
@@ -1,5 +1,5 @@
1
+ import { isPlainObject } from '@internals/utils'
1
2
  import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
2
- import { isPlainObject } from 'remeda'
3
3
  import type { DiscriminatorObject, SchemaObject } from './types.ts'
4
4
 
5
5
  /**
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { adapterOas, adapterOasName } from './adapter.ts'
2
+ export type { ParseOptions, ValidateDocumentOptions } from './factory.ts'
2
3
  export { mergeDocuments, parseDocument, parseFromConfig, validateDocument } from './factory.ts'
3
- export { HttpMethods } from './types.ts'
4
4
  export type {
5
5
  AdapterOas,
6
6
  AdapterOasOptions,
@@ -15,4 +15,4 @@ export type {
15
15
  ResponseObject,
16
16
  SchemaObject,
17
17
  } from './types.ts'
18
- export type { ParseOptions, ValidateDocumentOptions } from './factory.ts'
18
+ export { HttpMethods } from './types.ts'
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) {
@@ -404,18 +404,24 @@ function createSchemaParser(ctx: OasParserContext) {
404
404
  | 'number'
405
405
  | 'boolean'
406
406
  | 'string'
407
- const sourceValues = extensionKey
408
- ? [...new Set((schema as Record<string, unknown>)[extensionKey] as Array<string | number>)]
409
- : [...new Set(filteredValues)]
407
+ const rawEnumNames = extensionKey ? ((schema as Record<string, unknown>)[extensionKey] as Array<string | number>) : undefined
408
+ const uniqueValues = [...new Set(filteredValues)]
409
+ const seenNames = new Set<string>()
410
410
 
411
411
  return ast.createSchema({
412
412
  ...enumBase,
413
413
  primitive: enumPrimitiveType,
414
- namedEnumValues: sourceValues.map((label, index) => ({
415
- name: String(label),
416
- value: extensionKey ? (filteredValues[index] ?? label) : label,
417
- primitive: enumPrimitiveType,
418
- })),
414
+ namedEnumValues: uniqueValues
415
+ .map((value, index) => ({
416
+ name: String(rawEnumNames?.[index] ?? value),
417
+ value,
418
+ primitive: enumPrimitiveType,
419
+ }))
420
+ .filter((entry) => {
421
+ if (seenNames.has(entry.name)) return false
422
+ seenNames.add(entry.name)
423
+ return true
424
+ }),
419
425
  })
420
426
  }
421
427
 
@@ -691,6 +697,49 @@ function createSchemaParser(ctx: OasParserContext) {
691
697
  })
692
698
  }
693
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
+
694
743
  /**
695
744
  * Converts an OAS `Operation` into an `OperationNode`.
696
745
  */
@@ -701,38 +750,15 @@ function createSchemaParser(ctx: OasParserContext) {
701
750
 
702
751
  const requestBodySchema = getRequestSchema(document, operation, { contentType: ctx.contentType })
703
752
  const requestBodySchemaNode = requestBodySchema ? parseSchema({ schema: requestBodySchema }, options) : undefined
704
-
705
- const requestBodyDescription =
706
- operation.schema.requestBody && !isReference(operation.schema.requestBody)
707
- ? (operation.schema.requestBody as { description?: string }).description
708
- : undefined
709
-
710
- const requestBodyKeysToOmit = requestBodySchema?.properties
711
- ? Object.entries(requestBodySchema.properties)
712
- .filter(([, prop]) => !isReference(prop) && (prop as { readOnly?: boolean }).readOnly)
713
- .map(([key]) => key)
714
- : undefined
715
-
716
- const requestBodyRequired =
717
- operation.schema.requestBody && !isReference(operation.schema.requestBody)
718
- ? (operation.schema.requestBody as { required?: boolean }).required === true
719
- : false
720
-
721
- const requestBodyContentType = (() => {
722
- if (!operation.schema.requestBody || isReference(operation.schema.requestBody)) {
723
- return undefined
724
- }
725
- const content = (operation.schema.requestBody as { content?: Record<string, unknown> }).content
726
- return content ? Object.keys(content)[0] : undefined
727
- })()
753
+ const requestBodyMeta = getRequestBodyMeta(operation)
728
754
 
729
755
  const requestBody = requestBodySchemaNode
730
756
  ? {
731
- description: requestBodyDescription,
732
- schema: ast.syncOptionality(requestBodySchemaNode, requestBodyRequired),
733
- keysToOmit: requestBodyKeysToOmit?.length ? requestBodyKeysToOmit : undefined,
734
- required: requestBodyRequired || undefined,
735
- 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,
736
762
  }
737
763
  : undefined
738
764
 
@@ -745,27 +771,15 @@ function createSchemaParser(ctx: OasParserContext) {
745
771
  ? parseSchema({ schema: responseSchema }, options)
746
772
  : ast.createSchema({ type: typeOptionMap.get(options.emptySchemaType)! })
747
773
 
748
- const description = typeof responseObj === 'object' && responseObj !== null && !Array.isArray(responseObj) ? responseObj.description : undefined
749
-
750
- const rawContent =
751
- typeof responseObj === 'object' && responseObj !== null && !Array.isArray(responseObj)
752
- ? (responseObj as { content?: Record<string, unknown> }).content
753
- : undefined
754
-
755
- const mediaType = rawContent ? getMediaType(Object.keys(rawContent)[0] ?? '') : getMediaType(operation.contentType ?? '')
756
-
757
- const keysToOmit = responseSchema?.properties
758
- ? Object.entries(responseSchema.properties)
759
- .filter(([, prop]) => !isReference(prop) && (prop as { writeOnly?: boolean }).writeOnly)
760
- .map(([key]) => key)
761
- : undefined
774
+ const { description, content } = getResponseMeta(responseObj)
775
+ const mediaType = content ? getMediaType(Object.keys(content)[0] ?? '') : getMediaType(operation.contentType ?? '')
762
776
 
763
777
  return ast.createResponse({
764
778
  statusCode: statusCode as ast.StatusCode,
765
779
  description,
766
780
  schema,
767
781
  mediaType,
768
- keysToOmit: keysToOmit?.length ? keysToOmit : undefined,
782
+ keysToOmit: collectPropertyKeysByFlag(responseSchema, 'writeOnly'),
769
783
  })
770
784
  })
771
785
 
package/src/refs.ts CHANGED
@@ -1,4 +1,3 @@
1
- import jsonpointer from 'jsonpointer'
2
1
  import { isReference } from './guards.ts'
3
2
  import type { Document } from './types.ts'
4
3
 
@@ -24,7 +23,10 @@ export function resolveRef<T = unknown>(document: Document, $ref: string): T | n
24
23
  } else {
25
24
  return null
26
25
  }
27
- const current = jsonpointer.get(document, $ref)
26
+ const current = $ref
27
+ .split('/')
28
+ .filter(Boolean)
29
+ .reduce((obj: unknown, key: string) => (obj as Record<string, unknown>)?.[key], document as unknown)
28
30
 
29
31
  if (!current) {
30
32
  throw new Error(`Could not find a definition for ${origRef}.`)
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)