@kubb/adapter-oas 5.0.0-beta.6 → 5.0.0-beta.61
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/LICENSE +17 -10
- package/README.md +100 -0
- package/dist/index.cjs +1459 -613
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +821 -63
- package/dist/index.js +1461 -610
- package/dist/index.js.map +1 -1
- package/package.json +12 -8
- package/src/adapter.bench.ts +60 -0
- package/src/adapter.ts +133 -56
- package/src/bundler.ts +71 -0
- package/src/constants.ts +17 -3
- package/src/dialect.ts +38 -0
- package/src/discriminator.ts +68 -44
- package/src/factory.ts +63 -48
- package/src/index.ts +0 -3
- package/src/mime.ts +22 -0
- package/src/operation.ts +194 -0
- package/src/parser.ts +335 -214
- package/src/refs.ts +31 -5
- package/src/resolvers.ts +86 -48
- package/src/schemaDiagnostics.ts +76 -0
- package/src/stream.ts +283 -0
- package/src/types.ts +82 -53
- package/extension.yaml +0 -344
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
package/src/refs.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { type Diagnostic, Diagnostics } from '@kubb/core'
|
|
1
2
|
import { isReference } from './guards.ts'
|
|
2
3
|
import type { Document } from './types.ts'
|
|
3
4
|
|
|
5
|
+
const _refCache = new WeakMap<Document, Map<string, unknown>>()
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* Resolves a local JSON pointer reference from a document.
|
|
6
9
|
*
|
|
@@ -18,19 +21,42 @@ export function resolveRef<T = unknown>(document: Document, $ref: string): T | n
|
|
|
18
21
|
if ($ref === '') {
|
|
19
22
|
return null
|
|
20
23
|
}
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
if (!$ref.startsWith('#')) return null
|
|
25
|
+
$ref = globalThis.decodeURIComponent($ref.substring(1))
|
|
26
|
+
|
|
27
|
+
let docCache = _refCache.get(document)
|
|
28
|
+
if (!docCache) {
|
|
29
|
+
docCache = new Map()
|
|
30
|
+
_refCache.set(document, docCache)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (docCache.has($ref)) {
|
|
34
|
+
return docCache.get($ref) as T
|
|
25
35
|
}
|
|
36
|
+
|
|
26
37
|
const current = $ref
|
|
27
38
|
.split('/')
|
|
28
39
|
.filter(Boolean)
|
|
29
40
|
.reduce((obj: unknown, key: string) => (obj as Record<string, unknown>)?.[key], document as unknown)
|
|
30
41
|
|
|
31
42
|
if (!current) {
|
|
32
|
-
|
|
43
|
+
const diagnostic: Diagnostic = {
|
|
44
|
+
code: Diagnostics.code.refNotFound,
|
|
45
|
+
severity: 'error',
|
|
46
|
+
message: `Could not find a definition for ${origRef}.`,
|
|
47
|
+
help: 'Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.',
|
|
48
|
+
location: { kind: 'schema', pointer: origRef, ref: origRef },
|
|
49
|
+
}
|
|
50
|
+
// Report the unresolved ref into the active build and resolve to null, like any
|
|
51
|
+
// other unresolvable ref. The build collects it and keeps going. Outside a build there is no
|
|
52
|
+
// sink, so throw rather than silently returning null.
|
|
53
|
+
if (!Diagnostics.report(diagnostic)) {
|
|
54
|
+
throw new Diagnostics.Error(diagnostic)
|
|
55
|
+
}
|
|
56
|
+
return null
|
|
33
57
|
}
|
|
58
|
+
|
|
59
|
+
docCache.set($ref, current)
|
|
34
60
|
return current as T
|
|
35
61
|
}
|
|
36
62
|
|
package/src/resolvers.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { pascalCase } from '@internals/utils'
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
5
|
-
import { matchesMimeType } from 'oas/utils'
|
|
6
|
-
import { formatMap, SCHEMA_REF_PREFIX, structuralKeys } from './constants.ts'
|
|
2
|
+
import { Diagnostics } from '@kubb/core'
|
|
3
|
+
import type { ast } from '@kubb/core'
|
|
4
|
+
import { formatMap, SCHEMA_REF_PREFIX, specialCasedFormats, structuralKeys } from './constants.ts'
|
|
7
5
|
import { isReference } from './guards.ts'
|
|
6
|
+
import { isJsonMimeType } from './mime.ts'
|
|
7
|
+
import { getRequestContent, getResponseByStatusCode } from './operation.ts'
|
|
8
8
|
import { dereferenceWithRef, resolveRef } from './refs.ts'
|
|
9
|
-
import type { ContentType, Document, MediaTypeObject, Operation, ResponseObject, SchemaObject } from './types.ts'
|
|
9
|
+
import type { ContentType, Document, MediaTypeObject, Operation, ParameterObject, ResponseObject, SchemaObject, ServerObject } from './types.ts'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
|
|
@@ -35,7 +35,13 @@ export function resolveServerUrl(server: ServerObject, overrides?: Record<string
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) {
|
|
38
|
-
throw new Error(
|
|
38
|
+
throw new Diagnostics.Error({
|
|
39
|
+
code: Diagnostics.code.invalidServerVariable,
|
|
40
|
+
severity: 'error',
|
|
41
|
+
message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(', ')}.`,
|
|
42
|
+
help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
|
|
43
|
+
location: { kind: 'document', pointer: '#/servers' },
|
|
44
|
+
})
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
url = url.replaceAll(`{${key}}`, value)
|
|
@@ -52,6 +58,16 @@ export function getSchemaType(format: string): ast.SchemaType | null {
|
|
|
52
58
|
return formatMap[format as keyof typeof formatMap] ?? null
|
|
53
59
|
}
|
|
54
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry and the
|
|
63
|
+
* `specialCasedFormats` `convertFormat` handles directly. False means the format falls back to the
|
|
64
|
+
* base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
|
|
65
|
+
* diagnostic in step with the parser as `formatMap` grows.
|
|
66
|
+
*/
|
|
67
|
+
export function isHandledFormat(format: string): boolean {
|
|
68
|
+
return getSchemaType(format) !== null || specialCasedFormats.has(format)
|
|
69
|
+
}
|
|
70
|
+
|
|
55
71
|
/**
|
|
56
72
|
* Converts an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
|
|
57
73
|
* Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
|
|
@@ -63,13 +79,6 @@ export function getPrimitiveType(type: string | undefined): ast.PrimitiveSchemaT
|
|
|
63
79
|
return 'string'
|
|
64
80
|
}
|
|
65
81
|
|
|
66
|
-
/**
|
|
67
|
-
* Narrows a content-type string to the `MediaType` union Kubb recognizes, or returns `null`.
|
|
68
|
-
*/
|
|
69
|
-
export function getMediaType(contentType: string): ast.MediaType | null {
|
|
70
|
-
return Object.values(ast.mediaTypes).includes(contentType as ast.MediaType) ? (contentType as ast.MediaType) : null
|
|
71
|
-
}
|
|
72
|
-
|
|
73
82
|
export type OperationsOptions = {
|
|
74
83
|
contentType?: ContentType
|
|
75
84
|
}
|
|
@@ -86,7 +95,7 @@ export type OperationsOptions = {
|
|
|
86
95
|
* ```
|
|
87
96
|
*/
|
|
88
97
|
export function getParameters(document: Document, operation: Operation): Array<ParameterObject> {
|
|
89
|
-
const resolveParams = (params: unknown
|
|
98
|
+
const resolveParams = (params: Array<unknown>): Array<ParameterObject> =>
|
|
90
99
|
params.map((p) => dereferenceWithRef(document, p)).filter((p): p is ParameterObject => !!p && typeof p === 'object' && 'in' in p && 'name' in p)
|
|
91
100
|
|
|
92
101
|
const operationParams = resolveParams(operation.schema?.parameters || [])
|
|
@@ -108,7 +117,7 @@ export function getParameters(document: Document, operation: Operation): Array<P
|
|
|
108
117
|
return Array.from(paramMap.values())
|
|
109
118
|
}
|
|
110
119
|
|
|
111
|
-
function getResponseBody(responseBody: boolean | ResponseObject, contentType?: string): MediaTypeObject | false | [string, MediaTypeObject, ...string
|
|
120
|
+
function getResponseBody(responseBody: boolean | ResponseObject, contentType?: string): MediaTypeObject | false | [string, MediaTypeObject, ...Array<string>] {
|
|
112
121
|
if (!responseBody) return false
|
|
113
122
|
if (isReference(responseBody)) return false
|
|
114
123
|
|
|
@@ -123,7 +132,7 @@ function getResponseBody(responseBody: boolean | ResponseObject, contentType?: s
|
|
|
123
132
|
let availableContentType: string | undefined
|
|
124
133
|
const contentTypes = Object.keys(body.content)
|
|
125
134
|
for (const mt of contentTypes) {
|
|
126
|
-
if (
|
|
135
|
+
if (isJsonMimeType(mt)) {
|
|
127
136
|
availableContentType = mt
|
|
128
137
|
break
|
|
129
138
|
}
|
|
@@ -162,7 +171,7 @@ export function getResponseSchema(document: Document, operation: Operation, stat
|
|
|
162
171
|
}
|
|
163
172
|
}
|
|
164
173
|
|
|
165
|
-
const responseBody = getResponseBody(
|
|
174
|
+
const responseBody = getResponseBody(getResponseByStatusCode({ document, operation, statusCode }), options.contentType)
|
|
166
175
|
|
|
167
176
|
if (responseBody === false) {
|
|
168
177
|
return {}
|
|
@@ -190,7 +199,7 @@ export function getRequestSchema(document: Document, operation: Operation, optio
|
|
|
190
199
|
operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody)
|
|
191
200
|
}
|
|
192
201
|
|
|
193
|
-
const requestBody = operation
|
|
202
|
+
const requestBody = getRequestContent({ document, operation, mediaType: options.contentType })
|
|
194
203
|
|
|
195
204
|
if (requestBody === false) {
|
|
196
205
|
return null
|
|
@@ -213,7 +222,7 @@ type SchemaSourceMode = 'schemas' | 'responses' | 'requestBodies'
|
|
|
213
222
|
/**
|
|
214
223
|
* A schema annotated with its component section source and original name. Used by `resolveNameCollisions` for cross-source collision resolution.
|
|
215
224
|
*/
|
|
216
|
-
|
|
225
|
+
type SchemaWithMetadata = {
|
|
217
226
|
schema: SchemaObject
|
|
218
227
|
source: SchemaSourceMode
|
|
219
228
|
originalName: string
|
|
@@ -231,7 +240,7 @@ export type GetSchemasResult = {
|
|
|
231
240
|
/**
|
|
232
241
|
* Flattens a keyword-only `allOf` into its parent schema.
|
|
233
242
|
*
|
|
234
|
-
* Only flattens when every member is a plain fragment
|
|
243
|
+
* Only flattens when every member is a plain fragment, with no `$ref` and no structural keywords
|
|
235
244
|
* (see `structuralKeys`). Outer schema values take precedence over fragment values.
|
|
236
245
|
* Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
|
|
237
246
|
*
|
|
@@ -241,7 +250,7 @@ export type GetSchemasResult = {
|
|
|
241
250
|
* // { type: 'object', properties: {}, description: 'A pet' }
|
|
242
251
|
*
|
|
243
252
|
* flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
|
|
244
|
-
* // returned unchanged
|
|
253
|
+
* // returned unchanged, contains a $ref
|
|
245
254
|
* ```
|
|
246
255
|
*/
|
|
247
256
|
/**
|
|
@@ -260,8 +269,8 @@ function hasStructuralKeywords(fragment: SchemaObject): boolean {
|
|
|
260
269
|
export function flattenSchema(schema: SchemaObject | null): SchemaObject | null {
|
|
261
270
|
if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null
|
|
262
271
|
|
|
263
|
-
const allOfFragments = schema.allOf as SchemaObject
|
|
264
|
-
if (allOfFragments.some((item) =>
|
|
272
|
+
const allOfFragments = schema.allOf as Array<SchemaObject>
|
|
273
|
+
if (allOfFragments.some((item) => isReference(item))) return schema
|
|
265
274
|
if (allOfFragments.some(hasStructuralKeywords)) return schema
|
|
266
275
|
|
|
267
276
|
const merged: SchemaObject = { ...schema }
|
|
@@ -281,7 +290,7 @@ export function flattenSchema(schema: SchemaObject | null): SchemaObject | null
|
|
|
281
290
|
/**
|
|
282
291
|
* Extracts the inline schema from a media-type `content` map.
|
|
283
292
|
*
|
|
284
|
-
* Prefers `preferredContentType` when given
|
|
293
|
+
* Prefers `preferredContentType` when given, otherwise uses the first key in the map.
|
|
285
294
|
* Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
|
|
286
295
|
*
|
|
287
296
|
* @example
|
|
@@ -305,27 +314,25 @@ export function extractSchemaFromContent(content: Record<string, unknown> | unde
|
|
|
305
314
|
/**
|
|
306
315
|
* Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
|
|
307
316
|
*/
|
|
308
|
-
function collectRefs(schema: unknown
|
|
317
|
+
function* collectRefs(schema: unknown): Generator<string, void, undefined> {
|
|
309
318
|
if (Array.isArray(schema)) {
|
|
310
|
-
for (const item of schema) collectRefs(item
|
|
311
|
-
return
|
|
319
|
+
for (const item of schema) yield* collectRefs(item)
|
|
320
|
+
return
|
|
312
321
|
}
|
|
313
322
|
|
|
314
323
|
if (schema && typeof schema === 'object') {
|
|
315
324
|
for (const key in schema) {
|
|
316
325
|
const value = (schema as Record<string, unknown>)[key]
|
|
317
|
-
if (key === '$ref' && typeof value === 'string') {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
326
|
+
if (!(key === '$ref' && typeof value === 'string')) {
|
|
327
|
+
yield* collectRefs(value)
|
|
328
|
+
continue
|
|
329
|
+
}
|
|
330
|
+
if (value.startsWith(SCHEMA_REF_PREFIX)) {
|
|
331
|
+
const name = value.slice(SCHEMA_REF_PREFIX.length)
|
|
332
|
+
if (name) yield name
|
|
324
333
|
}
|
|
325
334
|
}
|
|
326
335
|
}
|
|
327
|
-
|
|
328
|
-
return refs
|
|
329
336
|
}
|
|
330
337
|
|
|
331
338
|
/**
|
|
@@ -341,13 +348,13 @@ function collectRefs(schema: unknown, refs = new Set<string>()): Set<string> {
|
|
|
341
348
|
* ```
|
|
342
349
|
*/
|
|
343
350
|
export function sortSchemas(schemas: Record<string, SchemaObject>): Record<string, SchemaObject> {
|
|
344
|
-
const deps = new Map<string, string
|
|
351
|
+
const deps = new Map<string, Array<string>>()
|
|
345
352
|
|
|
346
353
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
347
|
-
deps.set(name,
|
|
354
|
+
deps.set(name, [...new Set(collectRefs(schema))])
|
|
348
355
|
}
|
|
349
356
|
|
|
350
|
-
const sorted: string
|
|
357
|
+
const sorted: Array<string> = []
|
|
351
358
|
const visited = new Set<string>()
|
|
352
359
|
|
|
353
360
|
function visit(name: string, stack: Set<string>) {
|
|
@@ -404,7 +411,7 @@ function resolveSchemaRef(document: Document, schema: SchemaObject): SchemaObjec
|
|
|
404
411
|
export function getSchemas(document: Document, { contentType }: GetSchemasOptions): GetSchemasResult {
|
|
405
412
|
const components = document.components
|
|
406
413
|
|
|
407
|
-
const candidates: SchemaWithMetadata
|
|
414
|
+
const candidates: Array<SchemaWithMetadata> = [
|
|
408
415
|
...Object.entries((components?.schemas as Record<string, SchemaObject>) ?? {}).map(([name, schema]) => ({
|
|
409
416
|
schema: resolveSchemaRef(document, schema),
|
|
410
417
|
source: 'schemas' as const,
|
|
@@ -426,7 +433,7 @@ export function getSchemas(document: Document, { contentType }: GetSchemasOption
|
|
|
426
433
|
),
|
|
427
434
|
]
|
|
428
435
|
|
|
429
|
-
const normalizedNames = new Map<string, SchemaWithMetadata
|
|
436
|
+
const normalizedNames = new Map<string, Array<SchemaWithMetadata>>()
|
|
430
437
|
for (const item of candidates) {
|
|
431
438
|
const key = pascalCase(item.originalName)
|
|
432
439
|
const bucket = normalizedNames.get(key) ?? []
|
|
@@ -463,7 +470,7 @@ export function getSchemas(document: Document, { contentType }: GetSchemasOption
|
|
|
463
470
|
|
|
464
471
|
/**
|
|
465
472
|
* Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
|
|
466
|
-
* Returns `null` when `dateType: false`,
|
|
473
|
+
* Returns `null` when `dateType: false`, so the format falls through to `string`.
|
|
467
474
|
*/
|
|
468
475
|
export function getDateType(
|
|
469
476
|
options: ast.ParserOptions,
|
|
@@ -514,15 +521,16 @@ export function buildSchemaNode(schema: SchemaObject, name: string | null | unde
|
|
|
514
521
|
writeOnly: schema.writeOnly,
|
|
515
522
|
default: defaultValue,
|
|
516
523
|
example: schema.example,
|
|
524
|
+
format: schema.format,
|
|
517
525
|
} as const
|
|
518
526
|
}
|
|
519
527
|
|
|
520
528
|
/**
|
|
521
529
|
* Returns all request body content type keys for an operation.
|
|
522
530
|
*
|
|
523
|
-
* The requestBody is dereferenced
|
|
524
|
-
*
|
|
525
|
-
*
|
|
531
|
+
* The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
|
|
532
|
+
* `getRequestSchema` already performs), so the returned list accurately reflects the
|
|
533
|
+
* available content types even for referenced bodies.
|
|
526
534
|
*
|
|
527
535
|
* @example
|
|
528
536
|
* ```ts
|
|
@@ -530,7 +538,7 @@ export function buildSchemaNode(schema: SchemaObject, name: string | null | unde
|
|
|
530
538
|
* // ['application/json', 'multipart/form-data']
|
|
531
539
|
* ```
|
|
532
540
|
*/
|
|
533
|
-
export function getRequestBodyContentTypes(document: Document, operation: Operation): string
|
|
541
|
+
export function getRequestBodyContentTypes(document: Document, operation: Operation): Array<string> {
|
|
534
542
|
if (operation.schema.requestBody) {
|
|
535
543
|
operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody)
|
|
536
544
|
}
|
|
@@ -539,6 +547,36 @@ export function getRequestBodyContentTypes(document: Document, operation: Operat
|
|
|
539
547
|
if (!body) return []
|
|
540
548
|
|
|
541
549
|
// dereferenceWithRef keeps $ref but spreads all resolved fields (including `content`).
|
|
542
|
-
// Do not bail out on isReference
|
|
550
|
+
// Do not bail out on isReference, the content is already present on the merged object.
|
|
551
|
+
return body.content ? Object.keys(body.content) : []
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Returns all response content type keys for an operation at a given status code.
|
|
556
|
+
*
|
|
557
|
+
* Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
|
|
558
|
+
* so the returned list reflects the available content types even for referenced responses.
|
|
559
|
+
*
|
|
560
|
+
* @example
|
|
561
|
+
* ```ts
|
|
562
|
+
* getResponseBodyContentTypes(document, operation, 200)
|
|
563
|
+
* // ['application/json', 'application/xml']
|
|
564
|
+
* ```
|
|
565
|
+
*/
|
|
566
|
+
export function getResponseBodyContentTypes(document: Document, operation: Operation, statusCode: string | number): Array<string> {
|
|
567
|
+
if (operation.schema.responses) {
|
|
568
|
+
const responses = operation.schema.responses
|
|
569
|
+
for (const key in responses) {
|
|
570
|
+
const schema = responses[key]
|
|
571
|
+
if (schema && isReference(schema)) {
|
|
572
|
+
responses[key] = resolveRef<any>(document, schema.$ref)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const responseObj = getResponseByStatusCode({ document, operation, statusCode })
|
|
578
|
+
if (!responseObj || typeof responseObj !== 'object' || isReference(responseObj)) return []
|
|
579
|
+
|
|
580
|
+
const body = responseObj as { content?: Record<string, unknown> }
|
|
543
581
|
return body.content ? Object.keys(body.content) : []
|
|
544
582
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Diagnostics } from '@kubb/core'
|
|
2
|
+
import type { ast } from '@kubb/core'
|
|
3
|
+
import { isHandledFormat } from './resolvers.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`) for one
|
|
7
|
+
* top-level schema. Walks the node the parser produced during `preScan`, threading the RFC 6901
|
|
8
|
+
* pointer as it descends so a nested field reports against its full path
|
|
9
|
+
* (`#/components/schemas/Pet/properties/owner/properties/name`). Refs are not followed, so the
|
|
10
|
+
* resolved schema is reported under its own walk. Reports land in the active build run, are a
|
|
11
|
+
* no-op outside one, and repeats are deduped by the build.
|
|
12
|
+
*/
|
|
13
|
+
export function reportSchemaDiagnostics({ node, name }: { node: ast.SchemaNode; name: string }): void {
|
|
14
|
+
visit(node, `#/components/schemas/${escapePointerToken(name)}`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Escapes a single JSON pointer reference token per RFC 6901 (`~` → `~0`, `/` → `~1`), so a
|
|
19
|
+
* property name with those characters maps to a distinct pointer instead of colliding in the dedupe.
|
|
20
|
+
*/
|
|
21
|
+
function escapePointerToken(token: string): string {
|
|
22
|
+
return token.replace(/~/g, '~0').replace(/\//g, '~1')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function visit(node: ast.SchemaNode, pointer: string): void {
|
|
26
|
+
if (node.deprecated) {
|
|
27
|
+
Diagnostics.report({
|
|
28
|
+
code: Diagnostics.code.deprecated,
|
|
29
|
+
severity: 'info',
|
|
30
|
+
message: 'This schema is marked as deprecated.',
|
|
31
|
+
location: { kind: 'schema', pointer },
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof node.format === 'string' && !isHandledFormat(node.format)) {
|
|
36
|
+
Diagnostics.report({
|
|
37
|
+
code: Diagnostics.code.unsupportedFormat,
|
|
38
|
+
severity: 'warning',
|
|
39
|
+
message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
|
|
40
|
+
help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
|
|
41
|
+
location: { kind: 'schema', pointer },
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (node.type === 'object') {
|
|
46
|
+
for (const property of node.properties) {
|
|
47
|
+
visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`)
|
|
48
|
+
}
|
|
49
|
+
if (node.additionalProperties && typeof node.additionalProperties === 'object') {
|
|
50
|
+
visit(node.additionalProperties, `${pointer}/additionalProperties`)
|
|
51
|
+
}
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (node.type === 'array') {
|
|
56
|
+
for (const item of node.items ?? []) {
|
|
57
|
+
visit(item, `${pointer}/items`)
|
|
58
|
+
}
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (node.type === 'tuple') {
|
|
63
|
+
// Each tuple position has its own pointer, so index them. A shared `/items` would collapse
|
|
64
|
+
// distinct diagnostics in the dedupe.
|
|
65
|
+
for (const [index, item] of (node.items ?? []).entries()) {
|
|
66
|
+
visit(item, `${pointer}/items/${index}`)
|
|
67
|
+
}
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (node.type === 'union' || node.type === 'intersection') {
|
|
72
|
+
for (const [index, member] of (node.members ?? []).entries()) {
|
|
73
|
+
visit(member, `${pointer}/members/${index}`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|