@kubb/adapter-oas 5.0.0-alpha.17 → 5.0.0-alpha.18
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 +746 -814
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +72 -125
- package/dist/index.js +745 -813
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/adapter.ts +52 -66
- package/src/constants.ts +56 -16
- package/src/discriminator.ts +99 -0
- package/src/factory.ts +151 -0
- package/src/guards.ts +68 -0
- package/src/parser.ts +154 -362
- package/src/refs.ts +57 -0
- package/src/resolvers.ts +490 -0
- package/src/types.ts +173 -37
- package/src/oas/Oas.ts +0 -615
- package/src/oas/resolveServerUrl.ts +0 -47
- package/src/oas/types.ts +0 -77
- package/src/oas/utils.ts +0 -401
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.18",
|
|
4
4
|
"description": "OpenAPI / Swagger adapter for Kubb — converts OAS input into a @kubb/ast RootNode.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openapi",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"!/**/__snapshots__/**"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@kubb/fabric-core": "0.
|
|
40
|
-
"@redocly/openapi-core": "^2.
|
|
39
|
+
"@kubb/fabric-core": "0.15.1",
|
|
40
|
+
"@redocly/openapi-core": "^2.25.1",
|
|
41
41
|
"@stoplight/yaml": "^4.3.0",
|
|
42
42
|
"fflate": "^0.8.2",
|
|
43
43
|
"jsonpointer": "^5.0.1",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"openapi-types": "^12.1.3",
|
|
47
47
|
"remeda": "^2.33.6",
|
|
48
48
|
"swagger2openapi": "^7.0.8",
|
|
49
|
-
"@kubb/ast": "5.0.0-alpha.
|
|
50
|
-
"@kubb/core": "5.0.0-alpha.
|
|
49
|
+
"@kubb/ast": "5.0.0-alpha.18",
|
|
50
|
+
"@kubb/core": "5.0.0-alpha.18"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/swagger2openapi": "^7.0.4",
|
package/src/adapter.ts
CHANGED
|
@@ -1,37 +1,39 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
1
|
import { collectImports, createRoot } from '@kubb/ast'
|
|
3
|
-
import type { AdapterSource } from '@kubb/core'
|
|
4
2
|
import { createAdapter } from '@kubb/core'
|
|
5
3
|
import { DEFAULT_PARSER_OPTIONS } from './constants.ts'
|
|
6
|
-
import {
|
|
7
|
-
import { parseFromConfig } from './
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
4
|
+
import { applyDiscriminatorInheritance } from './discriminator.ts'
|
|
5
|
+
import { parseFromConfig, validateDocument } from './factory.ts'
|
|
6
|
+
import { parseOas } from './parser.ts'
|
|
7
|
+
import { resolveServerUrl } from './resolvers.ts'
|
|
8
|
+
import type { AdapterOas } from './types.ts'
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Stable string identifier for the OAS adapter used in Kubb's adapter registry.
|
|
12
|
+
*/
|
|
13
|
+
export const adapterOasName = 'oas' satisfies AdapterOas['name']
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
|
-
* Creates
|
|
16
|
+
* Creates the default OpenAPI / Swagger adapter for Kubb.
|
|
15
17
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
+
* Parses the spec, optionally validates it, resolves the base URL, and converts
|
|
19
|
+
* everything into a `RootNode` that downstream plugins consume.
|
|
18
20
|
*
|
|
19
21
|
* @example
|
|
20
22
|
* ```ts
|
|
21
23
|
* import { defineConfig } from '@kubb/core'
|
|
22
24
|
* import { adapterOas } from '@kubb/adapter-oas'
|
|
25
|
+
* import { pluginTs } from '@kubb/plugin-ts'
|
|
23
26
|
*
|
|
24
27
|
* export default defineConfig({
|
|
25
|
-
* adapter: adapterOas({
|
|
26
|
-
* input:
|
|
27
|
-
* plugins: [pluginTs()
|
|
28
|
+
* adapter: adapterOas({ dateType: 'date', serverIndex: 0 }),
|
|
29
|
+
* input: { path: './openapi.yaml' },
|
|
30
|
+
* plugins: [pluginTs()],
|
|
28
31
|
* })
|
|
29
32
|
* ```
|
|
30
33
|
*/
|
|
31
|
-
export const adapterOas = createAdapter<
|
|
34
|
+
export const adapterOas = createAdapter<AdapterOas>((options) => {
|
|
32
35
|
const {
|
|
33
36
|
validate = true,
|
|
34
|
-
oasClass,
|
|
35
37
|
contentType,
|
|
36
38
|
serverIndex,
|
|
37
39
|
serverVariables,
|
|
@@ -43,25 +45,25 @@ export const adapterOas = createAdapter<OasAdapter>((options) => {
|
|
|
43
45
|
emptySchemaType = unknownType || DEFAULT_PARSER_OPTIONS.emptySchemaType,
|
|
44
46
|
} = options
|
|
45
47
|
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
const nameMapping = new Map<string, string>()
|
|
48
|
+
// Let-binding so parse() can replace it with a simple reassignment (no clear+loop).
|
|
49
|
+
let nameMapping = new Map<string, string>()
|
|
49
50
|
|
|
50
51
|
return {
|
|
51
52
|
name: adapterOasName,
|
|
52
|
-
options
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
53
|
+
get options() {
|
|
54
|
+
return {
|
|
55
|
+
validate,
|
|
56
|
+
contentType,
|
|
57
|
+
serverIndex,
|
|
58
|
+
serverVariables,
|
|
59
|
+
discriminator,
|
|
60
|
+
dateType,
|
|
61
|
+
integerType,
|
|
62
|
+
unknownType,
|
|
63
|
+
emptySchemaType,
|
|
64
|
+
enumSuffix,
|
|
65
|
+
nameMapping,
|
|
66
|
+
}
|
|
65
67
|
},
|
|
66
68
|
getImports(node, resolve) {
|
|
67
69
|
return collectImports({
|
|
@@ -70,60 +72,44 @@ export const adapterOas = createAdapter<OasAdapter>((options) => {
|
|
|
70
72
|
resolve: (schemaName) => {
|
|
71
73
|
const result = resolve(schemaName)
|
|
72
74
|
if (!result) return
|
|
75
|
+
|
|
73
76
|
return { name: [result.name], path: result.path }
|
|
74
77
|
},
|
|
75
78
|
})
|
|
76
79
|
},
|
|
77
80
|
async parse(source) {
|
|
78
|
-
const
|
|
79
|
-
const oas = await parseFromConfig(fakeConfig, oasClass)
|
|
80
|
-
|
|
81
|
-
oas.setOptions({ contentType, discriminator })
|
|
81
|
+
const document = await parseFromConfig(source)
|
|
82
82
|
|
|
83
83
|
if (validate) {
|
|
84
|
-
|
|
85
|
-
await oas.validate()
|
|
86
|
-
} catch (_err) {
|
|
87
|
-
// Validation failures are non-fatal — mirror plugin-oas behavior
|
|
88
|
-
}
|
|
84
|
+
await validateDocument(document)
|
|
89
85
|
}
|
|
90
86
|
|
|
91
|
-
const server = serverIndex !== undefined ?
|
|
87
|
+
const server = serverIndex !== undefined ? document.servers?.at(serverIndex) : undefined
|
|
92
88
|
const baseURL = server?.url ? resolveServerUrl(server, serverVariables) : undefined
|
|
93
89
|
|
|
94
|
-
const
|
|
95
|
-
|
|
90
|
+
const { root: parsedRoot, nameMapping: parsedNameMapping } = parseOas(document, {
|
|
91
|
+
contentType,
|
|
92
|
+
dateType,
|
|
93
|
+
integerType,
|
|
94
|
+
unknownType,
|
|
95
|
+
emptySchemaType,
|
|
96
|
+
enumSuffix,
|
|
97
|
+
})
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
99
|
+
const root = discriminator === 'inherit' ? applyDiscriminatorInheritance(parsedRoot) : parsedRoot
|
|
100
|
+
|
|
101
|
+
// This must happen after parseOas() because legacy enum remapping is finalized there.
|
|
102
|
+
nameMapping = parsedNameMapping
|
|
102
103
|
|
|
103
104
|
return createRoot({
|
|
104
105
|
...root,
|
|
105
106
|
meta: {
|
|
106
|
-
title:
|
|
107
|
-
description:
|
|
108
|
-
version:
|
|
107
|
+
title: document.info?.title,
|
|
108
|
+
description: document.info?.description,
|
|
109
|
+
version: document.info?.version,
|
|
109
110
|
baseURL,
|
|
110
111
|
},
|
|
111
112
|
})
|
|
112
113
|
},
|
|
113
114
|
}
|
|
114
115
|
})
|
|
115
|
-
|
|
116
|
-
// TODO: remove once parseFromConfig accepts AdapterSource directly
|
|
117
|
-
function sourceToFakeConfig(source: AdapterSource): Parameters<typeof parseFromConfig>[0] {
|
|
118
|
-
switch (source.type) {
|
|
119
|
-
case 'path':
|
|
120
|
-
return { root: path.dirname(source.path), input: { path: source.path } } as Parameters<typeof parseFromConfig>[0]
|
|
121
|
-
case 'data':
|
|
122
|
-
return { root: process.cwd(), input: { data: source.data } } as Parameters<typeof parseFromConfig>[0]
|
|
123
|
-
case 'paths':
|
|
124
|
-
return {
|
|
125
|
-
root: source.paths[0] ? path.dirname(source.paths[0]) : process.cwd(),
|
|
126
|
-
input: source.paths.map((p) => ({ path: p })),
|
|
127
|
-
} as Parameters<typeof parseFromConfig>[0]
|
|
128
|
-
}
|
|
129
|
-
}
|
package/src/constants.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { schemaTypes } from '@kubb/ast'
|
|
2
|
+
import type { ParserOptions, ScalarSchemaType, SchemaType } from '@kubb/ast/types'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* Default
|
|
5
|
+
* Default parser options applied when no explicit options are provided.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { DEFAULT_PARSER_OPTIONS } from '@kubb/adapter-oas'
|
|
10
|
+
*
|
|
11
|
+
* const parser = createOasParser(oas)
|
|
12
|
+
* const root = parser.parse({ ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
|
|
13
|
+
* ```
|
|
5
14
|
*/
|
|
6
15
|
export const DEFAULT_PARSER_OPTIONS = {
|
|
7
16
|
dateType: 'string',
|
|
@@ -12,36 +21,51 @@ export const DEFAULT_PARSER_OPTIONS = {
|
|
|
12
21
|
} as const satisfies ParserOptions
|
|
13
22
|
|
|
14
23
|
/**
|
|
15
|
-
* OpenAPI version string written into
|
|
24
|
+
* OpenAPI version string written into the stub document created during multi-spec merges.
|
|
16
25
|
*/
|
|
17
26
|
export const MERGE_OPENAPI_VERSION = '3.0.0' as const
|
|
18
27
|
|
|
19
28
|
/**
|
|
20
|
-
* Fallback `info.title`
|
|
29
|
+
* Fallback `info.title` placed in the stub document when merging multiple API files.
|
|
21
30
|
*/
|
|
22
31
|
export const MERGE_DEFAULT_TITLE = 'Merged API' as const
|
|
23
32
|
|
|
24
33
|
/**
|
|
25
|
-
* Fallback `info.version`
|
|
34
|
+
* Fallback `info.version` placed in the stub document when merging multiple API files.
|
|
26
35
|
*/
|
|
27
36
|
export const MERGE_DEFAULT_VERSION = '1.0.0' as const
|
|
28
37
|
|
|
29
38
|
/**
|
|
30
|
-
* JSON Schema keywords that
|
|
31
|
-
*
|
|
32
|
-
*
|
|
39
|
+
* Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
|
|
40
|
+
*
|
|
41
|
+
* A fragment that contains any of these keys carries structural meaning of its own and must stay as a separate
|
|
42
|
+
* intersection member rather than being merged into the parent.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { structuralKeys } from '@kubb/adapter-oas'
|
|
47
|
+
*
|
|
48
|
+
* const isStructural = Object.keys(fragment).some((key) => structuralKeys.has(key))
|
|
49
|
+
* // true when fragment has e.g. 'properties' or 'oneOf'
|
|
50
|
+
* ```
|
|
33
51
|
*/
|
|
34
52
|
export const structuralKeys = new Set(['properties', 'items', 'additionalProperties', 'oneOf', 'anyOf', 'allOf', 'not'] as const)
|
|
35
53
|
|
|
36
54
|
/**
|
|
37
|
-
*
|
|
55
|
+
* Static map from OAS `format` strings to Kubb `SchemaType` values.
|
|
56
|
+
*
|
|
57
|
+
* Only formats whose AST type differs from the OAS `type` field appear here.
|
|
58
|
+
* Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled separately
|
|
59
|
+
* in the parser. `ipv4`, `ipv6`, and `hostname` map to `'url'` as the closest supported scalar.
|
|
38
60
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* import { formatMap } from '@kubb/adapter-oas'
|
|
42
64
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
65
|
+
* formatMap['uuid'] // 'uuid'
|
|
66
|
+
* formatMap['binary'] // 'blob'
|
|
67
|
+
* formatMap['float'] // 'number'
|
|
68
|
+
* ```
|
|
45
69
|
*/
|
|
46
70
|
export const formatMap = {
|
|
47
71
|
uuid: 'uuid',
|
|
@@ -64,7 +88,23 @@ export const formatMap = {
|
|
|
64
88
|
} as const satisfies Record<string, SchemaType>
|
|
65
89
|
|
|
66
90
|
/**
|
|
67
|
-
* Vendor extension keys
|
|
68
|
-
*
|
|
91
|
+
* Vendor extension keys that attach human-readable labels to enum values, checked in priority order.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* import { enumExtensionKeys } from '@kubb/adapter-oas'
|
|
96
|
+
*
|
|
97
|
+
* const key = enumExtensionKeys.find((k) => k in schema) // 'x-enumNames' | 'x-enum-varnames' | undefined
|
|
98
|
+
* ```
|
|
69
99
|
*/
|
|
70
100
|
export const enumExtensionKeys = ['x-enumNames', 'x-enum-varnames'] as const
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Maps `'any' | 'unknown' | 'void'` option strings to their `ScalarSchemaType` constant.
|
|
104
|
+
* Replaces a plain object lookup with a `Map` for explicit key membership testing via `.has()`.
|
|
105
|
+
*/
|
|
106
|
+
export const typeOptionMap = new Map<'any' | 'unknown' | 'void', ScalarSchemaType>([
|
|
107
|
+
['any', schemaTypes.any],
|
|
108
|
+
['unknown', schemaTypes.unknown],
|
|
109
|
+
['void', schemaTypes.void],
|
|
110
|
+
])
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createProperty, createSchema, narrowSchema, transform } from '@kubb/ast'
|
|
2
|
+
import type { RootNode } from '@kubb/ast/types'
|
|
3
|
+
|
|
4
|
+
type DiscriminatorTarget = { propertyName: string; enumValues: Array<string | number | boolean> }
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Injects discriminator enum values into child schemas so they know which value identifies them.
|
|
8
|
+
*
|
|
9
|
+
* Finds every union schema in `root.schemas` that has a `discriminatorPropertyName`, collects the
|
|
10
|
+
* enum value each union member is mapped to, then adds (or replaces) that property on the matching
|
|
11
|
+
* child object schema.
|
|
12
|
+
*
|
|
13
|
+
* Returns a new `RootNode` — the original is never mutated.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const { root } = parseOas(document, options)
|
|
18
|
+
* const next = applyDiscriminatorInheritance(root)
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function applyDiscriminatorInheritance(root: RootNode): RootNode {
|
|
22
|
+
const childMap = new Map<string, DiscriminatorTarget>()
|
|
23
|
+
|
|
24
|
+
for (const schema of root.schemas) {
|
|
25
|
+
// Case 1: top-level schema is a union (oneOf/anyOf with discriminator)
|
|
26
|
+
// Case 2: top-level schema is an intersection wrapping a union (oneOf/anyOf + shared properties)
|
|
27
|
+
let unionNode = narrowSchema(schema, 'union')
|
|
28
|
+
|
|
29
|
+
if (!unionNode) {
|
|
30
|
+
const intersectionMembers = narrowSchema(schema, 'intersection')?.members
|
|
31
|
+
if (intersectionMembers) {
|
|
32
|
+
for (const m of intersectionMembers) {
|
|
33
|
+
const u = narrowSchema(m, 'union')
|
|
34
|
+
if (u) {
|
|
35
|
+
unionNode = u
|
|
36
|
+
break
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!unionNode?.discriminatorPropertyName || !unionNode.members) continue
|
|
43
|
+
|
|
44
|
+
const { discriminatorPropertyName, members } = unionNode
|
|
45
|
+
|
|
46
|
+
for (const member of members) {
|
|
47
|
+
// Members with a discriminant value are intersections: [RefSchemaNode, ObjectSchemaNode]
|
|
48
|
+
const intersectionNode = narrowSchema(member, 'intersection')
|
|
49
|
+
if (!intersectionNode?.members) continue
|
|
50
|
+
|
|
51
|
+
let refNode: ReturnType<typeof narrowSchema<'ref'>> | undefined
|
|
52
|
+
let objNode: ReturnType<typeof narrowSchema<'object'>> | undefined
|
|
53
|
+
|
|
54
|
+
for (const m of intersectionNode.members) {
|
|
55
|
+
refNode ??= narrowSchema(m, 'ref')
|
|
56
|
+
objNode ??= narrowSchema(m, 'object')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!refNode?.name || !objNode) continue
|
|
60
|
+
|
|
61
|
+
const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName)
|
|
62
|
+
const enumNode = prop ? narrowSchema(prop.schema, 'enum') : undefined
|
|
63
|
+
if (!enumNode?.enumValues?.length) continue
|
|
64
|
+
|
|
65
|
+
const enumValues = enumNode.enumValues.filter((v): v is string | number | boolean => v !== null)
|
|
66
|
+
if (!enumValues.length) continue
|
|
67
|
+
|
|
68
|
+
const existing = childMap.get(refNode.name)
|
|
69
|
+
if (existing) {
|
|
70
|
+
existing.enumValues.push(...enumValues)
|
|
71
|
+
} else {
|
|
72
|
+
childMap.set(refNode.name, { propertyName: discriminatorPropertyName, enumValues: [...enumValues] })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (childMap.size === 0) return root
|
|
78
|
+
|
|
79
|
+
return transform(root, {
|
|
80
|
+
schema(node, { parent }) {
|
|
81
|
+
if (parent?.kind !== 'Root' || !node.name) return
|
|
82
|
+
|
|
83
|
+
const entry = childMap.get(node.name)
|
|
84
|
+
if (!entry) return
|
|
85
|
+
|
|
86
|
+
const objectNode = narrowSchema(node, 'object')
|
|
87
|
+
if (!objectNode) return
|
|
88
|
+
|
|
89
|
+
const { propertyName, enumValues } = entry
|
|
90
|
+
const enumSchema = createSchema({ type: 'enum', enumValues })
|
|
91
|
+
const newProp = createProperty({ name: propertyName, required: true, schema: enumSchema })
|
|
92
|
+
|
|
93
|
+
const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName)
|
|
94
|
+
const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => (i === existingIdx ? newProp : p)) : [...objectNode.properties, newProp]
|
|
95
|
+
|
|
96
|
+
return { ...objectNode, properties: newProperties }
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
}
|
package/src/factory.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { URLPath } from '@internals/utils'
|
|
3
|
+
import type { AdapterSource } from '@kubb/core'
|
|
4
|
+
import { bundle, loadConfig } from '@redocly/openapi-core'
|
|
5
|
+
import yaml from '@stoplight/yaml'
|
|
6
|
+
import OASNormalize from 'oas-normalize'
|
|
7
|
+
import { mergeDeep } from 'remeda'
|
|
8
|
+
import swagger2openapi from 'swagger2openapi'
|
|
9
|
+
import { MERGE_DEFAULT_TITLE, MERGE_DEFAULT_VERSION, MERGE_OPENAPI_VERSION } from './constants.ts'
|
|
10
|
+
import { isOpenApiV2Document } from './guards.ts'
|
|
11
|
+
import type { Document } from './types.ts'
|
|
12
|
+
|
|
13
|
+
export type ParseOptions = {
|
|
14
|
+
canBundle?: boolean
|
|
15
|
+
enablePaths?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Loads and dereferences an OpenAPI document, returning the raw `Document`.
|
|
20
|
+
*
|
|
21
|
+
* Accepts a file path string or an already-parsed document object. File paths are bundled via
|
|
22
|
+
* Redocly to resolve external `$ref`s. Swagger 2.0 documents are automatically up-converted
|
|
23
|
+
* to OpenAPI 3.0 via `swagger2openapi`.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const document = await parseDocument('./openapi.yaml')
|
|
28
|
+
* const document = await parse(rawDocumentObject, { canBundle: false })
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export async function parseDocument(pathOrApi: string | Document, { canBundle = true, enablePaths = true }: ParseOptions = {}): Promise<Document> {
|
|
32
|
+
if (typeof pathOrApi === 'string' && canBundle) {
|
|
33
|
+
const config = await loadConfig()
|
|
34
|
+
const bundleResults = await bundle({ ref: pathOrApi, config, base: pathOrApi })
|
|
35
|
+
|
|
36
|
+
return parseDocument(bundleResults.bundle.parsed as string, { canBundle, enablePaths })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const oasNormalize = new OASNormalize(pathOrApi, {
|
|
40
|
+
enablePaths,
|
|
41
|
+
colorizeErrors: true,
|
|
42
|
+
})
|
|
43
|
+
const document = (await oasNormalize.load()) as Document
|
|
44
|
+
|
|
45
|
+
if (isOpenApiV2Document(document)) {
|
|
46
|
+
const { openapi } = await swagger2openapi.convertObj(document, {
|
|
47
|
+
anchors: true,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return openapi as Document
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return document
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Deep-merges multiple OpenAPI documents into a single `Document`.
|
|
58
|
+
*
|
|
59
|
+
* Each document is parsed independently then recursively merged with `remeda`'s `mergeDeep`.
|
|
60
|
+
* Throws when the input array is empty.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* const document = await mergeDocuments(['./pets.yaml', './orders.yaml'])
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export async function mergeDocuments(pathOrApi: Array<string | Document>): Promise<Document> {
|
|
68
|
+
const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, { enablePaths: false, canBundle: false })))
|
|
69
|
+
|
|
70
|
+
if (documents.length === 0) {
|
|
71
|
+
throw new Error('No OAS documents provided for merging.')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const seed: Document = {
|
|
75
|
+
openapi: MERGE_OPENAPI_VERSION,
|
|
76
|
+
info: { title: MERGE_DEFAULT_TITLE, version: MERGE_DEFAULT_VERSION },
|
|
77
|
+
paths: {},
|
|
78
|
+
components: { schemas: {} },
|
|
79
|
+
} as Document
|
|
80
|
+
|
|
81
|
+
const merged = documents.reduce((acc, current) => mergeDeep(acc, current as Document), seed)
|
|
82
|
+
|
|
83
|
+
return parseDocument(merged)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates a `Document` from an `AdapterSource`.
|
|
88
|
+
*
|
|
89
|
+
* Handles all three source types:
|
|
90
|
+
* - `{ type: 'path' }` — resolves and bundles a local file path or remote URL.
|
|
91
|
+
* - `{ type: 'paths' }` — merges multiple file paths into a single document.
|
|
92
|
+
* - `{ type: 'data' }` — parses an inline string (YAML/JSON) or raw object.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* const document = await parseFromConfig({ type: 'path', path: './openapi.yaml' })
|
|
97
|
+
* const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function parseFromConfig(source: AdapterSource): Promise<Document> {
|
|
101
|
+
if (source.type === 'data') {
|
|
102
|
+
if (typeof source.data === 'object') {
|
|
103
|
+
return parseDocument(structuredClone(source.data) as Document)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const api: string = yaml.parse(source.data as string)
|
|
108
|
+
return parseDocument(api)
|
|
109
|
+
} catch {
|
|
110
|
+
return parseDocument(source.data as string)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (source.type === 'paths') {
|
|
115
|
+
return mergeDocuments(source.paths)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// type === 'path'
|
|
119
|
+
if (new URLPath(source.path).isURL) {
|
|
120
|
+
return parseDocument(source.path)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return parseDocument(path.resolve(path.dirname(source.path), source.path))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Validates an OpenAPI document using `oas-normalize` with colorized error output.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* await validateDocument(document)
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export async function validateDocument(document: Document): Promise<void> {
|
|
135
|
+
try {
|
|
136
|
+
const oasNormalize = new OASNormalize(document, {
|
|
137
|
+
enablePaths: true,
|
|
138
|
+
colorizeErrors: true,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
await oasNormalize.validate({
|
|
142
|
+
parser: {
|
|
143
|
+
validate: {
|
|
144
|
+
errors: { colorize: true },
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
} catch (_err) {
|
|
149
|
+
// Validation failures are non-fatal — mirror plugin-oas behavior
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/guards.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
|
|
2
|
+
import { isPlainObject } from 'remeda'
|
|
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
|
+
}
|