@kubb/adapter-oas 5.0.0-beta.63 → 5.0.0-beta.65

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/src/dedupe.ts DELETED
@@ -1,237 +0,0 @@
1
- import { containsCircularRef, extractRefName } from '@kubb/ast/utils'
2
- import { ast } from '@kubb/core'
3
- import { SCHEMA_REF_PREFIX } from './constants.ts'
4
-
5
- /**
6
- * The destination a deduplicated shape points at: the shared schema name and the
7
- * synthetic `$ref` path stored on the generated `ref` nodes.
8
- */
9
- type Target = {
10
- name: string
11
- ref: string
12
- }
13
-
14
- /**
15
- * The result of {@link plan}: the shared definitions to prepend to the schema list, plus the
16
- * rewriting behavior that repoints duplicate shapes at their shared target. The lookup maps stay
17
- * private to the closure, so callers interact with one object instead of three collections.
18
- */
19
- export type Plan = {
20
- /**
21
- * New top-level schema definitions created for inline shapes that had no existing named
22
- * component. Nested duplicates inside each definition are already collapsed.
23
- */
24
- extracted: Array<ast.SchemaNode>
25
- /**
26
- * Rewrites an operation or nested schema, replacing every duplicate sub-schema with a `ref` to
27
- * its shared target. Replacing a node prunes its subtree, so nested duplicates inside a replaced
28
- * shape are not visited again. A `ref` to a duplicate top-level schema is repointed at the first
29
- * schema with the same content.
30
- */
31
- apply<T extends ast.Node>(node: T): T
32
- /**
33
- * Rewrites a top-level schema. A schema whose content duplicates a different shared one becomes a
34
- * `ref` alias to it (keeping its own name and docs). Otherwise its nested duplicates collapse
35
- * while its own root is preserved.
36
- */
37
- applyTopLevel(node: ast.SchemaNode): ast.SchemaNode
38
- /**
39
- * Whether a top-level name duplicates an earlier schema with the same content. Such a schema is
40
- * never emitted, since every reference to it is repointed at the first schema with that content.
41
- */
42
- isAlias(name: string): boolean
43
- }
44
-
45
- /**
46
- * Runtime state {@link plan} reads from the current parse: schema names that take part in a
47
- * circular chain (never extracted, extracting them would break the cycle) and the names already in
48
- * use (so extracted definitions resolve collisions).
49
- */
50
- export type DedupeContext = {
51
- circularSchemas: ReadonlySet<string>
52
- usedNames: Set<string>
53
- }
54
-
55
- /**
56
- * Minimum occurrences before a shape is deduplicated.
57
- */
58
- const MIN_OCCURRENCES = 2
59
-
60
- /**
61
- * Builds the shared `ref` replacement for a duplicate occurrence, carrying the
62
- * usage-slot and documentation fields that are not part of the shared type.
63
- */
64
- function createRefNode(node: ast.SchemaNode, target: Target): ast.SchemaNode {
65
- return ast.factory.createSchema({
66
- type: 'ref',
67
- name: target.name,
68
- ref: target.ref,
69
- optional: node.optional,
70
- nullish: node.nullish,
71
- readOnly: node.readOnly,
72
- writeOnly: node.writeOnly,
73
- deprecated: node.deprecated,
74
- description: node.description,
75
- default: node.default,
76
- example: node.example,
77
- })
78
- }
79
-
80
- /**
81
- * Strips usage-slot flags from an extracted definition and applies its name.
82
- * A standalone definition is never optional, so `optional`/`nullish` are cleared.
83
- */
84
- function cleanDefinition(node: ast.SchemaNode, name: string): ast.SchemaNode {
85
- return { ...node, name, optional: undefined, nullish: undefined }
86
- }
87
-
88
- /**
89
- * Returns `true` when a node is eligible for deduplication. Only enums and objects qualify, and
90
- * object shapes that take part in a circular chain are rejected so recursive structures are not
91
- * extracted (which would break the cycle).
92
- */
93
- function isCandidate(node: ast.SchemaNode, circularSchemas: ReadonlySet<string>): boolean {
94
- if (node.type === 'enum') return true
95
- if (node.type !== 'object') return false
96
- if (node.name && circularSchemas.has(node.name)) return false
97
- return !containsCircularRef(node, { circularSchemas })
98
- }
99
-
100
- /**
101
- * Produces the name for an inline shape with no existing named component, resolving
102
- * collisions against `usedNames`. Returns `null` for an unnamed shape, which stays inline.
103
- */
104
- function nameFor(node: ast.SchemaNode, usedNames: Set<string>): string | null {
105
- const base = node.name
106
- if (!base) return null
107
-
108
- let name = base
109
- let counter = 2
110
- while (usedNames.has(name)) {
111
- name = `${base}${counter++}`
112
- }
113
- usedNames.add(name)
114
- return name
115
- }
116
-
117
- /**
118
- * Scans a forest of schema and operation nodes and produces a {@link Plan}.
119
- *
120
- * A shape that occurs at least {@link MIN_OCCURRENCES} times is deduplicated: if any occurrence is
121
- * a named top-level schema, the first one becomes the target (so other top-level duplicates and
122
- * inline copies turn into references to it). Other top-level names with the same content are
123
- * recorded as aliases. Otherwise a new definition is extracted using {@link nameFor}. The returned
124
- * plan rewrites nodes against those decisions.
125
- *
126
- * @example
127
- * ```ts
128
- * const dedupePlan = plan([...schemaNodes, ...operationNodes], { circularSchemas, usedNames })
129
- * ```
130
- */
131
- export function plan(roots: ReadonlyArray<ast.Node>, context: DedupeContext): Plan {
132
- const { circularSchemas, usedNames } = context
133
-
134
- const topLevelNodes = new Set<ast.SchemaNode>()
135
-
136
- type Group = {
137
- count: number
138
- representative: ast.SchemaNode
139
- topLevelNames: Array<string>
140
- }
141
- const groups = new Map<string, Group>()
142
-
143
- for (const root of roots) {
144
- if (root.kind === 'Schema') topLevelNodes.add(root)
145
- for (const schemaNode of ast.collect<ast.SchemaNode>(root, { schema: (node) => node })) {
146
- if (!isCandidate(schemaNode, circularSchemas)) continue
147
-
148
- const signature = ast.signatureOf(schemaNode)
149
- const isTopLevel = topLevelNodes.has(schemaNode) && !!schemaNode.name
150
- const group = groups.get(signature)
151
- if (group) {
152
- group.count++
153
- if (isTopLevel) group.topLevelNames.push(schemaNode.name!)
154
- } else {
155
- groups.set(signature, { count: 1, representative: schemaNode, topLevelNames: isTopLevel ? [schemaNode.name!] : [] })
156
- }
157
- }
158
- }
159
-
160
- const bySignature = new Map<string, Target>()
161
- const byName = new Map<string, Target>()
162
- const pendingExtractions: Array<{ name: string; representative: ast.SchemaNode }> = []
163
-
164
- for (const [signature, group] of groups) {
165
- if (group.count < MIN_OCCURRENCES) continue
166
-
167
- const [firstName, ...duplicateNames] = group.topLevelNames
168
- if (firstName) {
169
- const target: Target = { name: firstName, ref: `${SCHEMA_REF_PREFIX}${firstName}` }
170
- bySignature.set(signature, target)
171
- for (const duplicate of duplicateNames) {
172
- byName.set(duplicate, target)
173
- }
174
- continue
175
- }
176
-
177
- const name = nameFor(group.representative, usedNames)
178
- if (!name) continue
179
-
180
- bySignature.set(signature, { name, ref: `${SCHEMA_REF_PREFIX}${name}` })
181
- pendingExtractions.push({ name, representative: group.representative })
182
- }
183
-
184
- // Rewrites a node against the resolved targets. `skipRootMatch` keeps a definition's own root from
185
- // turning into a reference to itself. Nested duplicates are still collapsed.
186
- function rewrite<T extends ast.Node>(node: T, skipRootMatch: boolean): T {
187
- if (bySignature.size === 0 && byName.size === 0) return node
188
-
189
- const root = node
190
-
191
- return ast.transform(node, {
192
- schema(schemaNode) {
193
- if (schemaNode.type === 'ref') {
194
- const refName = schemaNode.ref ? extractRefName(schemaNode.ref) : schemaNode.name
195
- const target = refName ? byName.get(refName) : undefined
196
-
197
- return target ? { ...schemaNode, name: target.name, ref: target.ref } : undefined
198
- }
199
-
200
- if (skipRootMatch && schemaNode === root) return undefined
201
-
202
- const target = bySignature.get(ast.signatureOf(schemaNode))
203
- if (!target) return undefined
204
-
205
- return createRefNode(schemaNode, target)
206
- },
207
- }) as T
208
- }
209
-
210
- // Build extracted definitions only after every target name is known, so nested
211
- // duplicates inside a definition also resolve to refs.
212
- const extracted = pendingExtractions.map(({ name, representative }) => cleanDefinition(rewrite(representative, true), name))
213
-
214
- return {
215
- extracted,
216
- apply<T extends ast.Node>(node: T): T {
217
- return rewrite(node, false)
218
- },
219
- applyTopLevel(node: ast.SchemaNode): ast.SchemaNode {
220
- const target = bySignature.get(ast.signatureOf(node))
221
- if (target && target.name !== node.name) {
222
- return ast.factory.createSchema({
223
- type: 'ref',
224
- name: node.name ?? null,
225
- ref: target.ref,
226
- description: node.description,
227
- deprecated: node.deprecated,
228
- })
229
- }
230
-
231
- return rewrite(node, true)
232
- },
233
- isAlias(name: string): boolean {
234
- return byName.has(name)
235
- },
236
- }
237
- }
package/src/dialect.ts DELETED
@@ -1,42 +0,0 @@
1
- import { ast } from '@kubb/core'
2
- import { plan } from './dedupe.ts'
3
- import { isDiscriminator, isNullable, isReference } from './guards.ts'
4
- import { resolveRef } from './refs.ts'
5
- import type { SchemaObject } from './types.ts'
6
-
7
- /**
8
- * The OpenAPI / Swagger dialect, the default used by `@kubb/adapter-oas`.
9
- *
10
- * Implements the spec-agnostic {@link ast.SchemaDialect} contract: it isolates the
11
- * decisions that differ between specs (nullability, `$ref`, discriminator, binary,
12
- * ref resolution) so the converter pipeline and dispatch rules stay shared. A
13
- * future adapter (e.g. AsyncAPI) ships its own dialect, `type: ['null', …]`
14
- * nullability, no discriminator object, binary via `contentEncoding` and reuses
15
- * the rest unchanged.
16
- *
17
- * Formats (`uuid`, `email`, dates, …) are intentionally NOT here: they are shared
18
- * JSON Schema vocabulary, so the converters keep that common case.
19
- *
20
- * @example
21
- * ```ts
22
- * const parser = createSchemaParser(context) // uses oasDialect
23
- * const parser = createSchemaParser(context, oasDialect) // explicit
24
- * ```
25
- */
26
- export const oasDialect = ast.defineDialect({
27
- name: 'oas',
28
- schema: {
29
- isNullable,
30
- isReference,
31
- isDiscriminator,
32
- isBinary: (schema: SchemaObject) => schema.type === 'string' && schema.contentMediaType === 'application/octet-stream',
33
- resolveRef,
34
- },
35
- dedupe: { plan },
36
- })
37
-
38
- /**
39
- * The concrete dialect type for `@kubb/adapter-oas`. Keeps the OAS guard predicates
40
- * (`isReference`, `isDiscriminator`) intact so converters narrow schemas after a check.
41
- */
42
- export type OasDialect = typeof oasDialect
@@ -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
- * Maps each child schema name to its 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 bundles 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,21 +0,0 @@
1
- /**
2
- * MIME type fragments that mark a media type as JSON-like.
3
- *
4
- * A content type is JSON when it contains any of these substrings. The `+json` entry catches
5
- * structured-syntax suffixes such as `application/vnd.api+json`.
6
- */
7
- const jsonMimeFragments = ['application/json', 'application/x-json', 'text/json', 'text/x-json', '+json'] as const
8
-
9
- /**
10
- * Returns `true` when a media type string is JSON-like.
11
- *
12
- * @example
13
- * ```ts
14
- * isJsonMimeType('application/json') // true
15
- * isJsonMimeType('application/vnd.api+json') // true
16
- * isJsonMimeType('multipart/form-data') // false
17
- * ```
18
- */
19
- export function isJsonMimeType(mimeType: string): boolean {
20
- return jsonMimeFragments.some((fragment) => mimeType.includes(fragment))
21
- }