@kubb/adapter-oas 5.12.8 → 5.13.1
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 +333 -157
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +340 -157
- package/dist/index.js.map +1 -1
- package/dist/validateWorker.js +17 -0
- package/dist/validateWorker.js.map +1 -0
- package/kubb-extension.json +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -6,15 +6,21 @@ import { bundle } from "api-ref-bundler";
|
|
|
6
6
|
import { CORE_SCHEMA, load } from "js-yaml";
|
|
7
7
|
import * as yaml from "yaml";
|
|
8
8
|
import { parse } from "yaml";
|
|
9
|
+
//#region \0rolldown/runtime.js
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __name = (target, value) => __defProp(target, "name", {
|
|
12
|
+
value,
|
|
13
|
+
configurable: true
|
|
14
|
+
});
|
|
9
15
|
//#endregion
|
|
10
16
|
//#region kubb-extension.json
|
|
11
17
|
var compatibility = {
|
|
12
18
|
"kubb": ">=5.8.3",
|
|
13
|
-
"node": ">=22.
|
|
19
|
+
"node": ">=22.22"
|
|
14
20
|
};
|
|
15
21
|
//#endregion
|
|
16
22
|
//#region package.json
|
|
17
|
-
var version = "5.
|
|
23
|
+
var version = "5.13.1";
|
|
18
24
|
//#endregion
|
|
19
25
|
//#region src/constants.ts
|
|
20
26
|
/**
|
|
@@ -608,8 +614,34 @@ async function validateDocument(document, { throwOnError = false } = {}) {
|
|
|
608
614
|
message: compileErrors(result),
|
|
609
615
|
help: "Fix each problem listed in the spec, then validate again."
|
|
610
616
|
});
|
|
611
|
-
|
|
612
|
-
|
|
617
|
+
reportValidationErrors(result);
|
|
618
|
+
}
|
|
619
|
+
function reportValidationErrors({ errors, additionalErrors }) {
|
|
620
|
+
for (const { message } of errors) reportInvalidSpec(message.replace(/\n.*/s, ""));
|
|
621
|
+
if (additionalErrors > 0) reportInvalidSpec(`${additionalErrors} more validation errors are not listed.`);
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Validates like {@link validateDocument} without `throwOnError`, but in a worker thread, so the
|
|
625
|
+
* build keeps generating while `@readme/openapi-parser` dereferences and checks the spec. Await the
|
|
626
|
+
* returned promise inside the build's diagnostics scope to report its warnings there. Falls back to
|
|
627
|
+
* validating in place where worker threads are missing, as in edge runtimes.
|
|
628
|
+
*/
|
|
629
|
+
async function validateDocumentInWorker(document) {
|
|
630
|
+
let message;
|
|
631
|
+
try {
|
|
632
|
+
const { Worker } = await import("node:worker_threads");
|
|
633
|
+
const workerFile = import.meta.url.endsWith(".ts") ? "./validateWorker.ts" : "./validateWorker.js";
|
|
634
|
+
const worker = new Worker(new URL(workerFile, import.meta.url), { workerData: document });
|
|
635
|
+
message = await new Promise((resolve, reject) => {
|
|
636
|
+
worker.once("message", resolve);
|
|
637
|
+
worker.once("error", reject);
|
|
638
|
+
worker.once("exit", (code) => reject(/* @__PURE__ */ new Error(`Validation worker exited with code ${code}`)));
|
|
639
|
+
}).finally(() => worker.terminate());
|
|
640
|
+
} catch {
|
|
641
|
+
return validateDocument(document);
|
|
642
|
+
}
|
|
643
|
+
if ("thrown" in message) return reportInvalidSpec(message.thrown);
|
|
644
|
+
if (!message.valid) reportValidationErrors(message);
|
|
613
645
|
}
|
|
614
646
|
//#endregion
|
|
615
647
|
//#region src/oas.ts
|
|
@@ -698,7 +730,7 @@ function isJsonMimeType(mimeType) {
|
|
|
698
730
|
//#endregion
|
|
699
731
|
//#region src/model/components.ts
|
|
700
732
|
/**
|
|
701
|
-
* Extracts the inline schema from a media-type `content` map.
|
|
733
|
+
* Extracts the inline schema from a media-type `content` map, with the media type it sits under.
|
|
702
734
|
*
|
|
703
735
|
* Prefers `preferredContentType` when given, otherwise uses the first key in the map.
|
|
704
736
|
* Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
|
|
@@ -706,15 +738,19 @@ function isJsonMimeType(mimeType) {
|
|
|
706
738
|
* @example
|
|
707
739
|
* ```ts
|
|
708
740
|
* extractSchemaFromContent(operation.content, 'application/json')
|
|
709
|
-
* // SchemaObject | null
|
|
741
|
+
* // { schema: SchemaObject, contentType: 'application/json' } | null
|
|
710
742
|
* ```
|
|
711
743
|
*/
|
|
712
744
|
function extractSchemaFromContent(content, preferredContentType) {
|
|
713
745
|
if (!content) return null;
|
|
714
746
|
const firstContentType = Object.keys(content)[0] ?? "application/json";
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
747
|
+
const targetContentType = preferredContentType ?? firstContentType;
|
|
748
|
+
const schema = content[targetContentType]?.schema;
|
|
749
|
+
if (!schema || isReference(schema)) return null;
|
|
750
|
+
return {
|
|
751
|
+
schema,
|
|
752
|
+
contentType: targetContentType
|
|
753
|
+
};
|
|
718
754
|
}
|
|
719
755
|
/**
|
|
720
756
|
* Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
|
|
@@ -766,7 +802,7 @@ function* collectNestedDefinitions(schema, pointer, name) {
|
|
|
766
802
|
const nestedName = `${name}_${key}`;
|
|
767
803
|
yield {
|
|
768
804
|
schema: definition,
|
|
769
|
-
|
|
805
|
+
ref: definitionPointer,
|
|
770
806
|
originalName: nestedName
|
|
771
807
|
};
|
|
772
808
|
yield* collectNestedDefinitions(definition, definitionPointer, nestedName);
|
|
@@ -844,25 +880,28 @@ function getSchemas(document, { contentType }, refs) {
|
|
|
844
880
|
return resolved && !isReference(resolved) ? resolved : schema;
|
|
845
881
|
}
|
|
846
882
|
const candidates = [...Object.entries(components?.schemas ?? {}).flatMap(([name, schema]) => {
|
|
847
|
-
const
|
|
883
|
+
const ref = `#/components/schemas/${escapePointerToken(name)}`;
|
|
848
884
|
return [{
|
|
849
885
|
schema: resolveSchemaRef(schema),
|
|
850
886
|
source: "schemas",
|
|
851
887
|
originalName: name,
|
|
852
|
-
|
|
853
|
-
}, ...[...collectNestedDefinitions(schema,
|
|
888
|
+
ref
|
|
889
|
+
}, ...[...collectNestedDefinitions(schema, ref, name)].map((nested) => ({
|
|
854
890
|
...nested,
|
|
855
891
|
source: "schemas",
|
|
856
892
|
nested: true
|
|
857
893
|
}))];
|
|
858
894
|
}), ...["responses", "requestBodies"].flatMap((source) => Object.entries(components?.[source] ?? {}).flatMap(([name, item]) => {
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
895
|
+
const extracted = extractSchemaFromContent(item.content, contentType);
|
|
896
|
+
if (!extracted) return [];
|
|
897
|
+
const ref = `#/components/${source}/${escapePointerToken(name)}`;
|
|
898
|
+
return [{
|
|
899
|
+
schema: resolveSchemaRef(extracted.schema),
|
|
862
900
|
source,
|
|
863
901
|
originalName: name,
|
|
864
|
-
|
|
865
|
-
|
|
902
|
+
ref,
|
|
903
|
+
pointer: `${ref}/content/${escapePointerToken(extracted.contentType)}/schema`
|
|
904
|
+
}];
|
|
866
905
|
}))];
|
|
867
906
|
const normalizedNames = /* @__PURE__ */ new Map();
|
|
868
907
|
for (const item of candidates) {
|
|
@@ -873,6 +912,7 @@ function getSchemas(document, { contentType }, refs) {
|
|
|
873
912
|
}
|
|
874
913
|
const schemas = {};
|
|
875
914
|
const renames = /* @__PURE__ */ new Map();
|
|
915
|
+
const pointers = /* @__PURE__ */ new Map();
|
|
876
916
|
for (const [, items] of normalizedNames) {
|
|
877
917
|
const isSingle = items.length === 1;
|
|
878
918
|
const hasMultipleSources = !isSingle && new Set(items.map((item) => item.source)).size > 1;
|
|
@@ -880,12 +920,14 @@ function getSchemas(document, { contentType }, refs) {
|
|
|
880
920
|
const suffix = isSingle ? "" : hasMultipleSources ? semanticSuffixes[item.source] : index ? String(index + 1) : "";
|
|
881
921
|
const uniqueName = item.originalName + suffix;
|
|
882
922
|
schemas[uniqueName] = item.schema;
|
|
883
|
-
|
|
923
|
+
pointers.set(uniqueName, item.pointer ?? item.ref);
|
|
924
|
+
if (suffix || item.nested) renames.set(item.ref, uniqueName);
|
|
884
925
|
});
|
|
885
926
|
}
|
|
886
927
|
return {
|
|
887
928
|
schemas: sortSchemas(schemas, renames),
|
|
888
|
-
renames
|
|
929
|
+
renames,
|
|
930
|
+
pointers
|
|
889
931
|
};
|
|
890
932
|
}
|
|
891
933
|
//#endregion
|
|
@@ -942,6 +984,115 @@ function resolveServerUrl(server, overrides) {
|
|
|
942
984
|
return url;
|
|
943
985
|
}
|
|
944
986
|
//#endregion
|
|
987
|
+
//#region src/emit/schemaShape.ts
|
|
988
|
+
/**
|
|
989
|
+
* Whether `format` maps to a dedicated type; `false` is what `KUBB_UNSUPPORTED_FORMAT` flags.
|
|
990
|
+
*/
|
|
991
|
+
function isHandledFormat(format) {
|
|
992
|
+
return formatMap[format] !== void 0 || specialCasedFormats.has(format);
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Resolves the `dateType` option down to the value for one format. The scalar form of
|
|
996
|
+
* `dateType` applies to every format; the object form picks `dateTime`/`date`/`time`
|
|
997
|
+
* individually and defaults an omitted key to `'string'`.
|
|
998
|
+
*/
|
|
999
|
+
function resolveDateTypeValue(dateType, format) {
|
|
1000
|
+
if (typeof dateType !== "object" || dateType === null) return dateType;
|
|
1001
|
+
return dateType[format === "date-time" ? "dateTime" : format] ?? "string";
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Reads schema examples as an array. OAS 3.1 uses an `examples` array, but specs (including ones
|
|
1005
|
+
* labeled 3.1) still use the singular OAS 3.0 `example`, which the upgrader only converts on the
|
|
1006
|
+
* 3.0 -> 3.1 hop. Normalize both into one array so the AST node exposes only `examples`.
|
|
1007
|
+
*/
|
|
1008
|
+
function extractExamples(schema) {
|
|
1009
|
+
if (Array.isArray(schema.examples)) return schema.examples;
|
|
1010
|
+
return schema.example !== void 0 ? [schema.example] : void 0;
|
|
1011
|
+
}
|
|
1012
|
+
function hasStructuralKeywords(fragment) {
|
|
1013
|
+
return Object.keys(fragment).some((key) => structuralKeys.has(key));
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Flattens a keyword-only `allOf` into its parent schema.
|
|
1017
|
+
*
|
|
1018
|
+
* Only flattens when every member is a plain fragment, with no `$ref` and no structural keywords
|
|
1019
|
+
* (see `structuralKeys`). Outer schema values take precedence over fragment values. Returns the
|
|
1020
|
+
* original schema unchanged when flattening is unsafe.
|
|
1021
|
+
*
|
|
1022
|
+
* @example
|
|
1023
|
+
* ```ts
|
|
1024
|
+
* flattenSchema({ allOf: [{ description: 'A pet' }], type: 'object', properties: {} })
|
|
1025
|
+
* // { type: 'object', properties: {}, description: 'A pet' }
|
|
1026
|
+
* flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
|
|
1027
|
+
* // returned unchanged, contains a $ref
|
|
1028
|
+
* ```
|
|
1029
|
+
*/
|
|
1030
|
+
function flattenSchema(schema) {
|
|
1031
|
+
if (!schema.allOf?.length) return schema;
|
|
1032
|
+
const allOfFragments = schema.allOf;
|
|
1033
|
+
if (allOfFragments.some((item) => isReference(item))) return schema;
|
|
1034
|
+
if (allOfFragments.some(hasStructuralKeywords)) return schema;
|
|
1035
|
+
const { allOf: _allOf, ...rest } = schema;
|
|
1036
|
+
const merged = rest;
|
|
1037
|
+
for (const fragment of allOfFragments) for (const [key, value] of Object.entries(fragment)) merged[key] ??= value;
|
|
1038
|
+
return merged;
|
|
1039
|
+
}
|
|
1040
|
+
//#endregion
|
|
1041
|
+
//#region src/schemaDiagnostics.ts
|
|
1042
|
+
/**
|
|
1043
|
+
* Links one freshly converted schema and its nested schemas to their RFC 6901 pointers
|
|
1044
|
+
* (`#/components/schemas/Pet/properties/owner/properties/name`), and reports the advisory
|
|
1045
|
+
* diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`) against them.
|
|
1046
|
+
* Refs are not followed, so the resolved schema is handled under its own walk.
|
|
1047
|
+
* Reports land in the active build run, are a no-op outside one, and repeats are deduped by the build.
|
|
1048
|
+
*/
|
|
1049
|
+
function scanSchema(node, pointer = node.pointer) {
|
|
1050
|
+
if (pointer) visit(node, pointer);
|
|
1051
|
+
}
|
|
1052
|
+
function visit(node, pointer) {
|
|
1053
|
+
node.pointer ??= pointer;
|
|
1054
|
+
if (node.deprecated) Diagnostics.report({
|
|
1055
|
+
code: Diagnostics.code.deprecated,
|
|
1056
|
+
severity: "info",
|
|
1057
|
+
message: "This schema is marked as deprecated.",
|
|
1058
|
+
location: {
|
|
1059
|
+
kind: "schema",
|
|
1060
|
+
pointer
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
if (typeof node.format === "string" && !isHandledFormat(node.format)) Diagnostics.report({
|
|
1064
|
+
code: Diagnostics.code.unsupportedFormat,
|
|
1065
|
+
severity: "warning",
|
|
1066
|
+
message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
|
|
1067
|
+
help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
|
|
1068
|
+
location: {
|
|
1069
|
+
kind: "schema",
|
|
1070
|
+
pointer
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
if (node.type === "object") {
|
|
1074
|
+
for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`);
|
|
1075
|
+
if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (node.type === "array") {
|
|
1079
|
+
for (const item of node.items ?? []) visit(item, `${pointer}/items`);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (node.type === "tuple") {
|
|
1083
|
+
for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}`);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if (node.type === "not") {
|
|
1087
|
+
for (const member of node.members ?? []) visit(member, `${pointer}/not`);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (node.type === "union" || node.type === "intersection") {
|
|
1091
|
+
const keyword = node.type === "intersection" ? "allOf" : node.strategy === "any" ? "anyOf" : "oneOf";
|
|
1092
|
+
for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/${keyword}/${index}`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
//#endregion
|
|
945
1096
|
//#region src/operation.ts
|
|
946
1097
|
function slugify(value) {
|
|
947
1098
|
return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
@@ -991,11 +1142,18 @@ function getRequestContentType({ operation, refs }) {
|
|
|
991
1142
|
}
|
|
992
1143
|
/** Merges path-level and operation-level parameters; operation-level wins per `in:name`. */
|
|
993
1144
|
function getParameters({ operation, refs }) {
|
|
994
|
-
const resolveParams = (params) => params.
|
|
995
|
-
|
|
996
|
-
|
|
1145
|
+
const resolveParams = (params, pointer) => params.flatMap((entry, index) => {
|
|
1146
|
+
const param = refs.derefKeepingRef(entry);
|
|
1147
|
+
return param && typeof param === "object" && "in" in param && "name" in param ? [{
|
|
1148
|
+
param,
|
|
1149
|
+
pointer: `${pointer}/parameters/${index}`
|
|
1150
|
+
}] : [];
|
|
1151
|
+
});
|
|
1152
|
+
const pathPointer = `#/paths/${escapePointerToken(operation.path)}`;
|
|
1153
|
+
const operationParams = resolveParams(operation.schema?.parameters || [], `${pathPointer}/${operation.method.toLowerCase()}`);
|
|
1154
|
+
const pathLevelParams = resolveParams(operation.pathItem.parameters ?? [], pathPointer);
|
|
997
1155
|
const paramMap = /* @__PURE__ */ new Map();
|
|
998
|
-
for (const
|
|
1156
|
+
for (const entry of [...pathLevelParams, ...operationParams]) if (entry.param.name && entry.param.in) paramMap.set(`${entry.param.in}:${entry.param.name}`, entry);
|
|
999
1157
|
return Array.from(paramMap.values());
|
|
1000
1158
|
}
|
|
1001
1159
|
/** Schema for one media type of a `content` map, with the binary fallback for an emptied non-JSON entry. */
|
|
@@ -1043,7 +1201,7 @@ function collectPropertyKeysByFlag(schema, flag) {
|
|
|
1043
1201
|
}
|
|
1044
1202
|
return keys.length ? keys : null;
|
|
1045
1203
|
}
|
|
1046
|
-
function parseParameter({ param, parentName, options, parseSchema }) {
|
|
1204
|
+
function parseParameter({ param, pointer, parentName, options, parseSchema }) {
|
|
1047
1205
|
const schemaName = parentName && param.name ? pascalCase(`${parentName} ${param.name}`) : void 0;
|
|
1048
1206
|
const contentType = param.schema ? void 0 : Object.keys(param.content ?? {})[0];
|
|
1049
1207
|
const rawSchema = param.schema ?? (contentType ? param.content?.[contentType]?.schema : void 0);
|
|
@@ -1051,10 +1209,12 @@ function parseParameter({ param, parentName, options, parseSchema }) {
|
|
|
1051
1209
|
schema: rawSchema,
|
|
1052
1210
|
name: schemaName
|
|
1053
1211
|
}) : ast.factory.createSchema({ type: options.unknownType });
|
|
1212
|
+
if (rawSchema) scanSchema(schema, contentType ? `${pointer}/content/${escapePointerToken(contentType)}/schema` : `${pointer}/schema`);
|
|
1054
1213
|
const style = param.style;
|
|
1055
1214
|
const explode = param.explode;
|
|
1056
1215
|
const allowReserved = param.allowReserved;
|
|
1057
1216
|
return ast.factory.createParameter({
|
|
1217
|
+
pointer,
|
|
1058
1218
|
name: param.name,
|
|
1059
1219
|
in: param.in,
|
|
1060
1220
|
schema: {
|
|
@@ -1075,13 +1235,14 @@ function parseOperation({ operation, ...ctx }) {
|
|
|
1075
1235
|
const { refs, contentType, options, parseSchema } = ctx;
|
|
1076
1236
|
const operationId = getOperationId(operation);
|
|
1077
1237
|
const operationName = operationId ? pascalCase(operationId) : void 0;
|
|
1238
|
+
const pointer = `#/paths/${escapePointerToken(operation.path)}/${operation.method.toLowerCase()}`;
|
|
1078
1239
|
const parameters = getParameters({
|
|
1079
1240
|
operation,
|
|
1080
1241
|
refs
|
|
1081
|
-
}).map((
|
|
1242
|
+
}).map((entry) => parseParameter({
|
|
1082
1243
|
options,
|
|
1083
1244
|
parseSchema,
|
|
1084
|
-
|
|
1245
|
+
...entry,
|
|
1085
1246
|
parentName: operationName
|
|
1086
1247
|
}));
|
|
1087
1248
|
const body = getRequestBody({
|
|
@@ -1097,16 +1258,19 @@ function parseOperation({ operation, ...ctx }) {
|
|
|
1097
1258
|
refs
|
|
1098
1259
|
});
|
|
1099
1260
|
if (!schema) return [];
|
|
1261
|
+
const node = parseSchema({
|
|
1262
|
+
schema,
|
|
1263
|
+
name: requestBodyName
|
|
1264
|
+
});
|
|
1265
|
+
scanSchema(node, `${pointer}/requestBody/content/${escapePointerToken(ct)}/schema`);
|
|
1100
1266
|
return [ast.factory.createContent({
|
|
1101
1267
|
contentType: ct,
|
|
1102
|
-
schema: ast.optionality(
|
|
1103
|
-
schema,
|
|
1104
|
-
name: requestBodyName
|
|
1105
|
-
}), bodyRequired),
|
|
1268
|
+
schema: ast.optionality(node, bodyRequired),
|
|
1106
1269
|
keysToOmit: collectPropertyKeysByFlag(schema, "readOnly")
|
|
1107
1270
|
})];
|
|
1108
1271
|
});
|
|
1109
1272
|
const requestBody = content.length > 0 || body?.description ? {
|
|
1273
|
+
pointer: `${pointer}/requestBody`,
|
|
1110
1274
|
description: body?.description,
|
|
1111
1275
|
required: bodyRequired || void 0,
|
|
1112
1276
|
content: content.length > 0 ? content : void 0
|
|
@@ -1124,11 +1288,13 @@ function parseOperation({ operation, ...ctx }) {
|
|
|
1124
1288
|
contentType: ct,
|
|
1125
1289
|
refs
|
|
1126
1290
|
});
|
|
1291
|
+
const node = raw && Object.keys(raw).length > 0 ? parseSchema({
|
|
1292
|
+
schema: raw,
|
|
1293
|
+
name: responseName
|
|
1294
|
+
}) : ast.factory.createSchema({ type: options.emptySchemaType });
|
|
1295
|
+
if (raw && ct) scanSchema(node, `${pointer}/responses/${statusCode}/content/${escapePointerToken(ct)}/schema`);
|
|
1127
1296
|
return {
|
|
1128
|
-
schema:
|
|
1129
|
-
schema: raw,
|
|
1130
|
-
name: responseName
|
|
1131
|
-
}) : ast.factory.createSchema({ type: options.emptySchemaType }),
|
|
1297
|
+
schema: node,
|
|
1132
1298
|
keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly")
|
|
1133
1299
|
};
|
|
1134
1300
|
};
|
|
@@ -1144,6 +1310,7 @@ function parseOperation({ operation, ...ctx }) {
|
|
|
1144
1310
|
...parseEntrySchema(contentType)
|
|
1145
1311
|
}));
|
|
1146
1312
|
return ast.factory.createResponse({
|
|
1313
|
+
pointer: `${pointer}/responses/${statusCode}`,
|
|
1147
1314
|
statusCode,
|
|
1148
1315
|
description: response?.description,
|
|
1149
1316
|
content: responseContent
|
|
@@ -1156,6 +1323,7 @@ function parseOperation({ operation, ...ctx }) {
|
|
|
1156
1323
|
return typeof fallback === "string" ? fallback : void 0;
|
|
1157
1324
|
};
|
|
1158
1325
|
return ast.factory.createOperation({
|
|
1326
|
+
pointer,
|
|
1159
1327
|
operationId,
|
|
1160
1328
|
protocol: "http",
|
|
1161
1329
|
method: operation.method.toUpperCase(),
|
|
@@ -1171,60 +1339,6 @@ function parseOperation({ operation, ...ctx }) {
|
|
|
1171
1339
|
});
|
|
1172
1340
|
}
|
|
1173
1341
|
//#endregion
|
|
1174
|
-
//#region src/emit/schemaShape.ts
|
|
1175
|
-
/**
|
|
1176
|
-
* Whether `format` maps to a dedicated type; `false` is what `KUBB_UNSUPPORTED_FORMAT` flags.
|
|
1177
|
-
*/
|
|
1178
|
-
function isHandledFormat(format) {
|
|
1179
|
-
return formatMap[format] !== void 0 || specialCasedFormats.has(format);
|
|
1180
|
-
}
|
|
1181
|
-
/**
|
|
1182
|
-
* Resolves the `dateType` option down to the value for one format. The scalar form of
|
|
1183
|
-
* `dateType` applies to every format; the object form picks `dateTime`/`date`/`time`
|
|
1184
|
-
* individually and defaults an omitted key to `'string'`.
|
|
1185
|
-
*/
|
|
1186
|
-
function resolveDateTypeValue(dateType, format) {
|
|
1187
|
-
if (typeof dateType !== "object" || dateType === null) return dateType;
|
|
1188
|
-
return dateType[format === "date-time" ? "dateTime" : format] ?? "string";
|
|
1189
|
-
}
|
|
1190
|
-
/**
|
|
1191
|
-
* Reads schema examples as an array. OAS 3.1 uses an `examples` array, but specs (including ones
|
|
1192
|
-
* labeled 3.1) still use the singular OAS 3.0 `example`, which the upgrader only converts on the
|
|
1193
|
-
* 3.0 -> 3.1 hop. Normalize both into one array so the AST node exposes only `examples`.
|
|
1194
|
-
*/
|
|
1195
|
-
function extractExamples(schema) {
|
|
1196
|
-
if (Array.isArray(schema.examples)) return schema.examples;
|
|
1197
|
-
return schema.example !== void 0 ? [schema.example] : void 0;
|
|
1198
|
-
}
|
|
1199
|
-
function hasStructuralKeywords(fragment) {
|
|
1200
|
-
return Object.keys(fragment).some((key) => structuralKeys.has(key));
|
|
1201
|
-
}
|
|
1202
|
-
/**
|
|
1203
|
-
* Flattens a keyword-only `allOf` into its parent schema.
|
|
1204
|
-
*
|
|
1205
|
-
* Only flattens when every member is a plain fragment, with no `$ref` and no structural keywords
|
|
1206
|
-
* (see `structuralKeys`). Outer schema values take precedence over fragment values. Returns the
|
|
1207
|
-
* original schema unchanged when flattening is unsafe.
|
|
1208
|
-
*
|
|
1209
|
-
* @example
|
|
1210
|
-
* ```ts
|
|
1211
|
-
* flattenSchema({ allOf: [{ description: 'A pet' }], type: 'object', properties: {} })
|
|
1212
|
-
* // { type: 'object', properties: {}, description: 'A pet' }
|
|
1213
|
-
* flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
|
|
1214
|
-
* // returned unchanged, contains a $ref
|
|
1215
|
-
* ```
|
|
1216
|
-
*/
|
|
1217
|
-
function flattenSchema(schema) {
|
|
1218
|
-
if (!schema.allOf?.length) return schema;
|
|
1219
|
-
const allOfFragments = schema.allOf;
|
|
1220
|
-
if (allOfFragments.some((item) => isReference(item))) return schema;
|
|
1221
|
-
if (allOfFragments.some(hasStructuralKeywords)) return schema;
|
|
1222
|
-
const { allOf: _allOf, ...rest } = schema;
|
|
1223
|
-
const merged = rest;
|
|
1224
|
-
for (const fragment of allOfFragments) for (const [key, value] of Object.entries(fragment)) merged[key] ??= value;
|
|
1225
|
-
return merged;
|
|
1226
|
-
}
|
|
1227
|
-
//#endregion
|
|
1228
1342
|
//#region src/emit/createNode.ts
|
|
1229
1343
|
/**
|
|
1230
1344
|
* Builds a schema node from a converter's base context plus its type-specific fields. Every
|
|
@@ -1910,6 +2024,7 @@ function convertNumeric(context, type) {
|
|
|
1910
2024
|
* `options.unknownType`, otherwise parses it as a regular schema.
|
|
1911
2025
|
*/
|
|
1912
2026
|
function resolveMapSchema(mapSchema, { options, parse }) {
|
|
2027
|
+
if (mapSchema === false) return ast.factory.createSchema({ type: "never" });
|
|
1913
2028
|
if (mapSchema === true || typeof mapSchema === "object" && Object.keys(mapSchema).length === 0) return ast.factory.createSchema({ type: options.unknownType });
|
|
1914
2029
|
return parse({ schema: mapSchema });
|
|
1915
2030
|
}
|
|
@@ -1969,6 +2084,10 @@ function convertObject({ schema, name, nullable, defaultValue, options, parse })
|
|
|
1969
2084
|
options,
|
|
1970
2085
|
parse
|
|
1971
2086
|
})])) : void 0;
|
|
2087
|
+
const propertyNames = "propertyNames" in schema && schema.propertyNames !== void 0 ? resolveMapSchema(schema.propertyNames, {
|
|
2088
|
+
options,
|
|
2089
|
+
parse
|
|
2090
|
+
}) : void 0;
|
|
1972
2091
|
const objectNode = createNode({
|
|
1973
2092
|
schema,
|
|
1974
2093
|
name,
|
|
@@ -1980,6 +2099,7 @@ function convertObject({ schema, name, nullable, defaultValue, options, parse })
|
|
|
1980
2099
|
properties,
|
|
1981
2100
|
additionalProperties: additionalPropertiesNode,
|
|
1982
2101
|
patternProperties,
|
|
2102
|
+
propertyNames,
|
|
1983
2103
|
minProperties: schema.minProperties,
|
|
1984
2104
|
maxProperties: schema.maxProperties
|
|
1985
2105
|
});
|
|
@@ -2105,7 +2225,7 @@ const schemaRules = [
|
|
|
2105
2225
|
convert: convertEnum
|
|
2106
2226
|
},
|
|
2107
2227
|
{
|
|
2108
|
-
match: ({ schema, type }) => type === "object" || !!schema.properties || !!schema.additionalProperties || "patternProperties" in schema,
|
|
2228
|
+
match: ({ schema, type }) => type === "object" || !!schema.properties || !!schema.additionalProperties || "patternProperties" in schema || "propertyNames" in schema,
|
|
2109
2229
|
convert: convertObject
|
|
2110
2230
|
},
|
|
2111
2231
|
{
|
|
@@ -2334,55 +2454,6 @@ function createRefs(document) {
|
|
|
2334
2454
|
};
|
|
2335
2455
|
}
|
|
2336
2456
|
//#endregion
|
|
2337
|
-
//#region src/schemaDiagnostics.ts
|
|
2338
|
-
/**
|
|
2339
|
-
* Reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`) for one freshly
|
|
2340
|
-
* converted top-level schema.
|
|
2341
|
-
*
|
|
2342
|
-
* Walks the node the parser produced, threading the RFC 6901 pointer as it descends so a nested
|
|
2343
|
-
* field reports against its full path (`#/components/schemas/Pet/properties/owner/properties/name`).
|
|
2344
|
-
* Refs are not followed, so the resolved schema is reported under its own walk.
|
|
2345
|
-
* Reports land in the active build run, are a no-op outside one, and repeats are deduped by the build.
|
|
2346
|
-
*/
|
|
2347
|
-
function scanSchema({ node, name }) {
|
|
2348
|
-
visit(node, `#/components/schemas/${escapePointerToken(name)}`);
|
|
2349
|
-
}
|
|
2350
|
-
function visit(node, pointer) {
|
|
2351
|
-
if (node.deprecated) Diagnostics.report({
|
|
2352
|
-
code: Diagnostics.code.deprecated,
|
|
2353
|
-
severity: "info",
|
|
2354
|
-
message: "This schema is marked as deprecated.",
|
|
2355
|
-
location: {
|
|
2356
|
-
kind: "schema",
|
|
2357
|
-
pointer
|
|
2358
|
-
}
|
|
2359
|
-
});
|
|
2360
|
-
if (typeof node.format === "string" && !isHandledFormat(node.format)) Diagnostics.report({
|
|
2361
|
-
code: Diagnostics.code.unsupportedFormat,
|
|
2362
|
-
severity: "warning",
|
|
2363
|
-
message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
|
|
2364
|
-
help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
|
|
2365
|
-
location: {
|
|
2366
|
-
kind: "schema",
|
|
2367
|
-
pointer
|
|
2368
|
-
}
|
|
2369
|
-
});
|
|
2370
|
-
if (node.type === "object") {
|
|
2371
|
-
for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`);
|
|
2372
|
-
if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`);
|
|
2373
|
-
return;
|
|
2374
|
-
}
|
|
2375
|
-
if (node.type === "array") {
|
|
2376
|
-
for (const item of node.items ?? []) visit(item, `${pointer}/items`);
|
|
2377
|
-
return;
|
|
2378
|
-
}
|
|
2379
|
-
if (node.type === "tuple") {
|
|
2380
|
-
for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}`);
|
|
2381
|
-
return;
|
|
2382
|
-
}
|
|
2383
|
-
if (node.type === "union" || node.type === "intersection" || node.type === "not") for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/members/${index}`);
|
|
2384
|
-
}
|
|
2385
|
-
//#endregion
|
|
2386
2457
|
//#region src/sourceLocator.ts
|
|
2387
2458
|
let cached;
|
|
2388
2459
|
const toRange = ([start, end]) => ({
|
|
@@ -2423,17 +2494,123 @@ function findRefUsage(document, ref) {
|
|
|
2423
2494
|
return found[0];
|
|
2424
2495
|
}
|
|
2425
2496
|
/**
|
|
2426
|
-
*
|
|
2497
|
+
* One pass over JSON text, so a located diagnostic in a large JSON spec skips `yaml.parseDocument`.
|
|
2498
|
+
* Holds the range of every key and array item by pointer, and the first usage of every `$ref`, as
|
|
2499
|
+
* `findPointer` and `findRefUsage` return them. Throws where the text breaks JSON syntax, such as a
|
|
2500
|
+
* comment, an unquoted key, or an unquoted `$ref` value. Other bare values, such as `object`, pass.
|
|
2427
2501
|
*/
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2502
|
+
function indexJson(source) {
|
|
2503
|
+
const pointers = /* @__PURE__ */ new Map();
|
|
2504
|
+
const refs = /* @__PURE__ */ new Map();
|
|
2505
|
+
let at = source.indexOf("{");
|
|
2506
|
+
const fail = () => {
|
|
2507
|
+
throw new SyntaxError(`Unexpected character at ${at}`);
|
|
2508
|
+
};
|
|
2509
|
+
const skipSpace = () => {
|
|
2510
|
+
for (let code = source.charCodeAt(at); code === 32 || code === 9 || code === 10 || code === 13; code = source.charCodeAt(++at));
|
|
2511
|
+
};
|
|
2512
|
+
const readString = () => {
|
|
2513
|
+
if (source[at] !== "\"") fail();
|
|
2514
|
+
const start = at;
|
|
2515
|
+
for (at++; source.charCodeAt(at) !== 34; at++) {
|
|
2516
|
+
if (at >= source.length) fail();
|
|
2517
|
+
if (source.charCodeAt(at) === 92) at++;
|
|
2518
|
+
}
|
|
2519
|
+
const raw = source.slice(start + 1, at++);
|
|
2520
|
+
return {
|
|
2521
|
+
text: raw.includes("\\") ? JSON.parse(source.slice(start, at)) : raw,
|
|
2522
|
+
start,
|
|
2523
|
+
end: at
|
|
2524
|
+
};
|
|
2525
|
+
};
|
|
2526
|
+
const expect = (char) => {
|
|
2527
|
+
skipSpace();
|
|
2528
|
+
if (source[at++] !== char) fail();
|
|
2529
|
+
skipSpace();
|
|
2530
|
+
};
|
|
2531
|
+
const walk = (pointer) => {
|
|
2532
|
+
const open = source[at];
|
|
2533
|
+
if (open === "\"") {
|
|
2534
|
+
readString();
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
if (open !== "{" && open !== "[") {
|
|
2538
|
+
const start = at;
|
|
2539
|
+
while (at < source.length && !/[\s,\]}]/.test(source[at])) at++;
|
|
2540
|
+
if (at === start) fail();
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
const close = open === "{" ? "}" : "]";
|
|
2544
|
+
at++;
|
|
2545
|
+
for (let index = 0;; index++) {
|
|
2546
|
+
skipSpace();
|
|
2547
|
+
if (source[at] === close) break;
|
|
2548
|
+
if (index > 0) expect(",");
|
|
2549
|
+
if (open === "[") {
|
|
2550
|
+
const start = at;
|
|
2551
|
+
const next = `${pointer}/${index}`;
|
|
2552
|
+
walk(next);
|
|
2553
|
+
if (!pointers.has(next)) pointers.set(next, {
|
|
2554
|
+
start,
|
|
2555
|
+
end: at
|
|
2556
|
+
});
|
|
2557
|
+
continue;
|
|
2558
|
+
}
|
|
2559
|
+
const key = readString();
|
|
2560
|
+
const next = `${pointer}/${escapePointerToken(key.text)}`;
|
|
2561
|
+
if (!pointers.has(next)) pointers.set(next, {
|
|
2562
|
+
start: key.start,
|
|
2563
|
+
end: key.end
|
|
2564
|
+
});
|
|
2565
|
+
expect(":");
|
|
2566
|
+
if (key.text !== "$ref" || source[at] === "{" || source[at] === "[") {
|
|
2567
|
+
walk(next);
|
|
2568
|
+
continue;
|
|
2569
|
+
}
|
|
2570
|
+
const value = readString();
|
|
2571
|
+
if (!refs.has(value.text)) refs.set(value.text, {
|
|
2572
|
+
start: value.start,
|
|
2573
|
+
end: value.end
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
at++;
|
|
2577
|
+
};
|
|
2578
|
+
walk("#");
|
|
2579
|
+
return {
|
|
2580
|
+
pointers,
|
|
2581
|
+
refs
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
/** Indexes plain JSON text; anything else goes to the YAML parser. */
|
|
2585
|
+
function parse$1(source) {
|
|
2586
|
+
if (/^[ \t\n\r]*\{/.test(source)) try {
|
|
2587
|
+
return {
|
|
2588
|
+
source,
|
|
2589
|
+
json: indexJson(source)
|
|
2590
|
+
};
|
|
2591
|
+
} catch {}
|
|
2592
|
+
return {
|
|
2432
2593
|
source,
|
|
2433
2594
|
document: yaml.parseDocument(source)
|
|
2434
2595
|
};
|
|
2596
|
+
}
|
|
2597
|
+
__name(parse$1, "parse");
|
|
2598
|
+
/**
|
|
2599
|
+
* Maps OpenAPI JSON pointers and unresolved references to ranges in YAML or JSON source.
|
|
2600
|
+
*/
|
|
2601
|
+
const locate = (source, location) => {
|
|
2602
|
+
if (!("pointer" in location)) return void 0;
|
|
2603
|
+
cached = cached?.source === source ? {
|
|
2604
|
+
...cached,
|
|
2605
|
+
source
|
|
2606
|
+
} : parse$1(source);
|
|
2607
|
+
const ref = "ref" in location ? location.ref : void 0;
|
|
2608
|
+
if ("json" in cached) {
|
|
2609
|
+
const { pointers, refs } = cached.json;
|
|
2610
|
+
return pointers.get(location.pointer) ?? (ref ? refs.get(ref) : void 0);
|
|
2611
|
+
}
|
|
2435
2612
|
const { document } = cached;
|
|
2436
|
-
return findPointer(document, location.pointer) ?? (
|
|
2613
|
+
return findPointer(document, location.pointer) ?? (ref ? findRefUsage(document, ref) : void 0);
|
|
2437
2614
|
};
|
|
2438
2615
|
//#endregion
|
|
2439
2616
|
//#region src/adapter.ts
|
|
@@ -2490,25 +2667,26 @@ const adapterOas = defineExtension({
|
|
|
2490
2667
|
}),
|
|
2491
2668
|
build({ validate, contentType, server, discriminator, enums, ...parserOptions }) {
|
|
2492
2669
|
let parsedDocument = null;
|
|
2670
|
+
const pendingValidations = [];
|
|
2493
2671
|
const inputCache = /* @__PURE__ */ new WeakMap();
|
|
2494
2672
|
/**
|
|
2495
2673
|
* Parses every schema and operation once, resolving ref aliases and discriminator children from that same pass.
|
|
2496
2674
|
*/
|
|
2497
|
-
function parseInput({ document, refs, schemas, parseSchema }) {
|
|
2675
|
+
function parseInput({ document, refs, schemas, pointers, parseSchema }) {
|
|
2498
2676
|
const parsedByName = /* @__PURE__ */ new Map();
|
|
2499
2677
|
const refAliasMap = /* @__PURE__ */ new Map();
|
|
2500
2678
|
const enumNames = [];
|
|
2501
2679
|
const discriminatorParentNodes = [];
|
|
2502
2680
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
2503
|
-
const node =
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2681
|
+
const node = {
|
|
2682
|
+
...parseSchema({
|
|
2683
|
+
schema,
|
|
2684
|
+
name
|
|
2685
|
+
}),
|
|
2686
|
+
pointer: pointers.get(name)
|
|
2687
|
+
};
|
|
2507
2688
|
parsedByName.set(name, node);
|
|
2508
|
-
scanSchema(
|
|
2509
|
-
node,
|
|
2510
|
-
name
|
|
2511
|
-
});
|
|
2689
|
+
scanSchema(node);
|
|
2512
2690
|
if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
|
|
2513
2691
|
if (ast.narrowSchema(node, "enum") && node.name) enumNames.push(node.name);
|
|
2514
2692
|
if (discriminator === "propagate" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
|
|
@@ -2533,7 +2711,8 @@ const adapterOas = defineExtension({
|
|
|
2533
2711
|
const base = child ? patchDiscriminatorNode(parsed, child) : parsed;
|
|
2534
2712
|
const node = aliased ? {
|
|
2535
2713
|
...aliased,
|
|
2536
|
-
name
|
|
2714
|
+
name,
|
|
2715
|
+
pointer: parsed.pointer
|
|
2537
2716
|
} : base;
|
|
2538
2717
|
schemaNodes.push(promotedEnums ? refPromotedEnums(node, promotedEnums) : node);
|
|
2539
2718
|
}
|
|
@@ -2599,6 +2778,9 @@ const adapterOas = defineExtension({
|
|
|
2599
2778
|
assertDocument(document);
|
|
2600
2779
|
await validateDocument(document, options);
|
|
2601
2780
|
},
|
|
2781
|
+
async settle() {
|
|
2782
|
+
await Promise.all(pendingValidations.splice(0));
|
|
2783
|
+
},
|
|
2602
2784
|
async parse(source, { signal } = {}) {
|
|
2603
2785
|
signal?.throwIfAborted();
|
|
2604
2786
|
const cached = inputCache.get(source);
|
|
@@ -2607,14 +2789,15 @@ const adapterOas = defineExtension({
|
|
|
2607
2789
|
const document = await parseFromConfig(source);
|
|
2608
2790
|
signal?.throwIfAborted();
|
|
2609
2791
|
assertDocument(document);
|
|
2610
|
-
if (validate)
|
|
2792
|
+
if (validate) pendingValidations.push(validateDocumentInWorker(document));
|
|
2611
2793
|
parsedDocument = document;
|
|
2612
2794
|
const refs = createRefs(document);
|
|
2613
|
-
const { schemas, renames } = getSchemas(document, { contentType }, refs);
|
|
2795
|
+
const { schemas, renames, pointers } = getSchemas(document, { contentType }, refs);
|
|
2614
2796
|
return parseInput({
|
|
2615
2797
|
document,
|
|
2616
2798
|
refs,
|
|
2617
2799
|
schemas,
|
|
2800
|
+
pointers,
|
|
2618
2801
|
parseSchema: createSchemaParser({
|
|
2619
2802
|
document,
|
|
2620
2803
|
refs,
|