@kubb/adapter-oas 5.0.0-beta.37 → 5.0.0-beta.39

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-beta.37",
3
+ "version": "5.0.0-beta.39",
4
4
  "description": "OpenAPI and Swagger adapter for Kubb. Parses and validates OAS 2.0/3.x specifications into a @kubb/ast RootNode for downstream code generation plugins.",
5
5
  "keywords": [
6
6
  "adapter",
@@ -47,7 +47,7 @@
47
47
  "oas": "^32.1.18",
48
48
  "oas-normalize": "^16.0.4",
49
49
  "swagger2openapi": "^7.0.8",
50
- "@kubb/core": "5.0.0-beta.37"
50
+ "@kubb/core": "5.0.0-beta.39"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/swagger2openapi": "^7.0.4",
package/src/adapter.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { once } from '@internals/utils'
2
1
  import { ast, createAdapter } from '@kubb/core'
3
2
  import type { AdapterSource } from '@kubb/core'
4
3
  import BaseOas from 'oas'
@@ -68,39 +67,83 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
68
67
  let nameMapping = new Map<string, string>()
69
68
  let parsedDocument: Document | null = null
70
69
 
71
- // `once` collapses concurrent callers (e.g. a build's `stream()` racing with `openInStudio()`'s `parse()`) onto one in-flight promise.
72
- const ensureDocument = once(async (source: AdapterSource): Promise<Document> => {
73
- const fresh = await parseFromConfig(source)
74
- if (validate) await validateDocument(fresh)
75
- parsedDocument = fresh
76
- return fresh
77
- })
78
-
79
- const ensureSchemas = once(async (document: Document) => {
80
- const result = getSchemas(document, { contentType })
81
- nameMapping = result.nameMapping
82
- return result.schemas
83
- })
84
-
85
- const ensureBaseOas = once((document: Document) => new BaseOas(document))
86
-
87
- const ensureSchemaParser = once((document: Document) => createSchemaParser({ document, contentType }))
88
-
89
- const ensurePreScan = once(
90
- (
91
- schemas: Awaited<ReturnType<typeof ensureSchemas>>,
92
- parseSchema: ReturnType<typeof ensureSchemaParser>['parseSchema'],
93
- parseOperation: ReturnType<typeof ensureSchemaParser>['parseOperation'],
94
- baseOas: ReturnType<typeof ensureBaseOas>,
95
- ) => preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions, discriminator, dedupe }),
96
- )
70
+ // Cache per source and per document so one adapter instance reused across a `defineConfig` array
71
+ // parses each config's spec instead of replaying the first one. Keying the document by its source
72
+ // object still collapses a config's concurrent `stream()` (build) and `parse()` (studio) calls,
73
+ // which share one source object, onto a single parse. The document-derived caches key off the
74
+ // resulting document, so distinct configs (distinct documents) stay isolated.
75
+ const documentCache = new WeakMap<AdapterSource, Promise<Document>>()
76
+ const schemasCache = new WeakMap<Document, Promise<ReturnType<typeof getSchemas>['schemas']>>()
77
+ const baseOasCache = new WeakMap<Document, BaseOas>()
78
+ const schemaParserCache = new WeakMap<Document, ReturnType<typeof createSchemaParser>>()
79
+ const preScanCache = new WeakMap<Document, ReturnType<typeof preScan>>()
80
+
81
+ function ensureDocument(source: AdapterSource): Promise<Document> {
82
+ const cached = documentCache.get(source)
83
+ if (cached) return cached
84
+
85
+ const promise = (async () => {
86
+ const fresh = await parseFromConfig(source)
87
+ if (validate) await validateDocument(fresh)
88
+ parsedDocument = fresh
89
+ return fresh
90
+ })()
91
+ documentCache.set(source, promise)
92
+ return promise
93
+ }
94
+
95
+ function ensureSchemas(document: Document): Promise<ReturnType<typeof getSchemas>['schemas']> {
96
+ const cached = schemasCache.get(document)
97
+ if (cached) return cached
98
+
99
+ const promise = Promise.resolve().then(() => {
100
+ const result = getSchemas(document, { contentType })
101
+ nameMapping = result.nameMapping
102
+ return result.schemas
103
+ })
104
+ schemasCache.set(document, promise)
105
+ return promise
106
+ }
107
+
108
+ function ensureBaseOas(document: Document): BaseOas {
109
+ const cached = baseOasCache.get(document)
110
+ if (cached) return cached
111
+
112
+ const baseOas = new BaseOas(document)
113
+ baseOasCache.set(document, baseOas)
114
+ return baseOas
115
+ }
116
+
117
+ function ensureSchemaParser(document: Document): ReturnType<typeof createSchemaParser> {
118
+ const cached = schemaParserCache.get(document)
119
+ if (cached) return cached
120
+
121
+ const parser = createSchemaParser({ document, contentType })
122
+ schemaParserCache.set(document, parser)
123
+ return parser
124
+ }
125
+
126
+ function ensurePreScan(
127
+ document: Document,
128
+ schemas: ReturnType<typeof getSchemas>['schemas'],
129
+ parseSchema: ReturnType<typeof ensureSchemaParser>['parseSchema'],
130
+ parseOperation: ReturnType<typeof ensureSchemaParser>['parseOperation'],
131
+ baseOas: BaseOas,
132
+ ): ReturnType<typeof preScan> {
133
+ const cached = preScanCache.get(document)
134
+ if (cached) return cached
135
+
136
+ const result = preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions, discriminator, dedupe })
137
+ preScanCache.set(document, result)
138
+ return result
139
+ }
97
140
 
98
141
  async function createStream(source: AdapterSource): Promise<ast.InputStreamNode> {
99
142
  const document = await ensureDocument(source)
100
143
  const schemas = await ensureSchemas(document)
101
144
  const { parseSchema, parseOperation } = ensureSchemaParser(document)
102
145
  const baseOas = ensureBaseOas(document)
103
- const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(schemas, parseSchema, parseOperation, baseOas)
146
+ const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(document, schemas, parseSchema, parseOperation, baseOas)
104
147
 
105
148
  return createInputStream({
106
149
  schemas,
package/src/factory.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path'
2
2
  import { exists, mergeDeep, URLPath } from '@internals/utils'
3
- import { diagnosticCode, DiagnosticError } from '@kubb/core'
3
+ import { Diagnostics } from '@kubb/core'
4
4
  import type { AdapterSource } from '@kubb/core'
5
5
  import { bundle, loadConfig } from '@redocly/openapi-core'
6
6
  import OASNormalize from 'oas-normalize'
@@ -78,8 +78,8 @@ export async function mergeDocuments(pathOrApi: Array<string | Document>): Promi
78
78
  const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, { enablePaths: false, canBundle: false })))
79
79
 
80
80
  if (documents.length === 0) {
81
- throw new DiagnosticError({
82
- code: diagnosticCode.inputRequired,
81
+ throw new Diagnostics.Error({
82
+ code: Diagnostics.code.inputRequired,
83
83
  severity: 'error',
84
84
  message: 'No OAS documents were provided for merging.',
85
85
  help: 'Pass at least one path or document to `input.path`.',
@@ -149,8 +149,8 @@ export async function assertInputExists(input: string): Promise<void> {
149
149
  return
150
150
  }
151
151
  if (!(await exists(input))) {
152
- throw new DiagnosticError({
153
- code: diagnosticCode.inputNotFound,
152
+ throw new Diagnostics.Error({
153
+ code: Diagnostics.code.inputNotFound,
154
154
  severity: 'error',
155
155
  message: `Cannot read the file set in \`input.path\` (or via \`kubb generate PATH\`): ${input}`,
156
156
  help: 'Check that the path exists and is readable, then set it in `input.path` or pass it as `kubb generate PATH`.',
package/src/refs.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Diagnostic, diagnosticCode, Diagnostics } from '@kubb/core'
1
+ import { type Diagnostic, Diagnostics } from '@kubb/core'
2
2
  import { isReference } from './guards.ts'
3
3
  import type { Document } from './types.ts'
4
4
 
@@ -41,15 +41,18 @@ export function resolveRef<T = unknown>(document: Document, $ref: string): T | n
41
41
 
42
42
  if (!current) {
43
43
  const diagnostic: Diagnostic = {
44
- code: diagnosticCode.refNotFound,
44
+ code: Diagnostics.code.refNotFound,
45
45
  severity: 'error',
46
46
  message: `Could not find a definition for ${origRef}.`,
47
47
  help: 'Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.',
48
48
  location: { kind: 'schema', pointer: origRef, ref: origRef },
49
49
  }
50
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.
52
- Diagnostics.report(diagnostic)
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
+ }
53
56
  return null
54
57
  }
55
58
 
package/src/resolvers.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { pascalCase } from '@internals/utils'
2
- import { diagnosticCode, DiagnosticError } from '@kubb/core'
2
+ import { Diagnostics } from '@kubb/core'
3
3
  import type { ast } from '@kubb/core'
4
4
  import type { ParameterObject, ServerObject } from 'oas/types'
5
5
  import { isRef } from 'oas/types'
@@ -36,8 +36,8 @@ export function resolveServerUrl(server: ServerObject, overrides?: Record<string
36
36
  }
37
37
 
38
38
  if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) {
39
- throw new DiagnosticError({
40
- code: diagnosticCode.invalidServerVariable,
39
+ throw new Diagnostics.Error({
40
+ code: Diagnostics.code.invalidServerVariable,
41
41
  severity: 'error',
42
42
  message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(', ')}.`,
43
43
  help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
@@ -1,4 +1,4 @@
1
- import { diagnosticCode, Diagnostics } from '@kubb/core'
1
+ import { Diagnostics } from '@kubb/core'
2
2
  import type { ast } from '@kubb/core'
3
3
  import { isHandledFormat } from './resolvers.ts'
4
4
 
@@ -11,13 +11,21 @@ import { isHandledFormat } from './resolvers.ts'
11
11
  * no-op outside one, and repeats are deduped by the build.
12
12
  */
13
13
  export function reportSchemaDiagnostics({ node, name }: { node: ast.SchemaNode; name: string }): void {
14
- visit(node, `#/components/schemas/${name}`)
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')
15
23
  }
16
24
 
17
25
  function visit(node: ast.SchemaNode, pointer: string): void {
18
26
  if (node.deprecated) {
19
27
  Diagnostics.report({
20
- code: diagnosticCode.deprecated,
28
+ code: Diagnostics.code.deprecated,
21
29
  severity: 'info',
22
30
  message: 'This schema is marked as deprecated.',
23
31
  location: { kind: 'schema', pointer },
@@ -26,7 +34,7 @@ function visit(node: ast.SchemaNode, pointer: string): void {
26
34
 
27
35
  if (typeof node.format === 'string' && !isHandledFormat(node.format)) {
28
36
  Diagnostics.report({
29
- code: diagnosticCode.unsupportedFormat,
37
+ code: Diagnostics.code.unsupportedFormat,
30
38
  severity: 'warning',
31
39
  message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
32
40
  help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
@@ -36,7 +44,7 @@ function visit(node: ast.SchemaNode, pointer: string): void {
36
44
 
37
45
  if (node.type === 'object') {
38
46
  for (const property of node.properties) {
39
- visit(property.schema, `${pointer}/properties/${property.name}`)
47
+ visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`)
40
48
  }
41
49
  if (node.additionalProperties && typeof node.additionalProperties === 'object') {
42
50
  visit(node.additionalProperties, `${pointer}/additionalProperties`)
@@ -44,16 +52,25 @@ function visit(node: ast.SchemaNode, pointer: string): void {
44
52
  return
45
53
  }
46
54
 
47
- if (node.type === 'array' || node.type === 'tuple') {
55
+ if (node.type === 'array') {
48
56
  for (const item of node.items ?? []) {
49
57
  visit(item, `${pointer}/items`)
50
58
  }
51
59
  return
52
60
  }
53
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
+
54
71
  if (node.type === 'union' || node.type === 'intersection') {
55
- for (const member of node.members ?? []) {
56
- visit(member, pointer)
72
+ for (const [index, member] of (node.members ?? []).entries()) {
73
+ visit(member, `${pointer}/members/${index}`)
57
74
  }
58
75
  }
59
76
  }