schema-components 1.22.0 → 1.24.0

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.
Files changed (82) hide show
  1. package/README.md +3 -1
  2. package/dist/core/adapter.d.mts +97 -3
  3. package/dist/core/adapter.mjs +260 -111
  4. package/dist/core/constraints.d.mts +2 -2
  5. package/dist/core/constraints.mjs +0 -7
  6. package/dist/core/cssClasses.d.mts +52 -0
  7. package/dist/core/cssClasses.mjs +51 -0
  8. package/dist/core/diagnostics.d.mts +1 -1
  9. package/dist/core/errors.d.mts +1 -1
  10. package/dist/core/errors.mjs +5 -13
  11. package/dist/core/fieldOrder.d.mts +1 -1
  12. package/dist/core/formats.d.mts +9 -2
  13. package/dist/core/formats.mjs +12 -1
  14. package/dist/core/idPath.d.mts +54 -0
  15. package/dist/core/idPath.mjs +66 -0
  16. package/dist/core/merge.d.mts +10 -1
  17. package/dist/core/merge.mjs +49 -10
  18. package/dist/core/normalise.d.mts +14 -3
  19. package/dist/core/normalise.mjs +2 -2
  20. package/dist/core/openapi30.d.mts +15 -1
  21. package/dist/core/openapi30.mjs +2 -2
  22. package/dist/core/openapiConstants.d.mts +67 -0
  23. package/dist/core/openapiConstants.mjs +90 -0
  24. package/dist/core/ref.d.mts +2 -2
  25. package/dist/core/ref.mjs +83 -6
  26. package/dist/core/refChain.d.mts +70 -0
  27. package/dist/core/refChain.mjs +44 -0
  28. package/dist/core/renderer.d.mts +1 -1
  29. package/dist/core/swagger2.d.mts +1 -1
  30. package/dist/core/swagger2.mjs +1 -1
  31. package/dist/core/typeInference.d.mts +982 -2
  32. package/dist/core/types.d.mts +1 -1
  33. package/dist/core/unionMatch.d.mts +36 -0
  34. package/dist/core/unionMatch.mjs +53 -0
  35. package/dist/core/version.d.mts +1 -1
  36. package/dist/core/version.mjs +29 -17
  37. package/dist/core/walkBuilders.d.mts +23 -4
  38. package/dist/core/walkBuilders.mjs +27 -7
  39. package/dist/core/walker.d.mts +1 -1
  40. package/dist/core/walker.mjs +44 -45
  41. package/dist/{diagnostics-D0QCYGv0.d.mts → diagnostics-Cbwak-ZX.d.mts} +1 -1
  42. package/dist/{errors-DpFwqs5C.d.mts → errors-g_MCTQel.d.mts} +9 -15
  43. package/dist/html/a11y.d.mts +9 -4
  44. package/dist/html/a11y.mjs +10 -19
  45. package/dist/html/renderToHtml.d.mts +2 -2
  46. package/dist/html/renderToHtmlStream.d.mts +2 -2
  47. package/dist/html/renderToHtmlStream.mjs +12 -1
  48. package/dist/html/renderers.d.mts +32 -8
  49. package/dist/html/renderers.mjs +125 -111
  50. package/dist/html/streamRenderers.d.mts +4 -5
  51. package/dist/html/streamRenderers.mjs +40 -61
  52. package/dist/{normalise-DVEJQmF7.mjs → normalise-DCYp06Sr.mjs} +352 -162
  53. package/dist/openapi/ApiCallbacks.d.mts +1 -1
  54. package/dist/openapi/ApiLinks.d.mts +1 -1
  55. package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
  56. package/dist/openapi/ApiSecurity.d.mts +1 -1
  57. package/dist/openapi/components.d.mts +116 -37
  58. package/dist/openapi/components.mjs +54 -37
  59. package/dist/openapi/parser.d.mts +19 -9
  60. package/dist/openapi/parser.mjs +262 -86
  61. package/dist/openapi/resolve.d.mts +20 -11
  62. package/dist/openapi/resolve.mjs +135 -75
  63. package/dist/react/SchemaComponent.d.mts +32 -7
  64. package/dist/react/SchemaComponent.mjs +45 -21
  65. package/dist/react/SchemaView.d.mts +30 -10
  66. package/dist/react/a11y.d.mts +21 -0
  67. package/dist/react/a11y.mjs +24 -0
  68. package/dist/react/fieldPath.d.mts +1 -1
  69. package/dist/react/headless.d.mts +1 -1
  70. package/dist/react/headlessRenderers.d.mts +8 -9
  71. package/dist/react/headlessRenderers.mjs +41 -72
  72. package/dist/{ref-D-_JBZkF.d.mts → ref-DCDuswPe.d.mts} +38 -3
  73. package/dist/{renderer-BaRlQIuN.d.mts → renderer-CXJ8y0qw.d.mts} +1 -1
  74. package/dist/themes/mantine.d.mts +1 -1
  75. package/dist/themes/mui.d.mts +1 -1
  76. package/dist/themes/radix.d.mts +1 -1
  77. package/dist/themes/shadcn.d.mts +1 -1
  78. package/dist/themes/shadcn.mjs +2 -1
  79. package/dist/{types-BrRMV0en.d.mts → types-BTB73MB8.d.mts} +32 -4
  80. package/dist/{version-D2jfdX6E.d.mts → version-BFTVLsdb.d.mts} +7 -1
  81. package/package.json +1 -1
  82. package/dist/typeInference-DkcUHfaM.d.mts +0 -982
@@ -1,6 +1,9 @@
1
1
  import { isObject } from "./core/guards.mjs";
2
2
  import { appendPointer, emitDiagnostic } from "./core/diagnostics.mjs";
3
- import { isOpenApi30, isOpenApi31, isSwagger2, readJsonSchemaDialect } from "./core/version.mjs";
3
+ import { RECURSIVE_ANCHOR_SENTINEL } from "./core/ref.mjs";
4
+ import { isOpenApi30, isOpenApi31, isSwagger2, matchJsonSchemaDraftUri, readJsonSchemaDialect } from "./core/version.mjs";
5
+ import { SWAGGER_2_METHODS, rewriteSwaggerRefPrefix } from "./core/openapiConstants.mjs";
6
+ import { resolveRefChain } from "./core/refChain.mjs";
4
7
  //#region src/core/openapi30.ts
5
8
  /**
6
9
  * OpenAPI 3.0.x schema normalisation.
@@ -10,6 +13,29 @@ import { isOpenApi30, isOpenApi31, isSwagger2, readJsonSchemaDialect } from "./c
10
13
  * responses, headers, callbacks, links, examples) to apply normalisation.
11
14
  */
12
15
  /**
16
+ * Lift OpenAPI 3.x singular `example` onto the plural `examples` key.
17
+ *
18
+ * Two output shapes are spec-correct depending on the parent object type:
19
+ * - `"array"` — Schema Object: `examples: [example]` (Draft 2020-12 plural).
20
+ * - `"map"` — Parameter / Header / Media Type Object: an Examples Map
21
+ * keyed by name. The single value is wrapped under the
22
+ * synthetic key `default` to produce a valid one-entry map
23
+ * of one Example Object.
24
+ *
25
+ * When both `example` and `examples` coexist the spec declares them mutually
26
+ * exclusive — `example` is dropped and `examples` wins.
27
+ */
28
+ function liftExampleToExamples(node, shape) {
29
+ if (!("example" in node)) return;
30
+ if ("examples" in node) {
31
+ delete node.example;
32
+ return;
33
+ }
34
+ if (shape === "array") node.examples = [node.example];
35
+ else node.examples = { default: { value: node.example } };
36
+ delete node.example;
37
+ }
38
+ /**
13
39
  * Normalise OpenAPI 3.0.x `nullable` keyword to `anyOf [T, null]`.
14
40
  *
15
41
  * OpenAPI 3.0 uses `nullable: true` instead of the JSON Schema standard
@@ -20,24 +46,32 @@ import { isOpenApi30, isOpenApi31, isSwagger2, readJsonSchemaDialect } from "./c
20
46
  * absent is the default and requires no transformation.
21
47
  */
22
48
  function normaliseOpenApi30Node(node) {
23
- if ("example" in node && !("examples" in node)) {
24
- node.examples = [node.example];
25
- delete node.example;
26
- } else if ("example" in node) delete node.example;
49
+ liftExampleToExamples(node, "array");
27
50
  if (node.nullable !== true) {
28
51
  if ("nullable" in node) delete node.nullable;
29
52
  return node;
30
53
  }
31
54
  const nullOption = { type: "null" };
32
- if (typeof node.$ref === "string") return { anyOf: [{ $ref: node.$ref }, nullOption] };
33
- if (Array.isArray(node.enum) && !node.enum.includes(null)) node.enum = [...node.enum, null];
55
+ if (typeof node.$ref === "string") {
56
+ const wrapper = { anyOf: [{ $ref: node.$ref }, nullOption] };
57
+ for (const key of REF_DOC_SIBLINGS) if (key in node) wrapper[key] = node[key];
58
+ return wrapper;
59
+ }
60
+ if (Array.isArray(node.enum)) {
61
+ if (node.enum.includes(null)) {
62
+ delete node.nullable;
63
+ return node;
64
+ }
65
+ }
34
66
  if (Array.isArray(node.anyOf)) {
35
- node.anyOf = [...node.anyOf, nullOption];
67
+ const existing = node.anyOf;
68
+ node.anyOf = compositeAlreadyAllowsNull(existing) ? existing : [...existing, nullOption];
36
69
  delete node.nullable;
37
70
  return node;
38
71
  }
39
72
  if (Array.isArray(node.oneOf)) {
40
- node.anyOf = [...node.oneOf, nullOption];
73
+ const existing = node.oneOf;
74
+ node.anyOf = compositeAlreadyAllowsNull(existing) ? existing : [...existing, nullOption];
41
75
  delete node.oneOf;
42
76
  delete node.nullable;
43
77
  return node;
@@ -53,6 +87,39 @@ function normaliseOpenApi30Node(node) {
53
87
  return { anyOf: [wrapper, nullOption] };
54
88
  }
55
89
  /**
90
+ * Documentary keys that may legitimately sit alongside a `$ref` in an
91
+ * OpenAPI 3.0 Schema Object. They carry author-facing metadata, not
92
+ * validation semantics, so lifting them onto the `anyOf` wrapper
93
+ * preserves authorial intent without violating the spec rule that a
94
+ * Reference Object itself only carry `$ref`.
95
+ */
96
+ const REF_DOC_SIBLINGS = [
97
+ "description",
98
+ "summary",
99
+ "title",
100
+ "deprecated",
101
+ "readOnly",
102
+ "writeOnly",
103
+ "example",
104
+ "examples",
105
+ "default"
106
+ ];
107
+ /**
108
+ * Returns `true` when at least one option in a composite (`anyOf` /
109
+ * `oneOf`) already permits `null` — either a literal `{ type: "null" }`
110
+ * branch or an `enum` containing `null`. Used to dedup the synthetic
111
+ * null option appended when normalising `nullable: true`.
112
+ */
113
+ function compositeAlreadyAllowsNull(options) {
114
+ for (const option of options) {
115
+ if (!isObject(option)) continue;
116
+ if (option.type === "null") return true;
117
+ if (Array.isArray(option.type) && option.type.includes("null")) return true;
118
+ if (Array.isArray(option.enum) && option.enum.includes(null)) return true;
119
+ }
120
+ return false;
121
+ }
122
+ /**
56
123
  * Normalise OpenAPI 3.0.x `discriminator` keyword by injecting `const`
57
124
  * values into each `oneOf`/`anyOf` option's discriminator property.
58
125
  *
@@ -80,7 +147,7 @@ function normaliseOpenApi30Discriminator(node) {
80
147
  normalisedComposite.push(option);
81
148
  continue;
82
149
  }
83
- const props = isObject(option.properties) ? { ...option.properties } : void 0;
150
+ const props = isObject(option.properties) ? option.properties : void 0;
84
151
  const discProp = props?.[propertyName];
85
152
  if (isObject(discProp) && "const" in discProp) {
86
153
  normalisedComposite.push(option);
@@ -102,7 +169,7 @@ function normaliseOpenApi30Discriminator(node) {
102
169
  if (entry !== void 0) constValue = entry[0];
103
170
  }
104
171
  if (constValue !== void 0) {
105
- const normalisedProps = props ?? {};
172
+ const normalisedProps = { ...props };
106
173
  normalisedProps[propertyName] = {
107
174
  ...isObject(discProp) ? discProp : {},
108
175
  const: constValue
@@ -487,10 +554,7 @@ function normaliseParameter(param, normaliseSchema) {
487
554
  if (isObject(schema)) result.schema = normaliseSchema(schema);
488
555
  const content = param.content;
489
556
  if (isObject(content)) result.content = normaliseContentMap(content, normaliseSchema);
490
- if ("example" in result && !("examples" in result)) {
491
- result.examples = { default: { value: result.example } };
492
- delete result.example;
493
- } else if ("example" in result) delete result.example;
557
+ liftExampleToExamples(result, "map");
494
558
  return result;
495
559
  }
496
560
  function normaliseRequestBody(requestBody, normaliseSchema) {
@@ -513,10 +577,7 @@ function normaliseHeader(header, normaliseSchema) {
513
577
  if (isObject(schema)) result.schema = normaliseSchema(schema);
514
578
  const content = header.content;
515
579
  if (isObject(content)) result.content = normaliseContentMap(content, normaliseSchema);
516
- if ("example" in result && !("examples" in result)) {
517
- result.examples = { default: { value: result.example } };
518
- delete result.example;
519
- } else if ("example" in result) delete result.example;
580
+ liftExampleToExamples(result, "map");
520
581
  return result;
521
582
  }
522
583
  /**
@@ -539,10 +600,7 @@ function normaliseContentMap(content, normaliseSchema) {
539
600
  if (isObject(schema)) normalised.schema = normaliseSchema(schema);
540
601
  const encoding = mediaObj.encoding;
541
602
  if (isObject(encoding)) normalised.encoding = mapObjectValues(encoding, (enc) => isObject(enc) ? normaliseEncoding(enc, normaliseSchema) : enc);
542
- if ("example" in normalised && !("examples" in normalised)) {
543
- normalised.examples = { default: { value: normalised.example } };
544
- delete normalised.example;
545
- } else if ("example" in normalised) delete normalised.example;
603
+ liftExampleToExamples(normalised, "map");
546
604
  result[mediaType] = normalised;
547
605
  }
548
606
  return result;
@@ -639,16 +697,34 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
639
697
  const resolved = resolution.param;
640
698
  const location = resolved.in;
641
699
  if (location === "body") {
642
- if (consumesResolution.source === "synthesised") emitDiagnostic(diagnostics, {
643
- code: "swagger-missing-consumes",
644
- message: "Global body parameter declared but document-level `consumes` is absent; defaulting to application/json",
645
- pointer: appendPointer(appendPointer("", "parameters"), name),
646
- detail: {
647
- level: "document",
648
- name
649
- }
650
- });
651
- requestBodies[name] = buildRequestBody(resolved, globalConsumes);
700
+ const paramPointer = appendPointer(appendPointer("", "parameters"), name);
701
+ let bodyContentTypes;
702
+ if (consumesResolution.source === "synthesised") {
703
+ bodyContentTypes = globalConsumes;
704
+ emitDiagnostic(diagnostics, {
705
+ code: "swagger-missing-consumes",
706
+ message: "Global body parameter declared but document-level `consumes` is absent; defaulting to application/json",
707
+ pointer: paramPointer,
708
+ detail: {
709
+ level: "document",
710
+ name
711
+ }
712
+ });
713
+ } else if (globalConsumes.length === 0) {
714
+ bodyContentTypes = [];
715
+ emitDiagnostic(diagnostics, {
716
+ code: "swagger-missing-consumes",
717
+ message: "Global body parameter declared but document-level `consumes` is an explicit empty array; preserving an empty content map",
718
+ pointer: paramPointer,
719
+ detail: {
720
+ level: "document",
721
+ name,
722
+ reason: "explicitly-cleared",
723
+ source: consumesResolution.source
724
+ }
725
+ });
726
+ } else bodyContentTypes = globalConsumes;
727
+ requestBodies[name] = buildRequestBody(resolved, bodyContentTypes);
652
728
  } else if (location === "formData") requestBodies[name] = buildRequestBody(buildFormDataBody(resolved, [resolved]), formDataContentTypes(globalConsumes));
653
729
  else {
654
730
  const normalised = normaliseSwaggerParameter(resolved, doc, diagnostics, appendPointer(appendPointer("", "parameters"), name));
@@ -668,7 +744,11 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
668
744
  const securityDefinitions = doc.securityDefinitions;
669
745
  if (isObject(securityDefinitions)) {
670
746
  const translated = {};
671
- for (const [name, scheme] of Object.entries(securityDefinitions)) translated[name] = isObject(scheme) ? translateSwaggerSecurityScheme(scheme) : scheme;
747
+ const securityDefinitionsPointer = appendPointer("", "securityDefinitions");
748
+ for (const [name, scheme] of Object.entries(securityDefinitions)) {
749
+ const schemePointer = appendPointer(securityDefinitionsPointer, name);
750
+ translated[name] = isObject(scheme) ? translateSwaggerSecurityScheme(scheme, diagnostics, schemePointer, name) : scheme;
751
+ }
672
752
  components.securitySchemes = translated;
673
753
  }
674
754
  if (Object.keys(components).length > 0) result.components = components;
@@ -676,7 +756,7 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
676
756
  if (isObject(doc.externalDocs)) result.externalDocs = doc.externalDocs;
677
757
  if (Array.isArray(doc.security)) result.security = doc.security;
678
758
  rewriteSwaggerRefs(result);
679
- if (hasXmlAnywhere(doc.definitions) || hasXmlAnywhere(doc.paths) || hasXmlAnywhere(doc.parameters) || hasXmlAnywhere(doc.responses)) emitDiagnostic(diagnostics, {
759
+ if (documentContainsKeyword(doc.definitions, "xml") || documentContainsKeyword(doc.paths, "xml") || documentContainsKeyword(doc.parameters, "xml") || documentContainsKeyword(doc.responses, "xml")) emitDiagnostic(diagnostics, {
680
760
  code: "dropped-swagger-feature",
681
761
  message: "Swagger 2.0 xml markup is not supported and will be dropped",
682
762
  pointer: "",
@@ -686,22 +766,13 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
686
766
  }
687
767
  function normaliseSwaggerPaths(paths, doc, diagnostics) {
688
768
  const result = {};
689
- const METHODS = [
690
- "get",
691
- "post",
692
- "put",
693
- "patch",
694
- "delete",
695
- "head",
696
- "options"
697
- ];
698
769
  for (const [path, pathItem] of Object.entries(paths)) {
699
770
  if (!isObject(pathItem)) {
700
771
  result[path] = pathItem;
701
772
  continue;
702
773
  }
703
774
  const normalisedPath = {};
704
- for (const method of METHODS) {
775
+ for (const method of SWAGGER_2_METHODS) {
705
776
  const operation = pathItem[method];
706
777
  if (!isObject(operation)) continue;
707
778
  normalisedPath[method] = normaliseSwaggerOperation(operation, doc, path, method, diagnostics);
@@ -783,16 +854,34 @@ function normaliseSwaggerOperation(operation, doc, path, method, diagnostics) {
783
854
  }
784
855
  if (nonBodyParams.length > 0) result.parameters = nonBodyParams;
785
856
  if (bodyParam !== void 0) {
786
- const bodyContentTypes = usesFormData ? formDataContentTypes(consumes) : consumes;
787
- if (!usesFormData && consumesResolution.source === "synthesised") emitDiagnostic(diagnostics, {
788
- code: "swagger-missing-consumes",
789
- message: "Operation declares a body parameter but neither operation-level nor document-level `consumes` is set; defaulting to application/json",
790
- pointer: appendPointer(appendPointer(appendPointer("", "paths"), path), method),
791
- detail: {
792
- level: "operation",
793
- method
794
- }
795
- });
857
+ const operationPointer = appendPointer(appendPointer(appendPointer("", "paths"), path), method);
858
+ let bodyContentTypes;
859
+ if (usesFormData) bodyContentTypes = formDataContentTypes(consumes);
860
+ else if (consumesResolution.source === "synthesised") {
861
+ bodyContentTypes = consumes;
862
+ emitDiagnostic(diagnostics, {
863
+ code: "swagger-missing-consumes",
864
+ message: "Operation declares a body parameter but neither operation-level nor document-level `consumes` is set; defaulting to application/json",
865
+ pointer: operationPointer,
866
+ detail: {
867
+ level: "operation",
868
+ method
869
+ }
870
+ });
871
+ } else if (consumes.length === 0) {
872
+ bodyContentTypes = [];
873
+ emitDiagnostic(diagnostics, {
874
+ code: "swagger-missing-consumes",
875
+ message: "Operation declares a body parameter but `consumes` is an explicit empty array; preserving an empty content map",
876
+ pointer: operationPointer,
877
+ detail: {
878
+ level: "operation",
879
+ method,
880
+ reason: "explicitly-cleared",
881
+ source: consumesResolution.source
882
+ }
883
+ });
884
+ } else bodyContentTypes = consumes;
796
885
  result.requestBody = buildRequestBody(bodyParam, bodyContentTypes);
797
886
  }
798
887
  }
@@ -895,34 +984,37 @@ function buildSchemaFromSwaggerParameterShape(node) {
895
984
  * decide how to surface the failure. Non-ref parameters resolve to
896
985
  * themselves; ref targets that don't exist also resolve to the input
897
986
  * (the caller treats unknown refs the same as bare parameters).
898
- */
899
- function resolveSwaggerParameter(param, doc, visited = /* @__PURE__ */ new Set()) {
900
- const ref = param.$ref;
901
- if (typeof ref !== "string" || !ref.startsWith("#/parameters/")) return {
902
- kind: "ok",
903
- param
904
- };
905
- if (visited.has(ref)) return {
987
+ *
988
+ * Uses the shared {@link resolveRefChain} helper so cycle detection
989
+ * and hop accounting stay consistent with the other OpenAPI $ref
990
+ * walkers.
991
+ */
992
+ function resolveSwaggerParameter(param, doc) {
993
+ let cyclicRef;
994
+ const finalNode = resolveRefChain(param, {
995
+ extractRef: (node) => {
996
+ const ref = node.$ref;
997
+ if (typeof ref !== "string") return void 0;
998
+ return ref.startsWith("#/parameters/") ? ref : void 0;
999
+ },
1000
+ lookup: (ref) => {
1001
+ const name = ref.slice(13);
1002
+ const globalParams = doc.parameters;
1003
+ if (!isObject(globalParams)) return void 0;
1004
+ const resolved = globalParams[name];
1005
+ return isObject(resolved) ? resolved : void 0;
1006
+ },
1007
+ onCycle: (ref) => {
1008
+ cyclicRef = ref;
1009
+ }
1010
+ });
1011
+ if (cyclicRef !== void 0) return {
906
1012
  kind: "cycle",
907
- ref
1013
+ ref: cyclicRef
908
1014
  };
909
- const nextVisited = new Set(visited);
910
- nextVisited.add(ref);
911
- const name = ref.slice(13);
912
- const globalParams = doc.parameters;
913
- if (isObject(globalParams)) {
914
- const resolved = globalParams[name];
915
- if (isObject(resolved)) {
916
- if (typeof resolved.$ref === "string") return resolveSwaggerParameter(resolved, doc, nextVisited);
917
- return {
918
- kind: "ok",
919
- param: resolved
920
- };
921
- }
922
- }
923
1015
  return {
924
1016
  kind: "ok",
925
- param
1017
+ param: finalNode ?? param
926
1018
  };
927
1019
  }
928
1020
  /**
@@ -966,7 +1058,7 @@ function normaliseSwaggerParameter(param, doc, diagnostics, pointer = "") {
966
1058
  const cf = param.collectionFormat;
967
1059
  if (typeof cf === "string") switch (cf) {
968
1060
  case "csv":
969
- result.style = "form";
1061
+ result.style = param.in === "path" || param.in === "header" ? "simple" : "form";
970
1062
  result.explode = false;
971
1063
  break;
972
1064
  case "ssv":
@@ -974,8 +1066,15 @@ function normaliseSwaggerParameter(param, doc, diagnostics, pointer = "") {
974
1066
  result.explode = false;
975
1067
  break;
976
1068
  case "tsv":
977
- result.style = "tabDelimited";
978
- result.explode = false;
1069
+ emitDiagnostic(diagnostics, {
1070
+ code: "swagger-collection-format-dropped",
1071
+ message: "Swagger 2.0 collectionFormat: \"tsv\" has no OpenAPI 3.x equivalent; dropping the keyword",
1072
+ pointer,
1073
+ detail: {
1074
+ feature: "collectionFormat:tsv",
1075
+ location: "parameter"
1076
+ }
1077
+ });
979
1078
  break;
980
1079
  case "pipes":
981
1080
  result.style = "pipeDelimited";
@@ -1022,12 +1121,18 @@ function buildFormDataBody(param, allParams) {
1022
1121
  }
1023
1122
  /**
1024
1123
  * Build an OpenAPI 3.x request body from a Swagger 2.0 body parameter.
1124
+ *
1125
+ * `consumes` is taken at face value — the caller is responsible for
1126
+ * deciding whether an absent value should fall back to a default
1127
+ * (and emitting `swagger-missing-consumes`) or be preserved as an
1128
+ * empty content map (an explicit clear). Inventing a default here
1129
+ * would mask the difference and silently override the upstream
1130
+ * resolution.
1025
1131
  */
1026
1132
  function buildRequestBody(bodyParam, consumes) {
1027
1133
  const schema = bodyParam.schema;
1028
1134
  const content = {};
1029
- const contentTypes = consumes.length > 0 ? consumes : ["application/json"];
1030
- for (const ct of contentTypes) if (typeof ct === "string") content[ct] = isObject(schema) ? { schema } : {};
1135
+ for (const ct of consumes) if (typeof ct === "string") content[ct] = isObject(schema) ? { schema } : {};
1031
1136
  const result = { content };
1032
1137
  if (bodyParam.required === true) result.required = true;
1033
1138
  if (typeof bodyParam.description === "string") result.description = bodyParam.description;
@@ -1035,23 +1140,28 @@ function buildRequestBody(bodyParam, consumes) {
1035
1140
  }
1036
1141
  /**
1037
1142
  * Resolve a Swagger 2.0 response `$ref` (e.g. `#/responses/NotFound`).
1038
- */
1039
- function resolveSwaggerResponse(response, doc, visited = /* @__PURE__ */ new Set()) {
1040
- const ref = response.$ref;
1041
- if (typeof ref !== "string" || !ref.startsWith("#/responses/")) return response;
1042
- if (visited.has(ref)) return response;
1043
- const nextVisited = new Set(visited);
1044
- nextVisited.add(ref);
1045
- const name = ref.slice(12);
1046
- const globalResponses = doc.responses;
1047
- if (isObject(globalResponses)) {
1048
- const resolved = globalResponses[name];
1049
- if (isObject(resolved)) {
1050
- if (typeof resolved.$ref === "string") return resolveSwaggerResponse(resolved, doc, nextVisited);
1051
- return resolved;
1052
- }
1053
- }
1054
- return response;
1143
+ *
1144
+ * Uses the shared {@link resolveRefChain} helper so cycle detection
1145
+ * and hop accounting stay consistent with the other OpenAPI $ref
1146
+ * walkers. On cycle the original wrapper is returned (legacy
1147
+ * behaviour), preserving the existing response-resolution contract.
1148
+ */
1149
+ function resolveSwaggerResponse(response, doc) {
1150
+ return resolveRefChain(response, {
1151
+ extractRef: (node) => {
1152
+ const ref = node.$ref;
1153
+ if (typeof ref !== "string") return void 0;
1154
+ return ref.startsWith("#/responses/") ? ref : void 0;
1155
+ },
1156
+ lookup: (ref) => {
1157
+ const name = ref.slice(12);
1158
+ const globalResponses = doc.responses;
1159
+ if (!isObject(globalResponses)) return void 0;
1160
+ const resolved = globalResponses[name];
1161
+ return isObject(resolved) ? resolved : void 0;
1162
+ },
1163
+ onCycle: () => response
1164
+ }) ?? response;
1055
1165
  }
1056
1166
  function normaliseSwaggerResponses(responses, doc, produces, producesSource, diagnostics, path, method) {
1057
1167
  const result = {};
@@ -1096,7 +1206,11 @@ function normaliseSwaggerSingleResponse(response, doc, produces, producesSource
1096
1206
  const headers = resolved.headers;
1097
1207
  if (isObject(headers)) {
1098
1208
  const convertedHeaders = {};
1099
- for (const [name, header] of Object.entries(headers)) convertedHeaders[name] = isObject(header) ? normaliseSwaggerHeader(header) : header;
1209
+ const headersPointer = appendPointer(path !== void 0 && method !== void 0 && statusCode !== void 0 ? appendPointer(appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "responses"), statusCode) : "", "headers");
1210
+ for (const [name, header] of Object.entries(headers)) {
1211
+ const headerPointer = appendPointer(headersPointer, name);
1212
+ convertedHeaders[name] = isObject(header) ? normaliseSwaggerHeader(header, diagnostics, headerPointer) : header;
1213
+ }
1100
1214
  normalised.headers = convertedHeaders;
1101
1215
  }
1102
1216
  return normalised;
@@ -1114,7 +1228,7 @@ function normaliseSwaggerSingleResponse(response, doc, produces, producesSource
1114
1228
  * `simple`/`explode: false` rather than the `form` style used for query
1115
1229
  * parameters.
1116
1230
  */
1117
- function normaliseSwaggerHeader(header) {
1231
+ function normaliseSwaggerHeader(header, diagnostics, pointer = "") {
1118
1232
  const result = {};
1119
1233
  for (const [key, value] of Object.entries(header)) {
1120
1234
  if (PARAM_KEYWORDS_LIFTED_INTO_SCHEMA.has(key)) continue;
@@ -1132,8 +1246,15 @@ function normaliseSwaggerHeader(header) {
1132
1246
  result.explode = false;
1133
1247
  break;
1134
1248
  case "tsv":
1135
- result.style = "tabDelimited";
1136
- result.explode = false;
1249
+ emitDiagnostic(diagnostics, {
1250
+ code: "swagger-collection-format-dropped",
1251
+ message: "Swagger 2.0 collectionFormat: \"tsv\" has no OpenAPI 3.x equivalent; dropping the keyword",
1252
+ pointer,
1253
+ detail: {
1254
+ feature: "collectionFormat:tsv",
1255
+ location: "header"
1256
+ }
1257
+ });
1137
1258
  break;
1138
1259
  case "pipes":
1139
1260
  result.style = "pipeDelimited";
@@ -1143,29 +1264,15 @@ function normaliseSwaggerHeader(header) {
1143
1264
  return result;
1144
1265
  }
1145
1266
  /**
1146
- * Mapping of Swagger 2.0 $ref prefixes to OpenAPI 3.x equivalents.
1147
- * Applied after document restructuring so all $ref strings point
1148
- * to the correct locations in the normalised document.
1149
- */
1150
- const REF_REWRITES = [
1151
- ["#/definitions/", "#/components/schemas/"],
1152
- ["#/parameters/", "#/components/parameters/"],
1153
- ["#/responses/", "#/components/responses/"]
1154
- ];
1155
- /**
1156
1267
  * Deep-rewrite $ref strings in a normalised Swagger 2.0 document
1157
- * from Swagger 2.0 locations to OpenAPI 3.x locations.
1158
- * Mutates the object in place \u2014 called only on the fresh clone
1159
- * produced by normaliseSwagger2Document.
1268
+ * from Swagger 2.0 locations to OpenAPI 3.x locations using the
1269
+ * shared {@link rewriteSwaggerRefPrefix} mapping. Mutates the object
1270
+ * in place \u2014 called only on the fresh clone produced by
1271
+ * normaliseSwagger2Document.
1160
1272
  */
1161
1273
  function rewriteSwaggerRefs(node) {
1162
1274
  if (!isObject(node)) return;
1163
- if (typeof node.$ref === "string") {
1164
- for (const [from, to] of REF_REWRITES) if (node.$ref.startsWith(from)) {
1165
- node.$ref = to + node.$ref.slice(from.length);
1166
- break;
1167
- }
1168
- }
1275
+ if (typeof node.$ref === "string") node.$ref = rewriteSwaggerRefPrefix(node.$ref);
1169
1276
  for (const value of Object.values(node)) if (isObject(value)) rewriteSwaggerRefs(value);
1170
1277
  else if (Array.isArray(value)) for (const item of value) rewriteSwaggerRefs(item);
1171
1278
  }
@@ -1195,7 +1302,7 @@ const SWAGGER_OAUTH_FLOW_RENAME = {
1195
1302
  * (`unknown-security-scheme-type` diagnostic in the parser) handles
1196
1303
  * those cases.
1197
1304
  */
1198
- function translateSwaggerSecurityScheme(scheme) {
1305
+ function translateSwaggerSecurityScheme(scheme, diagnostics, pointer = "", name) {
1199
1306
  const type = scheme.type;
1200
1307
  if (type === "basic") {
1201
1308
  const result = {
@@ -1207,10 +1314,21 @@ function translateSwaggerSecurityScheme(scheme) {
1207
1314
  }
1208
1315
  if (type === "oauth2") {
1209
1316
  const flowName = scheme.flow;
1210
- if (typeof flowName !== "string") return {
1211
- ...scheme,
1212
- type: "oauth2"
1213
- };
1317
+ if (typeof flowName !== "string") {
1318
+ emitDiagnostic(diagnostics, {
1319
+ code: "swagger-malformed-oauth-flow",
1320
+ message: `Swagger 2.0 oauth2 security scheme${name !== void 0 ? ` "${name}"` : ""} is missing the required \`flow\` field; preserving the original shape verbatim`,
1321
+ pointer,
1322
+ detail: {
1323
+ name,
1324
+ flow: flowName
1325
+ }
1326
+ });
1327
+ return {
1328
+ ...scheme,
1329
+ type: "oauth2"
1330
+ };
1331
+ }
1214
1332
  const renamedFlow = SWAGGER_OAUTH_FLOW_RENAME[flowName] ?? flowName;
1215
1333
  const flowBody = {};
1216
1334
  if (typeof scheme.authorizationUrl === "string") flowBody.authorizationUrl = scheme.authorizationUrl;
@@ -1227,24 +1345,6 @@ function translateSwaggerSecurityScheme(scheme) {
1227
1345
  }
1228
1346
  return { ...scheme };
1229
1347
  }
1230
- /**
1231
- * Recursively check whether any node in the supplied subtree carries an
1232
- * `xml` annotation. Walks both objects and arrays so the check works for
1233
- * schemas (definitions, parameter schemas, response schemas, request body
1234
- * schemas) as well as operations and parameters that may carry `xml`
1235
- * metadata at any depth.
1236
- */
1237
- function hasXmlAnywhere(node) {
1238
- if (!isObject(node)) {
1239
- if (Array.isArray(node)) {
1240
- for (const item of node) if (hasXmlAnywhere(item)) return true;
1241
- }
1242
- return false;
1243
- }
1244
- if ("xml" in node) return true;
1245
- for (const value of Object.values(node)) if (hasXmlAnywhere(value)) return true;
1246
- return false;
1247
- }
1248
1348
  //#endregion
1249
1349
  //#region src/core/normalise.ts
1250
1350
  /**
@@ -1432,17 +1532,24 @@ function collectDependencyStrings(items, property, keyword, ctx) {
1432
1532
  * After splitting, `dependencies` is removed from the node.
1433
1533
  *
1434
1534
  * When `ctx` is supplied, diagnostics are emitted for:
1435
- * - `legacy-dependencies-split` once per node that contained the
1436
- * deprecated keyword (callers pass this only on draft paths where
1437
- * the keyword is unexpected, e.g. 2020-12).
1535
+ * - The supplied `legacyDiagnostic` code (if any) once per node that
1536
+ * contained the deprecated keyword. Callers pass
1537
+ * `legacy-dependencies-split-2019` on the 2019-09 path (the draft
1538
+ * that introduced the split — authors should already have migrated)
1539
+ * and `legacy-dependencies-split` on the defensive 2020-12 path.
1438
1540
  * - `dependent-required-invalid` for each array entry whose element is
1439
1541
  * not a string.
1542
+ * - `dependencies-conflict` when both the legacy `dependencies` keyword
1543
+ * and a pre-existing `dependentRequired`/`dependentSchemas` declare
1544
+ * the same key with different values. The pre-existing modern
1545
+ * keyword wins and the legacy value is discarded — silently
1546
+ * overwriting would mask the author's bug.
1440
1547
  */
1441
- function splitDependencies(node, ctx, emitLegacyDiagnostic) {
1548
+ function splitDependencies(node, ctx, legacyDiagnostic) {
1442
1549
  const deps = node.dependencies;
1443
1550
  if (!isObject(deps)) return;
1444
- if (emitLegacyDiagnostic && ctx !== void 0) emitDiagnostic(ctx.diagnostics, {
1445
- code: "legacy-dependencies-split",
1551
+ if (legacyDiagnostic !== void 0 && ctx !== void 0) emitDiagnostic(ctx.diagnostics, {
1552
+ code: legacyDiagnostic,
1446
1553
  message: "Legacy `dependencies` keyword was split into `dependentRequired`/`dependentSchemas`; `dependencies` was deprecated in Draft 2019-09",
1447
1554
  pointer: appendPointer(ctx.pointer, "dependencies"),
1448
1555
  detail: { keys: Object.keys(deps) }
@@ -1455,17 +1562,61 @@ function splitDependencies(node, ctx, emitLegacyDiagnostic) {
1455
1562
  } else if (isObject(value)) schemaEntries[key] = value;
1456
1563
  if (Object.keys(requiredEntries).length > 0) {
1457
1564
  const existing = node.dependentRequired;
1458
- if (isObject(existing)) for (const [k, v] of Object.entries(requiredEntries)) existing[k] = v;
1565
+ if (isObject(existing)) for (const [k, v] of Object.entries(requiredEntries)) {
1566
+ if (k in existing) {
1567
+ if (ctx !== void 0 && !arraysEqual(existing[k], v)) emitDiagnostic(ctx.diagnostics, {
1568
+ code: "dependencies-conflict",
1569
+ message: `Legacy \`dependencies.${k}\` conflicts with the pre-existing \`dependentRequired.${k}\`; keeping the modern keyword and discarding the legacy value`,
1570
+ pointer: appendPointer(ctx.pointer, "dependencies"),
1571
+ detail: {
1572
+ key: k,
1573
+ kept: existing[k],
1574
+ discarded: v,
1575
+ keyword: "dependentRequired"
1576
+ }
1577
+ });
1578
+ continue;
1579
+ }
1580
+ existing[k] = v;
1581
+ }
1459
1582
  else node.dependentRequired = requiredEntries;
1460
1583
  }
1461
1584
  if (Object.keys(schemaEntries).length > 0) {
1462
1585
  const existing = node.dependentSchemas;
1463
- if (isObject(existing)) for (const [k, v] of Object.entries(schemaEntries)) existing[k] = v;
1586
+ if (isObject(existing)) for (const [k, v] of Object.entries(schemaEntries)) {
1587
+ if (k in existing) {
1588
+ if (ctx !== void 0) emitDiagnostic(ctx.diagnostics, {
1589
+ code: "dependencies-conflict",
1590
+ message: `Legacy \`dependencies.${k}\` conflicts with the pre-existing \`dependentSchemas.${k}\`; keeping the modern keyword and discarding the legacy value`,
1591
+ pointer: appendPointer(ctx.pointer, "dependencies"),
1592
+ detail: {
1593
+ key: k,
1594
+ kept: existing[k],
1595
+ discarded: v,
1596
+ keyword: "dependentSchemas"
1597
+ }
1598
+ });
1599
+ continue;
1600
+ }
1601
+ existing[k] = v;
1602
+ }
1464
1603
  else node.dependentSchemas = schemaEntries;
1465
1604
  }
1466
1605
  delete node.dependencies;
1467
1606
  }
1468
1607
  /**
1608
+ * Compare two values that should both be string arrays. Returns true
1609
+ * when both are arrays of equal length holding the same strings in
1610
+ * the same order. Used to skip the `dependencies-conflict` diagnostic
1611
+ * when the legacy and modern keywords agree.
1612
+ */
1613
+ function arraysEqual(a, b) {
1614
+ if (!Array.isArray(a) || !Array.isArray(b)) return false;
1615
+ if (a.length !== b.length) return false;
1616
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
1617
+ return true;
1618
+ }
1619
+ /**
1469
1620
  * Emit diagnostics for any non-string entries inside a pre-existing
1470
1621
  * `dependentRequired` keyword. Used on draft paths where the author may
1471
1622
  * have already migrated to the Draft 2019-09 form but still produced
@@ -1563,7 +1714,7 @@ function applyDraft04Translations(node, ctx) {
1563
1714
  delete node.id;
1564
1715
  }
1565
1716
  translateTupleItems(node);
1566
- splitDependencies(node, ctx, false);
1717
+ splitDependencies(node, ctx, void 0);
1567
1718
  validateDependentRequired(node, ctx);
1568
1719
  }
1569
1720
  /**
@@ -1616,7 +1767,7 @@ function normaliseDraft04NodeWithContext(node, ctx) {
1616
1767
  */
1617
1768
  function normaliseDraft06Or07NodeWithContext(node, ctx) {
1618
1769
  translateTupleItems(node);
1619
- splitDependencies(node, ctx, false);
1770
+ splitDependencies(node, ctx, void 0);
1620
1771
  validateDependentRequired(node, ctx);
1621
1772
  return node;
1622
1773
  }
@@ -1635,6 +1786,12 @@ function normaliseDraft06Or07NodeWithContext(node, ctx) {
1635
1786
  * `$recursiveAnchor` names are likewise preserved as `$anchor`.
1636
1787
  */
1637
1788
  function normaliseDraft201909NodeWithContext(node, ctx) {
1789
+ if (node.$anchor === "__recursive__" && node.$recursiveAnchor !== true) emitDiagnostic(ctx.diagnostics, {
1790
+ code: "recursive-anchor-collision",
1791
+ message: `User-supplied \`$anchor: "${RECURSIVE_ANCHOR_SENTINEL}"\` collides with the canonical sentinel synthesised by the Draft 2019-09 \`$recursiveAnchor: true\` rewrite; lookups for this anchor may resolve to either declaration`,
1792
+ pointer: appendPointer(ctx.pointer, "$anchor"),
1793
+ detail: { anchor: RECURSIVE_ANCHOR_SENTINEL }
1794
+ });
1638
1795
  const recursiveRef = node.$recursiveRef;
1639
1796
  if (typeof recursiveRef === "string") {
1640
1797
  if (!recursiveRef.startsWith("#")) emitDiagnostic(ctx.diagnostics, {
@@ -1650,13 +1807,13 @@ function normaliseDraft201909NodeWithContext(node, ctx) {
1650
1807
  delete node.$recursiveRef;
1651
1808
  }
1652
1809
  if (node.$recursiveAnchor === true) {
1653
- if (typeof node.$anchor !== "string") node.$anchor = "__recursive__";
1810
+ if (typeof node.$anchor !== "string") node.$anchor = RECURSIVE_ANCHOR_SENTINEL;
1654
1811
  delete node.$recursiveAnchor;
1655
1812
  } else if (typeof node.$recursiveAnchor === "string") {
1656
1813
  if (typeof node.$anchor !== "string") node.$anchor = node.$recursiveAnchor;
1657
1814
  delete node.$recursiveAnchor;
1658
1815
  }
1659
- splitDependencies(node, ctx, false);
1816
+ splitDependencies(node, ctx, "legacy-dependencies-split-2019");
1660
1817
  validateDependentRequired(node, ctx);
1661
1818
  return node;
1662
1819
  }
@@ -1680,7 +1837,8 @@ function normaliseDynamicRefNodeWithContext(node, ctx) {
1680
1837
  if (typeof node.$anchor !== "string") node.$anchor = node.$dynamicAnchor;
1681
1838
  delete node.$dynamicAnchor;
1682
1839
  }
1683
- splitDependencies(node, ctx, true);
1840
+ translateTupleItems(node);
1841
+ splitDependencies(node, ctx, "legacy-dependencies-split");
1684
1842
  validateDependentRequired(node, ctx);
1685
1843
  return node;
1686
1844
  }
@@ -1948,9 +2106,41 @@ function normaliseOpenApiSchemas(doc, version, diagnostics) {
1948
2106
  }
1949
2107
  return deepNormaliseOpenApiDoc(applyDiscriminatorAllOfPrepass(doc), (schema) => {
1950
2108
  let intermediate = schema;
1951
- if (dialectDraft !== void 0 && dialectDraft !== "draft-2020-12") intermediate = deepNormaliseWithContext(intermediate, selectDraftTransform(dialectDraft), buildRootContext(intermediate, diagnostics, dialectDraft));
2109
+ const effectiveDraft = resolveSchemaLocalDialect(intermediate, diagnostics) ?? dialectDraft;
2110
+ if (effectiveDraft !== void 0 && effectiveDraft !== "draft-2020-12") intermediate = deepNormaliseWithContext(intermediate, selectDraftTransform(effectiveDraft), buildRootContext(intermediate, diagnostics, effectiveDraft));
1952
2111
  return resolveRelativeRefs(deepNormalise(intermediate, normaliseOpenApi30Discriminator), diagnostics);
1953
2112
  });
1954
2113
  }
2114
+ /**
2115
+ * Resolve a Schema-Object-level `$schema` declaration to a
2116
+ * `JsonSchemaDraft`. Returns `undefined` when the keyword is absent or
2117
+ * its value is not a string; emits `unknown-json-schema-dialect` and
2118
+ * returns `undefined` when the value is a string that does not match
2119
+ * any supported draft URI. The caller falls back to the document-level
2120
+ * dialect (or 2020-12) in that case.
2121
+ *
2122
+ * Per OpenAPI 3.1 §4.7.5, the override applies to the Schema Object
2123
+ * and its subschemas. Nested overrides inside individual sub-schemas
2124
+ * are not currently honoured — the dispatch happens once at the top
2125
+ * of each Schema Object surfaced by `deepNormaliseOpenApiDoc`.
2126
+ */
2127
+ function resolveSchemaLocalDialect(schema, diagnostics) {
2128
+ const value = schema.$schema;
2129
+ if (typeof value !== "string") return void 0;
2130
+ const draft = matchJsonSchemaDraftUri(value);
2131
+ if (draft === void 0) {
2132
+ emitDiagnostic(diagnostics, {
2133
+ code: "unknown-json-schema-dialect",
2134
+ message: `Schema Object-level \`$schema\` URI "${value}" does not match a supported JSON Schema draft; falling back to the document dialect`,
2135
+ pointer: "/$schema",
2136
+ detail: {
2137
+ uri: value,
2138
+ level: "schema-object"
2139
+ }
2140
+ });
2141
+ return;
2142
+ }
2143
+ return draft;
2144
+ }
1955
2145
  //#endregion
1956
- export { normaliseOpenApiSchemas as a, applyDiscriminatorAllOfPrepass as c, normaliseOpenApi30Combined as d, normaliseOpenApi30Discriminator as f, normaliseJsonSchema as i, deepNormaliseOpenApi30Doc as l, deepNormaliseWithContext as n, selectDraftTransform as o, normaliseOpenApi30Node as p, normaliseDraft04Node as r, normaliseSwagger2Document as s, deepNormalise as t, deepNormaliseOpenApiDoc as u };
2146
+ export { normaliseJsonSchema as a, normaliseSwagger2Document as c, deepNormaliseOpenApiDoc as d, liftExampleToExamples as f, normaliseOpenApi30Node as h, normaliseDraft04Node as i, applyDiscriminatorAllOfPrepass as l, normaliseOpenApi30Discriminator as m, deepNormaliseWithContext as n, normaliseOpenApiSchemas as o, normaliseOpenApi30Combined as p, documentContainsKeyword as r, selectDraftTransform as s, deepNormalise as t, deepNormaliseOpenApi30Doc as u };