docusaurus-theme-openapi-docs 0.0.0-1313 → 0.0.0-1330
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/lib/theme/Schema/index.js +74 -173
- package/lib/theme/Schema/normalize.d.ts +41 -0
- package/lib/theme/Schema/normalize.js +210 -0
- package/lib/theme/Schema/normalize.test.d.ts +1 -0
- package/lib/theme/Schema/normalize.test.js +271 -0
- package/package.json +3 -3
- package/src/theme/Schema/index.tsx +78 -186
- package/src/theme/Schema/normalize.test.ts +288 -0
- package/src/theme/Schema/normalize.ts +224 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* ============================================================================
|
|
3
|
+
* Copyright (c) Palo Alto Networks
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
* ========================================================================== */
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
const normalize_1 = require("./normalize");
|
|
10
|
+
describe("normalizeSchema", () => {
|
|
11
|
+
it("returns the input as-is (identity-stable pass-through)", () => {
|
|
12
|
+
const schema = { type: "object", properties: { a: { type: "string" } } };
|
|
13
|
+
expect((0, normalize_1.normalizeSchema)(schema)).toBe(schema);
|
|
14
|
+
});
|
|
15
|
+
it("handles null, undefined, and primitives", () => {
|
|
16
|
+
expect((0, normalize_1.normalizeSchema)(null)).toBeNull();
|
|
17
|
+
expect((0, normalize_1.normalizeSchema)(undefined)).toBeUndefined();
|
|
18
|
+
expect((0, normalize_1.normalizeSchema)("string")).toBe("string");
|
|
19
|
+
expect((0, normalize_1.normalizeSchema)(42)).toBe(42);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe("mergeAllOf", () => {
|
|
23
|
+
it("merges allOf members into a flat schema", () => {
|
|
24
|
+
const schema = {
|
|
25
|
+
allOf: [
|
|
26
|
+
{ type: "object", properties: { a: { type: "string" } } },
|
|
27
|
+
{ type: "object", properties: { b: { type: "number" } } },
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
const result = (0, normalize_1.mergeAllOf)(schema);
|
|
31
|
+
expect(result.allOf).toBeUndefined();
|
|
32
|
+
expect(result.properties.a).toEqual({ type: "string" });
|
|
33
|
+
expect(result.properties.b).toEqual({ type: "number" });
|
|
34
|
+
});
|
|
35
|
+
it("strips conflicting additionalProperties: false from allOf members", () => {
|
|
36
|
+
const schema = {
|
|
37
|
+
allOf: [
|
|
38
|
+
{
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: { a: { type: "string" } },
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: { b: { type: "number" } },
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
const result = (0, normalize_1.mergeAllOf)(schema);
|
|
51
|
+
expect(result.properties.a).toBeDefined();
|
|
52
|
+
expect(result.properties.b).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
it("preserves both properties and oneOf when merging incompatible-shape members", () => {
|
|
55
|
+
const result = (0, normalize_1.mergeAllOf)({
|
|
56
|
+
allOf: [
|
|
57
|
+
{ type: "object", properties: { id: { type: "string" } } },
|
|
58
|
+
{ oneOf: [{ title: "a" }, { title: "b" }] },
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
expect(result.properties.id).toBeDefined();
|
|
62
|
+
expect(result.oneOf).toHaveLength(2);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe("foldSiblingsIntoBranches", () => {
|
|
66
|
+
it("returns schema unchanged when no oneOf/anyOf is present", () => {
|
|
67
|
+
const schema = { type: "object", properties: { a: { type: "string" } } };
|
|
68
|
+
expect((0, normalize_1.foldSiblingsIntoBranches)(schema)).toBe(schema);
|
|
69
|
+
});
|
|
70
|
+
it("returns schema unchanged when branches have no siblings", () => {
|
|
71
|
+
const schema = {
|
|
72
|
+
oneOf: [{ properties: { a: { type: "string" } } }],
|
|
73
|
+
};
|
|
74
|
+
expect((0, normalize_1.foldSiblingsIntoBranches)(schema)).toBe(schema);
|
|
75
|
+
});
|
|
76
|
+
it("returns schema unchanged when a discriminator is present", () => {
|
|
77
|
+
const schema = {
|
|
78
|
+
discriminator: { propertyName: "type" },
|
|
79
|
+
properties: { shared: { type: "string" } },
|
|
80
|
+
oneOf: [{ properties: { a: { type: "number" } } }],
|
|
81
|
+
};
|
|
82
|
+
expect((0, normalize_1.foldSiblingsIntoBranches)(schema)).toBe(schema);
|
|
83
|
+
});
|
|
84
|
+
it("folds sibling properties into oneOf branches", () => {
|
|
85
|
+
const schema = {
|
|
86
|
+
properties: { shared: { type: "string" } },
|
|
87
|
+
oneOf: [
|
|
88
|
+
{ properties: { a: { type: "number" } } },
|
|
89
|
+
{ properties: { b: { type: "boolean" } } },
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
const result = (0, normalize_1.foldSiblingsIntoBranches)(schema);
|
|
93
|
+
expect(result.properties).toBeUndefined();
|
|
94
|
+
expect(result.oneOf).toHaveLength(2);
|
|
95
|
+
expect(result.oneOf[0].properties.shared).toBeDefined();
|
|
96
|
+
expect(result.oneOf[0].properties.a).toBeDefined();
|
|
97
|
+
});
|
|
98
|
+
it("folds sibling properties into anyOf branches", () => {
|
|
99
|
+
const schema = {
|
|
100
|
+
properties: { shared: { type: "string" } },
|
|
101
|
+
anyOf: [
|
|
102
|
+
{ properties: { a: { type: "number" } } },
|
|
103
|
+
{ properties: { b: { type: "boolean" } } },
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
const result = (0, normalize_1.foldSiblingsIntoBranches)(schema);
|
|
107
|
+
expect(result.properties).toBeUndefined();
|
|
108
|
+
expect(result.anyOf).toHaveLength(2);
|
|
109
|
+
expect(result.anyOf[1].properties.shared).toBeDefined();
|
|
110
|
+
expect(result.anyOf[1].properties.b).toBeDefined();
|
|
111
|
+
});
|
|
112
|
+
it("preserves metadata and x-extension keys when folding", () => {
|
|
113
|
+
const schema = {
|
|
114
|
+
title: "Test",
|
|
115
|
+
description: "desc",
|
|
116
|
+
deprecated: true,
|
|
117
|
+
"x-vendor": true,
|
|
118
|
+
properties: { shared: { type: "string" } },
|
|
119
|
+
anyOf: [{ properties: { a: { type: "number" } } }],
|
|
120
|
+
};
|
|
121
|
+
const result = (0, normalize_1.foldSiblingsIntoBranches)(schema);
|
|
122
|
+
expect(result.title).toBe("Test");
|
|
123
|
+
expect(result.description).toBe("desc");
|
|
124
|
+
expect(result.deprecated).toBe(true);
|
|
125
|
+
expect(result["x-vendor"]).toBe(true);
|
|
126
|
+
expect(result.properties).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
it("strips non-metadata keys from result", () => {
|
|
129
|
+
const schema = {
|
|
130
|
+
type: "object",
|
|
131
|
+
required: ["shared"],
|
|
132
|
+
properties: { shared: { type: "string" } },
|
|
133
|
+
items: { type: "string" },
|
|
134
|
+
oneOf: [{ properties: { a: { type: "number" } } }],
|
|
135
|
+
};
|
|
136
|
+
const result = (0, normalize_1.foldSiblingsIntoBranches)(schema);
|
|
137
|
+
expect(result.type).toBeUndefined();
|
|
138
|
+
expect(result.required).toBeUndefined();
|
|
139
|
+
expect(result.properties).toBeUndefined();
|
|
140
|
+
expect(result.items).toBeUndefined();
|
|
141
|
+
expect(result.oneOf).toHaveLength(1);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe("getDiscriminator", () => {
|
|
145
|
+
it("returns direct discriminator", () => {
|
|
146
|
+
const schema = { discriminator: { propertyName: "type" } };
|
|
147
|
+
expect((0, normalize_1.getDiscriminator)(schema)).toEqual({
|
|
148
|
+
propertyName: "type",
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
it("finds discriminator in oneOf branches", () => {
|
|
152
|
+
const schema = {
|
|
153
|
+
oneOf: [{ discriminator: { propertyName: "kind" } }, { type: "string" }],
|
|
154
|
+
};
|
|
155
|
+
expect((0, normalize_1.getDiscriminator)(schema)).toEqual({
|
|
156
|
+
propertyName: "kind",
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
it("finds discriminator in anyOf branches", () => {
|
|
160
|
+
const schema = {
|
|
161
|
+
anyOf: [
|
|
162
|
+
{ type: "string" },
|
|
163
|
+
{ discriminator: { propertyName: "variant" } },
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
expect((0, normalize_1.getDiscriminator)(schema)).toEqual({
|
|
167
|
+
propertyName: "variant",
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
it("finds discriminator in allOf branches", () => {
|
|
171
|
+
const schema = {
|
|
172
|
+
allOf: [
|
|
173
|
+
{ properties: { id: { type: "string" } } },
|
|
174
|
+
{ discriminator: { propertyName: "type" } },
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
expect((0, normalize_1.getDiscriminator)(schema)).toEqual({
|
|
178
|
+
propertyName: "type",
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
it("returns undefined when no discriminator exists", () => {
|
|
182
|
+
const schema = {
|
|
183
|
+
type: "object",
|
|
184
|
+
properties: { name: { type: "string" } },
|
|
185
|
+
};
|
|
186
|
+
expect((0, normalize_1.getDiscriminator)(schema)).toBeUndefined();
|
|
187
|
+
});
|
|
188
|
+
it("returns undefined for null/primitive input", () => {
|
|
189
|
+
expect((0, normalize_1.getDiscriminator)(null)).toBeUndefined();
|
|
190
|
+
expect((0, normalize_1.getDiscriminator)("string")).toBeUndefined();
|
|
191
|
+
});
|
|
192
|
+
it("caches results for same object reference", () => {
|
|
193
|
+
const inner = { discriminator: { propertyName: "type" } };
|
|
194
|
+
const schema = { oneOf: [inner] };
|
|
195
|
+
const first = (0, normalize_1.getDiscriminator)(schema);
|
|
196
|
+
const second = (0, normalize_1.getDiscriminator)(schema);
|
|
197
|
+
expect(first).toBe(second);
|
|
198
|
+
});
|
|
199
|
+
it("handles cycles without infinite recursion", () => {
|
|
200
|
+
const a = { properties: {} };
|
|
201
|
+
const b = { properties: { a } };
|
|
202
|
+
a.properties.b = b;
|
|
203
|
+
expect(() => (0, normalize_1.getDiscriminator)(a)).not.toThrow();
|
|
204
|
+
expect((0, normalize_1.getDiscriminator)(a)).toBeUndefined();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe("findPropertyDeep", () => {
|
|
208
|
+
it("returns property at top level", () => {
|
|
209
|
+
const schema = { properties: { foo: { type: "string" } } };
|
|
210
|
+
expect((0, normalize_1.findPropertyDeep)(schema, "foo")).toEqual({
|
|
211
|
+
type: "string",
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
it("finds property in oneOf branches", () => {
|
|
215
|
+
const schema = {
|
|
216
|
+
oneOf: [
|
|
217
|
+
{ properties: { a: { type: "string" } } },
|
|
218
|
+
{ properties: { b: { type: "number" } } },
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
expect((0, normalize_1.findPropertyDeep)(schema, "a")).toEqual({
|
|
222
|
+
type: "string",
|
|
223
|
+
});
|
|
224
|
+
expect((0, normalize_1.findPropertyDeep)(schema, "b")).toEqual({
|
|
225
|
+
type: "number",
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
it("finds property in anyOf branches", () => {
|
|
229
|
+
const schema = {
|
|
230
|
+
anyOf: [{ properties: { x: { type: "integer" } } }],
|
|
231
|
+
};
|
|
232
|
+
expect((0, normalize_1.findPropertyDeep)(schema, "x")).toEqual({
|
|
233
|
+
type: "integer",
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
it("finds property in allOf branches", () => {
|
|
237
|
+
const schema = {
|
|
238
|
+
allOf: [{ properties: { y: { type: "boolean" } } }],
|
|
239
|
+
};
|
|
240
|
+
expect((0, normalize_1.findPropertyDeep)(schema, "y")).toEqual({
|
|
241
|
+
type: "boolean",
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
it("returns undefined when property not found", () => {
|
|
245
|
+
const schema = { properties: { foo: { type: "string" } } };
|
|
246
|
+
expect((0, normalize_1.findPropertyDeep)(schema, "bar")).toBeUndefined();
|
|
247
|
+
});
|
|
248
|
+
it("returns undefined for null/primitive input", () => {
|
|
249
|
+
expect((0, normalize_1.findPropertyDeep)(null, "foo")).toBeUndefined();
|
|
250
|
+
expect((0, normalize_1.findPropertyDeep)("string", "foo")).toBeUndefined();
|
|
251
|
+
});
|
|
252
|
+
it("handles cycles without infinite recursion", () => {
|
|
253
|
+
const a = { properties: {} };
|
|
254
|
+
a.oneOf = [a];
|
|
255
|
+
expect(() => (0, normalize_1.findPropertyDeep)(a, "foo")).not.toThrow();
|
|
256
|
+
expect((0, normalize_1.findPropertyDeep)(a, "foo")).toBeUndefined();
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
describe("isCircularMarker", () => {
|
|
260
|
+
it("returns true for circular marker strings", () => {
|
|
261
|
+
expect((0, normalize_1.isCircularMarker)("circular(Title)")).toBe(true);
|
|
262
|
+
expect((0, normalize_1.isCircularMarker)("circular()")).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
it("returns false for other values", () => {
|
|
265
|
+
expect((0, normalize_1.isCircularMarker)("normal string")).toBe(false);
|
|
266
|
+
expect((0, normalize_1.isCircularMarker)(null)).toBe(false);
|
|
267
|
+
expect((0, normalize_1.isCircularMarker)(undefined)).toBe(false);
|
|
268
|
+
expect((0, normalize_1.isCircularMarker)({})).toBe(false);
|
|
269
|
+
expect((0, normalize_1.isCircularMarker)(42)).toBe(false);
|
|
270
|
+
});
|
|
271
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docusaurus-theme-openapi-docs",
|
|
3
3
|
"description": "OpenAPI theme for Docusaurus.",
|
|
4
|
-
"version": "0.0.0-
|
|
4
|
+
"version": "0.0.0-1330",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"openapi",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@types/postman-collection": "^3.5.11",
|
|
39
39
|
"@types/react-modal": "^3.16.3",
|
|
40
40
|
"concurrently": "^10.0.3",
|
|
41
|
-
"docusaurus-plugin-openapi-docs": "0.0.0-
|
|
41
|
+
"docusaurus-plugin-openapi-docs": "0.0.0-1330",
|
|
42
42
|
"docusaurus-plugin-sass": "^0.2.6",
|
|
43
43
|
"eslint-plugin-prettier": "^5.5.1"
|
|
44
44
|
},
|
|
@@ -82,5 +82,5 @@
|
|
|
82
82
|
"engines": {
|
|
83
83
|
"node": ">=14"
|
|
84
84
|
},
|
|
85
|
-
"gitHead": "
|
|
85
|
+
"gitHead": "ebd7d312b3837149550af406eca7f5a3911817b0"
|
|
86
86
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
* ========================================================================== */
|
|
7
7
|
|
|
8
|
-
import React, { useCallback } from "react";
|
|
8
|
+
import React, { useCallback, useMemo } from "react";
|
|
9
9
|
|
|
10
10
|
import { translate } from "@docusaurus/Translate";
|
|
11
11
|
import { setSchemaSelection } from "@theme/ApiExplorer/SchemaSelection/slice";
|
|
@@ -14,6 +14,14 @@ import { ClosingArrayBracket, OpeningArrayBracket } from "@theme/ArrayBrackets";
|
|
|
14
14
|
import Details from "@theme/Details";
|
|
15
15
|
import DiscriminatorTabs from "@theme/DiscriminatorTabs";
|
|
16
16
|
import Markdown from "@theme/Markdown";
|
|
17
|
+
import {
|
|
18
|
+
findPropertyDeep,
|
|
19
|
+
foldSiblingsIntoBranches,
|
|
20
|
+
getDiscriminator,
|
|
21
|
+
isCircularMarker,
|
|
22
|
+
mergeAllOf,
|
|
23
|
+
normalizeSchema,
|
|
24
|
+
} from "@theme/Schema/normalize";
|
|
17
25
|
import {
|
|
18
26
|
SchemaDepthProvider,
|
|
19
27
|
useSchemaDepth,
|
|
@@ -22,192 +30,12 @@ import {
|
|
|
22
30
|
import SchemaItem from "@theme/SchemaItem";
|
|
23
31
|
import SchemaTabs from "@theme/SchemaTabs";
|
|
24
32
|
import TabItem from "@theme/TabItem";
|
|
25
|
-
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
26
|
-
import { merge } from "allof-merge";
|
|
27
33
|
import clsx from "clsx";
|
|
28
34
|
import isEmpty from "lodash/isEmpty";
|
|
29
35
|
|
|
30
36
|
import { getQualifierMessage, getSchemaName } from "../../markdown/schema";
|
|
31
37
|
import type { SchemaObject } from "../../types.d";
|
|
32
38
|
|
|
33
|
-
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
34
|
-
// const jsonSchemaMergeAllOf = require("json-schema-merge-allof");
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Strip `additionalProperties: false` from sibling allOf members so the
|
|
38
|
-
* strict-AND semantics of `allof-merge` don't collapse the result to an
|
|
39
|
-
* unsatisfiable empty schema.
|
|
40
|
-
*
|
|
41
|
-
* Per JSON Schema, two allOf members that each set `additionalProperties: false`
|
|
42
|
-
* with disjoint `properties` sets define an unsatisfiable schema (no value can
|
|
43
|
-
* satisfy both — each member rejects the other's properties). `allof-merge` is
|
|
44
|
-
* technically correct to drop all properties in that case, but it leaves the
|
|
45
|
-
* rendered schema blank.
|
|
46
|
-
*
|
|
47
|
-
* NSwag and Swashbuckle emit this pattern by default whenever a model uses
|
|
48
|
-
* inheritance/composition, so it's the dominant style for .NET-generated specs.
|
|
49
|
-
* Redoc, Swagger UI, and Stoplight all union the properties and ignore the
|
|
50
|
-
* conflicting flag — the approach this helper emulates by stripping the flag
|
|
51
|
-
* before delegating to `allof-merge`. The flag is render-irrelevant anyway:
|
|
52
|
-
* `additionalProperties: false` is treated identically to `undefined` by the
|
|
53
|
-
* AdditionalProperties component below (line ~641).
|
|
54
|
-
*
|
|
55
|
-
* Strips from every allOf member whenever the parent has ≥2 members. The
|
|
56
|
-
* collapse triggers symmetrically (both siblings strict) or asymmetrically
|
|
57
|
-
* (one strict member rejects another's properties as "additional"), so the
|
|
58
|
-
* presence of multiple members is the right gate. Single-member allOf is left
|
|
59
|
-
* alone — it can't conflict with anything.
|
|
60
|
-
*
|
|
61
|
-
* See https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1119
|
|
62
|
-
* Mirrored in plugin: docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts
|
|
63
|
-
*/
|
|
64
|
-
const stripConflictingAdditionalProps = (node: any): any => {
|
|
65
|
-
if (Array.isArray(node)) return node.map(stripConflictingAdditionalProps);
|
|
66
|
-
if (!node || typeof node !== "object") return node;
|
|
67
|
-
|
|
68
|
-
let working: any = node;
|
|
69
|
-
if (Array.isArray(node.allOf) && node.allOf.length > 1) {
|
|
70
|
-
const hasStrictMember = node.allOf.some(
|
|
71
|
-
(m: any) => m && m.additionalProperties === false
|
|
72
|
-
);
|
|
73
|
-
if (hasStrictMember) {
|
|
74
|
-
working = {
|
|
75
|
-
...node,
|
|
76
|
-
allOf: node.allOf.map((m: any) => {
|
|
77
|
-
if (m && m.additionalProperties === false) {
|
|
78
|
-
const { additionalProperties: _drop, ...rest } = m;
|
|
79
|
-
return rest;
|
|
80
|
-
}
|
|
81
|
-
return m;
|
|
82
|
-
}),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const result: any = {};
|
|
88
|
-
for (const [k, v] of Object.entries(working)) {
|
|
89
|
-
result[k] = stripConflictingAdditionalProps(v);
|
|
90
|
-
}
|
|
91
|
-
return result;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const mergeAllOf = (allOf: any) => {
|
|
95
|
-
const onMergeError = (msg: string) => {
|
|
96
|
-
console.warn(msg);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const mergedSchemas = merge(stripConflictingAdditionalProps(allOf), {
|
|
100
|
-
onMergeError,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
return mergedSchemas ?? {};
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Fold sibling fields into each `oneOf`/`anyOf` branch via allOf-merge, so each
|
|
108
|
-
* branch is self-contained. Mirrors Redoc's `SchemaModel.initOneOf` behavior.
|
|
109
|
-
* Without this, when an `allOf` override redefines a nested property with
|
|
110
|
-
* `oneOf`, the merged schema ends up with both `properties` and `oneOf` as
|
|
111
|
-
* siblings — and the renderer prints the shared properties twice.
|
|
112
|
-
*
|
|
113
|
-
* See https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1218
|
|
114
|
-
*/
|
|
115
|
-
const foldSiblingsIntoBranches = (schema: any): any => {
|
|
116
|
-
const branchKey = schema?.oneOf
|
|
117
|
-
? "oneOf"
|
|
118
|
-
: schema?.anyOf
|
|
119
|
-
? "anyOf"
|
|
120
|
-
: undefined;
|
|
121
|
-
if (!branchKey) return schema;
|
|
122
|
-
|
|
123
|
-
const branches = schema[branchKey];
|
|
124
|
-
if (!Array.isArray(branches) || branches.length === 0) return schema;
|
|
125
|
-
|
|
126
|
-
const siblings = { ...schema };
|
|
127
|
-
delete siblings[branchKey];
|
|
128
|
-
if (Object.keys(siblings).length === 0) return schema;
|
|
129
|
-
|
|
130
|
-
const folded = branches.map((branch: any) =>
|
|
131
|
-
mergeAllOf({ allOf: [siblings, branch] })
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
return { [branchKey]: folded };
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Recursively searches for a property in a schema, including nested
|
|
139
|
-
* oneOf, anyOf, and allOf structures. This is needed for discriminators
|
|
140
|
-
* where the property definition may be in a nested schema.
|
|
141
|
-
*/
|
|
142
|
-
const findProperty = (
|
|
143
|
-
schema: SchemaObject,
|
|
144
|
-
propertyName: string
|
|
145
|
-
): SchemaObject | undefined => {
|
|
146
|
-
// Check direct properties first
|
|
147
|
-
if (schema.properties?.[propertyName]) {
|
|
148
|
-
return schema.properties[propertyName];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Search in oneOf schemas
|
|
152
|
-
if (schema.oneOf) {
|
|
153
|
-
for (const subschema of schema.oneOf) {
|
|
154
|
-
const found = findProperty(subschema as SchemaObject, propertyName);
|
|
155
|
-
if (found) return found;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Search in anyOf schemas
|
|
160
|
-
if (schema.anyOf) {
|
|
161
|
-
for (const subschema of schema.anyOf) {
|
|
162
|
-
const found = findProperty(subschema as SchemaObject, propertyName);
|
|
163
|
-
if (found) return found;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Search in allOf schemas
|
|
168
|
-
if (schema.allOf) {
|
|
169
|
-
for (const subschema of schema.allOf) {
|
|
170
|
-
const found = findProperty(subschema as SchemaObject, propertyName);
|
|
171
|
-
if (found) return found;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return undefined;
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Recursively searches for a discriminator in a schema, including nested
|
|
180
|
-
* oneOf, anyOf, and allOf structures.
|
|
181
|
-
*/
|
|
182
|
-
const findDiscriminator = (schema: SchemaObject): any | undefined => {
|
|
183
|
-
if (schema.discriminator) {
|
|
184
|
-
return schema.discriminator;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (schema.oneOf) {
|
|
188
|
-
for (const subschema of schema.oneOf) {
|
|
189
|
-
const found = findDiscriminator(subschema as SchemaObject);
|
|
190
|
-
if (found) return found;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (schema.anyOf) {
|
|
195
|
-
for (const subschema of schema.anyOf) {
|
|
196
|
-
const found = findDiscriminator(subschema as SchemaObject);
|
|
197
|
-
if (found) return found;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (schema.allOf) {
|
|
202
|
-
for (const subschema of schema.allOf) {
|
|
203
|
-
const found = findDiscriminator(subschema as SchemaObject);
|
|
204
|
-
if (found) return found;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return undefined;
|
|
209
|
-
};
|
|
210
|
-
|
|
211
39
|
interface MarkdownProps {
|
|
212
40
|
text: string | undefined;
|
|
213
41
|
}
|
|
@@ -616,9 +444,11 @@ const DiscriminatorNode: React.FC<DiscriminatorNodeProps> = ({
|
|
|
616
444
|
let discriminatedSchemas: any = {};
|
|
617
445
|
let inferredMapping: any = {};
|
|
618
446
|
|
|
619
|
-
// Search for the discriminator property in the schema, including nested
|
|
447
|
+
// Search for the discriminator property in the schema, including nested
|
|
448
|
+
// structures. Cached recursive lookup replaces the O(subtree) findProperty
|
|
449
|
+
// walk that caused #1525's O(N^2) render cost.
|
|
620
450
|
const discriminatorProperty =
|
|
621
|
-
|
|
451
|
+
findPropertyDeep(schema, discriminator.propertyName) ?? {};
|
|
622
452
|
|
|
623
453
|
if (schema.allOf) {
|
|
624
454
|
const mergedSchemas = mergeAllOf(schema) as SchemaObject;
|
|
@@ -699,6 +529,20 @@ const AdditionalProperties: React.FC<SchemaProps> = ({
|
|
|
699
529
|
|
|
700
530
|
if (!additionalProperties) return null;
|
|
701
531
|
|
|
532
|
+
if (isCircularMarker(additionalProperties)) {
|
|
533
|
+
return (
|
|
534
|
+
<SchemaItem
|
|
535
|
+
name="property name*"
|
|
536
|
+
required={false}
|
|
537
|
+
schemaName={additionalProperties}
|
|
538
|
+
schema={additionalProperties}
|
|
539
|
+
collapsible={false}
|
|
540
|
+
discriminator={false}
|
|
541
|
+
children={null}
|
|
542
|
+
/>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
702
546
|
// Handle free-form objects
|
|
703
547
|
if (additionalProperties === true || isEmpty(additionalProperties)) {
|
|
704
548
|
return (
|
|
@@ -812,6 +656,23 @@ const Items: React.FC<{
|
|
|
812
656
|
schemaType: "request" | "response";
|
|
813
657
|
schemaPath?: string;
|
|
814
658
|
}> = ({ schema, schemaType, schemaPath }) => {
|
|
659
|
+
if (isCircularMarker(schema.items)) {
|
|
660
|
+
return (
|
|
661
|
+
<div style={{ marginLeft: ".5rem" }}>
|
|
662
|
+
<OpeningArrayBracket />
|
|
663
|
+
<SchemaItem
|
|
664
|
+
collapsible={false}
|
|
665
|
+
name=""
|
|
666
|
+
schemaName={schema.items}
|
|
667
|
+
schema={schema.items}
|
|
668
|
+
discriminator={false}
|
|
669
|
+
children={null}
|
|
670
|
+
/>
|
|
671
|
+
<ClosingArrayBracket />
|
|
672
|
+
</div>
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
815
676
|
// Process schema.items to handle allOf merging
|
|
816
677
|
let itemsSchema = schema.items;
|
|
817
678
|
if (schema.items?.allOf) {
|
|
@@ -932,6 +793,20 @@ const SchemaEdge: React.FC<SchemaEdgeProps> = ({
|
|
|
932
793
|
return null;
|
|
933
794
|
}
|
|
934
795
|
|
|
796
|
+
if (isCircularMarker(schema)) {
|
|
797
|
+
return (
|
|
798
|
+
<SchemaItem
|
|
799
|
+
collapsible={false}
|
|
800
|
+
name={name}
|
|
801
|
+
required={Array.isArray(required) ? required.includes(name) : required}
|
|
802
|
+
schemaName={schema}
|
|
803
|
+
schema={schema}
|
|
804
|
+
discriminator={false}
|
|
805
|
+
children={null}
|
|
806
|
+
/>
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
|
|
935
810
|
const schemaName = getSchemaName(schema);
|
|
936
811
|
|
|
937
812
|
if (discriminator && discriminator.propertyName === name) {
|
|
@@ -1018,6 +893,20 @@ const SchemaEdge: React.FC<SchemaEdgeProps> = ({
|
|
|
1018
893
|
);
|
|
1019
894
|
}
|
|
1020
895
|
|
|
896
|
+
if (isCircularMarker(schema.items)) {
|
|
897
|
+
return (
|
|
898
|
+
<SchemaNodeDetails
|
|
899
|
+
name={name}
|
|
900
|
+
schemaName={schemaName}
|
|
901
|
+
required={required}
|
|
902
|
+
nullable={schema.nullable}
|
|
903
|
+
schema={schema}
|
|
904
|
+
schemaType={schemaType}
|
|
905
|
+
schemaPath={schemaPath}
|
|
906
|
+
/>
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
|
|
1021
910
|
if (schema.allOf) {
|
|
1022
911
|
// handle circular properties
|
|
1023
912
|
if (
|
|
@@ -1176,10 +1065,12 @@ function renderChildren(
|
|
|
1176
1065
|
}
|
|
1177
1066
|
|
|
1178
1067
|
const SchemaNode: React.FC<SchemaProps> = ({
|
|
1179
|
-
schema,
|
|
1068
|
+
schema: rawSchema,
|
|
1180
1069
|
schemaType,
|
|
1181
1070
|
schemaPath,
|
|
1182
1071
|
}) => {
|
|
1072
|
+
const schema = useMemo(() => normalizeSchema(rawSchema), [rawSchema]);
|
|
1073
|
+
|
|
1183
1074
|
if (
|
|
1184
1075
|
(schemaType === "request" && schema.readOnly) ||
|
|
1185
1076
|
(schemaType === "response" && schema.writeOnly)
|
|
@@ -1188,10 +1079,11 @@ const SchemaNode: React.FC<SchemaProps> = ({
|
|
|
1188
1079
|
}
|
|
1189
1080
|
|
|
1190
1081
|
// Resolve discriminator recursively so nested oneOf/anyOf/allOf compositions
|
|
1191
|
-
// can still render discriminator tabs.
|
|
1082
|
+
// can still render discriminator tabs. Cached via getDiscriminator so per-
|
|
1083
|
+
// render cost is O(1) amortized (see #1525).
|
|
1192
1084
|
let workingSchema = schema;
|
|
1193
1085
|
const resolvedDiscriminator =
|
|
1194
|
-
schema.discriminator ??
|
|
1086
|
+
schema.discriminator ?? getDiscriminator(schema);
|
|
1195
1087
|
if (schema.allOf && !schema.discriminator && resolvedDiscriminator) {
|
|
1196
1088
|
workingSchema = mergeAllOf(schema) as SchemaObject;
|
|
1197
1089
|
}
|