@kubb/adapter-oas 5.0.0-beta.35 → 5.0.0-beta.37
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 +159 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +160 -30
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +2 -1
- package/src/constants.ts +8 -0
- package/src/dialect.ts +3 -3
- package/src/factory.ts +37 -8
- package/src/parser.ts +11 -16
- package/src/refs.ts +12 -1
- package/src/resolvers.ts +27 -10
- package/src/schemaDiagnostics.ts +59 -0
- package/src/stream.ts +5 -3
- package/src/types.ts +5 -5
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
|
+
let node_fs_promises = require("node:fs/promises");
|
|
24
25
|
let node_path = require("node:path");
|
|
25
26
|
node_path = __toESM(node_path, 1);
|
|
26
27
|
let _kubb_core = require("@kubb/core");
|
|
@@ -97,6 +98,23 @@ function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
|
|
|
97
98
|
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
98
99
|
}
|
|
99
100
|
//#endregion
|
|
101
|
+
//#region ../../internals/utils/src/fs.ts
|
|
102
|
+
/**
|
|
103
|
+
* Resolves to `true` when the file or directory at `path` exists.
|
|
104
|
+
* Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* if (await exists('./kubb.config.ts')) {
|
|
109
|
+
* const content = await read('./kubb.config.ts')
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
async function exists(path) {
|
|
114
|
+
if (typeof Bun !== "undefined") return Bun.file(path).exists();
|
|
115
|
+
return (0, node_fs_promises.access)(path).then(() => true, () => false);
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
100
118
|
//#region ../../internals/utils/src/object.ts
|
|
101
119
|
/**
|
|
102
120
|
* Returns `true` when `value` is a plain (non-null, non-array) object.
|
|
@@ -487,6 +505,18 @@ const structuralKeys = new Set([
|
|
|
487
505
|
* formatMap['float'] // 'number'
|
|
488
506
|
* ```
|
|
489
507
|
*/
|
|
508
|
+
/**
|
|
509
|
+
* Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
|
|
510
|
+
* `int64` and the date/time family. Keep this in sync with the `convertFormat`
|
|
511
|
+
* special-cases in `parser.ts`. `isHandledFormat` reads it so the
|
|
512
|
+
* `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
|
|
513
|
+
*/
|
|
514
|
+
const specialCasedFormats = new Set([
|
|
515
|
+
"int64",
|
|
516
|
+
"date-time",
|
|
517
|
+
"date",
|
|
518
|
+
"time"
|
|
519
|
+
]);
|
|
490
520
|
const formatMap = {
|
|
491
521
|
uuid: "uuid",
|
|
492
522
|
email: "email",
|
|
@@ -634,7 +664,13 @@ async function mergeDocuments(pathOrApi) {
|
|
|
634
664
|
enablePaths: false,
|
|
635
665
|
canBundle: false
|
|
636
666
|
})));
|
|
637
|
-
if (documents.length === 0) throw new
|
|
667
|
+
if (documents.length === 0) throw new _kubb_core.DiagnosticError({
|
|
668
|
+
code: _kubb_core.diagnosticCode.inputRequired,
|
|
669
|
+
severity: "error",
|
|
670
|
+
message: "No OAS documents were provided for merging.",
|
|
671
|
+
help: "Pass at least one path or document to `input.path`.",
|
|
672
|
+
location: { kind: "config" }
|
|
673
|
+
});
|
|
638
674
|
const seed = {
|
|
639
675
|
openapi: MERGE_OPENAPI_VERSION,
|
|
640
676
|
info: {
|
|
@@ -650,9 +686,9 @@ async function mergeDocuments(pathOrApi) {
|
|
|
650
686
|
* Creates a `Document` from an `AdapterSource`.
|
|
651
687
|
*
|
|
652
688
|
* Handles all three source types:
|
|
653
|
-
* - `{ type: 'path' }`
|
|
654
|
-
* - `{ type: 'paths' }`
|
|
655
|
-
* - `{ type: 'data' }`
|
|
689
|
+
* - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
|
|
690
|
+
* - `{ type: 'paths' }` merges multiple file paths into a single document.
|
|
691
|
+
* - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
|
|
656
692
|
*
|
|
657
693
|
* @example
|
|
658
694
|
* ```ts
|
|
@@ -660,14 +696,31 @@ async function mergeDocuments(pathOrApi) {
|
|
|
660
696
|
* const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
|
|
661
697
|
* ```
|
|
662
698
|
*/
|
|
663
|
-
function parseFromConfig(source) {
|
|
699
|
+
async function parseFromConfig(source) {
|
|
664
700
|
if (source.type === "data") {
|
|
665
701
|
if (typeof source.data === "object") return parseDocument(structuredClone(source.data));
|
|
666
702
|
return parseDocument(source.data, { canBundle: false });
|
|
667
703
|
}
|
|
668
704
|
if (source.type === "paths") return mergeDocuments(source.paths);
|
|
669
705
|
if (new URLPath(source.path).isURL) return parseDocument(source.path);
|
|
670
|
-
|
|
706
|
+
const resolved = node_path.default.resolve(node_path.default.dirname(source.path), source.path);
|
|
707
|
+
await assertInputExists(resolved);
|
|
708
|
+
return parseDocument(resolved);
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
|
|
712
|
+
* URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
|
|
713
|
+
* its parse error instead.
|
|
714
|
+
*/
|
|
715
|
+
async function assertInputExists(input) {
|
|
716
|
+
if (new URLPath(input).isURL) return;
|
|
717
|
+
if (!await exists(input)) throw new _kubb_core.DiagnosticError({
|
|
718
|
+
code: _kubb_core.diagnosticCode.inputNotFound,
|
|
719
|
+
severity: "error",
|
|
720
|
+
message: `Cannot read the file set in \`input.path\` (or via \`kubb generate PATH\`): ${input}`,
|
|
721
|
+
help: "Check that the path exists and is readable, then set it in `input.path` or pass it as `kubb generate PATH`.",
|
|
722
|
+
location: { kind: "config" }
|
|
723
|
+
});
|
|
671
724
|
}
|
|
672
725
|
/**
|
|
673
726
|
* Validates an OpenAPI document using `oas-normalize` with colorized error output.
|
|
@@ -714,7 +767,21 @@ function resolveRef(document, $ref) {
|
|
|
714
767
|
}
|
|
715
768
|
if (docCache.has($ref)) return docCache.get($ref);
|
|
716
769
|
const current = $ref.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
|
|
717
|
-
if (!current)
|
|
770
|
+
if (!current) {
|
|
771
|
+
const diagnostic = {
|
|
772
|
+
code: _kubb_core.diagnosticCode.refNotFound,
|
|
773
|
+
severity: "error",
|
|
774
|
+
message: `Could not find a definition for ${origRef}.`,
|
|
775
|
+
help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
|
|
776
|
+
location: {
|
|
777
|
+
kind: "schema",
|
|
778
|
+
pointer: origRef,
|
|
779
|
+
ref: origRef
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
_kubb_core.Diagnostics.report(diagnostic);
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
718
785
|
docCache.set($ref, current);
|
|
719
786
|
return current;
|
|
720
787
|
}
|
|
@@ -741,13 +808,13 @@ function dereferenceWithRef(document, schema) {
|
|
|
741
808
|
//#endregion
|
|
742
809
|
//#region src/dialect.ts
|
|
743
810
|
/**
|
|
744
|
-
* The OpenAPI / Swagger dialect
|
|
811
|
+
* The OpenAPI / Swagger dialect, the default used by `@kubb/adapter-oas`.
|
|
745
812
|
*
|
|
746
813
|
* Implements the spec-agnostic {@link ast.SchemaDialect} contract: it isolates the
|
|
747
814
|
* decisions that differ between specs (nullability, `$ref`, discriminator, binary,
|
|
748
815
|
* ref resolution) so the converter pipeline and dispatch rules stay shared. A
|
|
749
|
-
* future adapter (e.g. AsyncAPI) ships its own dialect
|
|
750
|
-
* nullability, no discriminator object, binary via `contentEncoding`
|
|
816
|
+
* future adapter (e.g. AsyncAPI) ships its own dialect, `type: ['null', …]`
|
|
817
|
+
* nullability, no discriminator object, binary via `contentEncoding` and reuses
|
|
751
818
|
* the rest unchanged.
|
|
752
819
|
*
|
|
753
820
|
* Formats (`uuid`, `email`, dates, …) are intentionally NOT here: they are shared
|
|
@@ -789,7 +856,16 @@ function resolveServerUrl(server, overrides) {
|
|
|
789
856
|
for (const [key, variable] of Object.entries(server.variables)) {
|
|
790
857
|
const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
|
|
791
858
|
if (value === void 0) continue;
|
|
792
|
-
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new
|
|
859
|
+
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new _kubb_core.DiagnosticError({
|
|
860
|
+
code: _kubb_core.diagnosticCode.invalidServerVariable,
|
|
861
|
+
severity: "error",
|
|
862
|
+
message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
|
|
863
|
+
help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
|
|
864
|
+
location: {
|
|
865
|
+
kind: "document",
|
|
866
|
+
pointer: "#/servers"
|
|
867
|
+
}
|
|
868
|
+
});
|
|
793
869
|
url = url.replaceAll(`{${key}}`, value);
|
|
794
870
|
}
|
|
795
871
|
return url;
|
|
@@ -802,6 +878,15 @@ function getSchemaType(format) {
|
|
|
802
878
|
return formatMap[format] ?? null;
|
|
803
879
|
}
|
|
804
880
|
/**
|
|
881
|
+
* Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry and the
|
|
882
|
+
* `specialCasedFormats` `convertFormat` handles directly. False means the format falls back to the
|
|
883
|
+
* base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
|
|
884
|
+
* diagnostic in step with the parser as `formatMap` grows.
|
|
885
|
+
*/
|
|
886
|
+
function isHandledFormat(format) {
|
|
887
|
+
return getSchemaType(format) !== null || specialCasedFormats.has(format);
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
805
890
|
* Converts an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
|
|
806
891
|
* Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
|
|
807
892
|
*/
|
|
@@ -898,7 +983,7 @@ function getRequestSchema(document, operation, options = {}) {
|
|
|
898
983
|
/**
|
|
899
984
|
* Flattens a keyword-only `allOf` into its parent schema.
|
|
900
985
|
*
|
|
901
|
-
* Only flattens when every member is a plain fragment
|
|
986
|
+
* Only flattens when every member is a plain fragment, with no `$ref` and no structural keywords
|
|
902
987
|
* (see `structuralKeys`). Outer schema values take precedence over fragment values.
|
|
903
988
|
* Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
|
|
904
989
|
*
|
|
@@ -908,7 +993,7 @@ function getRequestSchema(document, operation, options = {}) {
|
|
|
908
993
|
* // { type: 'object', properties: {}, description: 'A pet' }
|
|
909
994
|
*
|
|
910
995
|
* flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
|
|
911
|
-
* // returned unchanged
|
|
996
|
+
* // returned unchanged, contains a $ref
|
|
912
997
|
* ```
|
|
913
998
|
*/
|
|
914
999
|
/**
|
|
@@ -934,7 +1019,7 @@ function flattenSchema(schema) {
|
|
|
934
1019
|
/**
|
|
935
1020
|
* Extracts the inline schema from a media-type `content` map.
|
|
936
1021
|
*
|
|
937
|
-
* Prefers `preferredContentType` when given
|
|
1022
|
+
* Prefers `preferredContentType` when given, otherwise uses the first key in the map.
|
|
938
1023
|
* Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
|
|
939
1024
|
*
|
|
940
1025
|
* @example
|
|
@@ -1126,9 +1211,9 @@ function buildSchemaNode(schema, name, nullable, defaultValue) {
|
|
|
1126
1211
|
/**
|
|
1127
1212
|
* Returns all request body content type keys for an operation.
|
|
1128
1213
|
*
|
|
1129
|
-
* The requestBody is dereferenced
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1214
|
+
* The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
|
|
1215
|
+
* `getRequestSchema` already performs), so the returned list accurately reflects the
|
|
1216
|
+
* available content types even for referenced bodies.
|
|
1132
1217
|
*
|
|
1133
1218
|
* @example
|
|
1134
1219
|
* ```ts
|
|
@@ -1145,7 +1230,7 @@ function getRequestBodyContentTypes(document, operation) {
|
|
|
1145
1230
|
/**
|
|
1146
1231
|
* Returns all response content type keys for an operation at a given status code.
|
|
1147
1232
|
*
|
|
1148
|
-
* Response `$ref`s are resolved in
|
|
1233
|
+
* Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
|
|
1149
1234
|
* so the returned list reflects the available content types even for referenced responses.
|
|
1150
1235
|
*
|
|
1151
1236
|
* @example
|
|
@@ -1205,16 +1290,12 @@ function createSchemaParser(ctx, dialect = oasDialect) {
|
|
|
1205
1290
|
*/
|
|
1206
1291
|
const resolvingRefs = /* @__PURE__ */ new Set();
|
|
1207
1292
|
/**
|
|
1208
|
-
* Cache of
|
|
1209
|
-
*
|
|
1210
|
-
* Without this, the same referenced schema (e.g. `customer`) is fully re-expanded
|
|
1211
|
-
* every time it appears as a `$ref` in a different parent schema. In heavily
|
|
1212
|
-
* cross-referenced specs like Stripe (~1 400 schemas), this causes exponential
|
|
1213
|
-
* blowup — `customer` alone may be referenced from dozens of top-level schemas,
|
|
1214
|
-
* each triggering a fresh recursive expansion of its entire sub-tree.
|
|
1293
|
+
* Cache of `$ref` schemas already resolved in this parser instance, keyed by ref path.
|
|
1215
1294
|
*
|
|
1216
|
-
*
|
|
1217
|
-
*
|
|
1295
|
+
* Without it, a shared schema (e.g. `customer`) is re-expanded for every `$ref` that points at
|
|
1296
|
+
* it. In cross-referenced specs like Stripe (~1400 schemas) that becomes exponential blowup,
|
|
1297
|
+
* since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
|
|
1298
|
+
* Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
|
|
1218
1299
|
*/
|
|
1219
1300
|
const resolvedRefCache = /* @__PURE__ */ new Map();
|
|
1220
1301
|
/**
|
|
@@ -1752,7 +1833,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
|
|
|
1752
1833
|
/**
|
|
1753
1834
|
* Ordered schema-dispatch table. Order is significant: composition keywords (`$ref`, `allOf`,
|
|
1754
1835
|
* `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
|
|
1755
|
-
* `type`. The first matching rule that produces a node wins
|
|
1836
|
+
* `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
|
|
1756
1837
|
* match/convert/fall-through contract.
|
|
1757
1838
|
*/
|
|
1758
1839
|
const schemaRules = [
|
|
@@ -2097,6 +2178,50 @@ function patchDiscriminatorNode(node, entry) {
|
|
|
2097
2178
|
};
|
|
2098
2179
|
}
|
|
2099
2180
|
//#endregion
|
|
2181
|
+
//#region src/schemaDiagnostics.ts
|
|
2182
|
+
/**
|
|
2183
|
+
* Reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`) for one
|
|
2184
|
+
* top-level schema. Walks the node the parser produced during `preScan`, threading the RFC 6901
|
|
2185
|
+
* pointer as it descends so a nested field reports against its full path
|
|
2186
|
+
* (`#/components/schemas/Pet/properties/owner/properties/name`). Refs are not followed, so the
|
|
2187
|
+
* resolved schema is reported under its own walk. Reports land in the active build run, are a
|
|
2188
|
+
* no-op outside one, and repeats are deduped by the build.
|
|
2189
|
+
*/
|
|
2190
|
+
function reportSchemaDiagnostics({ node, name }) {
|
|
2191
|
+
visit(node, `#/components/schemas/${name}`);
|
|
2192
|
+
}
|
|
2193
|
+
function visit(node, pointer) {
|
|
2194
|
+
if (node.deprecated) _kubb_core.Diagnostics.report({
|
|
2195
|
+
code: _kubb_core.diagnosticCode.deprecated,
|
|
2196
|
+
severity: "info",
|
|
2197
|
+
message: "This schema is marked as deprecated.",
|
|
2198
|
+
location: {
|
|
2199
|
+
kind: "schema",
|
|
2200
|
+
pointer
|
|
2201
|
+
}
|
|
2202
|
+
});
|
|
2203
|
+
if (typeof node.format === "string" && !isHandledFormat(node.format)) _kubb_core.Diagnostics.report({
|
|
2204
|
+
code: _kubb_core.diagnosticCode.unsupportedFormat,
|
|
2205
|
+
severity: "warning",
|
|
2206
|
+
message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
|
|
2207
|
+
help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
|
|
2208
|
+
location: {
|
|
2209
|
+
kind: "schema",
|
|
2210
|
+
pointer
|
|
2211
|
+
}
|
|
2212
|
+
});
|
|
2213
|
+
if (node.type === "object") {
|
|
2214
|
+
for (const property of node.properties) visit(property.schema, `${pointer}/properties/${property.name}`);
|
|
2215
|
+
if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`);
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
if (node.type === "array" || node.type === "tuple") {
|
|
2219
|
+
for (const item of node.items ?? []) visit(item, `${pointer}/items`);
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
if (node.type === "union" || node.type === "intersection") for (const member of node.members ?? []) visit(member, pointer);
|
|
2223
|
+
}
|
|
2224
|
+
//#endregion
|
|
2100
2225
|
//#region src/stream.ts
|
|
2101
2226
|
/**
|
|
2102
2227
|
* Builds the deduplication plan from the already-parsed top-level schema nodes plus a
|
|
@@ -2156,7 +2281,7 @@ function resolveBaseUrl({ document, serverIndex, serverVariables }) {
|
|
|
2156
2281
|
* After this call, only `refAliasMap` and `discriminatorChildMap` stay alive in the adapter closure.
|
|
2157
2282
|
* Both are proportional to the number of aliases or discriminator parents, not total schema count.
|
|
2158
2283
|
*
|
|
2159
|
-
* Each schema is parsed again during the streaming pass
|
|
2284
|
+
* Each schema is parsed again during the streaming pass. This is intentional.
|
|
2160
2285
|
* Holding the parsed nodes in memory here would defeat the streaming memory benefit.
|
|
2161
2286
|
*
|
|
2162
2287
|
* @example
|
|
@@ -2180,6 +2305,10 @@ function preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions,
|
|
|
2180
2305
|
name
|
|
2181
2306
|
}, parserOptions);
|
|
2182
2307
|
allNodes.push(node);
|
|
2308
|
+
reportSchemaDiagnostics({
|
|
2309
|
+
node,
|
|
2310
|
+
name
|
|
2311
|
+
});
|
|
2183
2312
|
if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
|
|
2184
2313
|
if (_kubb_core.ast.narrowSchema(node, _kubb_core.ast.schemaTypes.enum) && node.name) enumNames.push(node.name);
|
|
2185
2314
|
if (discriminator === "inherit" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
|
|
@@ -2396,6 +2525,7 @@ const adapterOas = (0, _kubb_core.createAdapter)((options) => {
|
|
|
2396
2525
|
return parsedDocument;
|
|
2397
2526
|
},
|
|
2398
2527
|
async validate(input, options) {
|
|
2528
|
+
await assertInputExists(input);
|
|
2399
2529
|
await validateDocument(await parseDocument(input), options);
|
|
2400
2530
|
},
|
|
2401
2531
|
getImports(node, resolve) {
|