docusaurus-plugin-openapi-docs 4.7.0 → 5.0.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 (34) hide show
  1. package/README.md +11 -8
  2. package/lib/index.js +4 -4
  3. package/lib/markdown/createLicense.js +5 -1
  4. package/lib/markdown/createSchema.js +3 -34
  5. package/lib/markdown/schema.d.ts +0 -1
  6. package/lib/markdown/schema.js +0 -105
  7. package/lib/markdown/utils.js +3 -1
  8. package/lib/openapi/createSchemaExample.test.js +26 -0
  9. package/lib/openapi/openapi.js +17 -13
  10. package/lib/openapi/openapi.test.js +152 -0
  11. package/lib/openapi/types.d.ts +1 -0
  12. package/lib/openapi/utils/loadAndResolveSpec.js +5 -4
  13. package/lib/openapi/utils/utils/openapi.d.ts +0 -2
  14. package/lib/openapi/utils/utils/openapi.js +0 -82
  15. package/lib/sidebars/index.js +24 -8
  16. package/lib/sidebars/index.test.js +68 -0
  17. package/package.json +13 -13
  18. package/src/index.ts +4 -4
  19. package/src/markdown/__snapshots__/createSchema.test.ts.snap +0 -100
  20. package/src/markdown/createLicense.ts +7 -1
  21. package/src/markdown/createSchema.ts +4 -43
  22. package/src/markdown/schema.ts +0 -126
  23. package/src/markdown/utils.ts +3 -1
  24. package/src/openapi/createSchemaExample.test.ts +32 -0
  25. package/src/openapi/openapi.test.ts +176 -0
  26. package/src/openapi/openapi.ts +31 -15
  27. package/src/openapi/types.ts +1 -0
  28. package/src/openapi/utils/loadAndResolveSpec.ts +8 -6
  29. package/src/openapi/utils/utils/openapi.ts +0 -110
  30. package/src/sidebars/index.test.ts +94 -0
  31. package/src/sidebars/index.ts +28 -9
  32. package/lib/markdown/schema.test.js +0 -181
  33. package/src/markdown/schema.test.ts +0 -208
  34. /package/lib/{markdown/schema.test.d.ts → sidebars/index.test.d.ts} +0 -0
@@ -10,7 +10,7 @@ import { LicenseObject } from "../openapi/types";
10
10
 
11
11
  export function createLicense(license: LicenseObject) {
12
12
  if (!license || !Object.keys(license).length) return "";
13
- const { name, url } = license;
13
+ const { name, url, identifier } = license;
14
14
 
15
15
  return create("div", {
16
16
  style: {
@@ -29,6 +29,12 @@ export function createLicense(license: LicenseObject) {
29
29
  children: name ?? url,
30
30
  })
31
31
  ),
32
+ guard(identifier, () =>
33
+ create("a", {
34
+ href: `https://spdx.org/licenses/${identifier}.html`,
35
+ children: name ?? identifier,
36
+ })
37
+ ),
32
38
  ],
33
39
  });
34
40
  }
@@ -17,7 +17,7 @@ import {
17
17
  import { createDescription } from "./createDescription";
18
18
  import { createDetails } from "./createDetails";
19
19
  import { createDetailsSummary } from "./createDetailsSummary";
20
- import { getQualifierMessage, getSchemaName } from "./schema";
20
+ import { getSchemaName } from "./schema";
21
21
  import { create, guard } from "./utils";
22
22
  import { SchemaObject } from "../openapi/types";
23
23
 
@@ -32,7 +32,7 @@ export function mergeAllOf(allOf: SchemaObject) {
32
32
  };
33
33
 
34
34
  const mergedSchemas = merge(allOf, { onMergeError }) as SchemaObject;
35
- return mergedSchemas;
35
+ return mergedSchemas ?? ({} as SchemaObject);
36
36
  }
37
37
 
38
38
  /**
@@ -140,7 +140,6 @@ function createProperties(schema: SchemaObject) {
140
140
  name: "",
141
141
  required: false,
142
142
  schemaName: "object",
143
- qualifierMessage: undefined,
144
143
  schema: {},
145
144
  });
146
145
  }
@@ -169,7 +168,6 @@ function createAdditionalProperties(schema: SchemaObject) {
169
168
  name: "property name*",
170
169
  required: false,
171
170
  schemaName: "any",
172
- qualifierMessage: getQualifierMessage(schema),
173
171
  schema: schema,
174
172
  collapsible: false,
175
173
  discriminator: false,
@@ -209,7 +207,6 @@ function createAdditionalProperties(schema: SchemaObject) {
209
207
  name: "property name*",
210
208
  required: false,
211
209
  schemaName: schemaName,
212
- qualifierMessage: getQualifierMessage(schema),
213
210
  schema: additionalProperties,
214
211
  collapsible: false,
215
212
  discriminator: false,
@@ -399,12 +396,6 @@ function createDetailsNode(
399
396
  children: createDescription(description),
400
397
  })
401
398
  ),
402
- guard(getQualifierMessage(schema), (message) =>
403
- create("div", {
404
- style: { marginTop: ".5rem", marginBottom: ".5rem" },
405
- children: createDescription(message),
406
- })
407
- ),
408
399
  createNodes(schema, SCHEMA_TYPE),
409
400
  ],
410
401
  }),
@@ -545,14 +536,6 @@ function createPropertyDiscriminator(
545
536
  children: createDescription(description),
546
537
  })
547
538
  ),
548
- guard(getQualifierMessage(discriminator), (message) =>
549
- create("div", {
550
- style: {
551
- paddingLeft: "1rem",
552
- },
553
- children: createDescription(message),
554
- })
555
- ),
556
539
  create("DiscriminatorTabs", {
557
540
  className: "openapi-tabs__discriminator",
558
541
  children: Object.keys(discriminator?.mapping!).map((key, index) => {
@@ -727,7 +710,6 @@ function createEdges({
727
710
  name,
728
711
  required: Array.isArray(required) ? required.includes(name) : required,
729
712
  schemaName: mergedSchemaName,
730
- qualifierMessage: getQualifierMessage(mergedSchemas),
731
713
  schema: mergedSchemas,
732
714
  });
733
715
  }
@@ -738,7 +720,6 @@ function createEdges({
738
720
  name,
739
721
  required: Array.isArray(required) ? required.includes(name) : required,
740
722
  schemaName: schemaName,
741
- qualifierMessage: getQualifierMessage(schema),
742
723
  schema: schema,
743
724
  });
744
725
  }
@@ -823,17 +804,7 @@ export function createNodes(
823
804
  marginTop: ".5rem",
824
805
  marginBottom: ".5rem",
825
806
  },
826
- children: [
827
- createDescription(schema.type),
828
- guard(getQualifierMessage(schema), (message) =>
829
- create("div", {
830
- style: {
831
- paddingTop: "1rem",
832
- },
833
- children: createDescription(message),
834
- })
835
- ),
836
- ],
807
+ children: [createDescription(schema.type)],
837
808
  });
838
809
  }
839
810
 
@@ -844,17 +815,7 @@ export function createNodes(
844
815
  marginTop: ".5rem",
845
816
  marginBottom: ".5rem",
846
817
  },
847
- children: [
848
- createDescription(schema),
849
- guard(getQualifierMessage(schema), (message) =>
850
- create("div", {
851
- style: {
852
- paddingTop: "1rem",
853
- },
854
- children: createDescription(message),
855
- })
856
- ),
857
- ],
818
+ children: [createDescription(schema)],
858
819
  });
859
820
  }
860
821
 
@@ -66,129 +66,3 @@ export function getSchemaName(
66
66
 
67
67
  return prettyName(schema, circular) ?? "";
68
68
  }
69
-
70
- export function getQualifierMessage(schema?: SchemaObject): string | undefined {
71
- // TODO:
72
- // - uniqueItems
73
- // - maxProperties
74
- // - minProperties
75
- // - multipleOf
76
- if (!schema) {
77
- return undefined;
78
- }
79
-
80
- if (
81
- schema.items &&
82
- schema.minItems === undefined &&
83
- schema.maxItems === undefined
84
- ) {
85
- return getQualifierMessage(schema.items);
86
- }
87
-
88
- let message = "**Possible values:** ";
89
-
90
- let qualifierGroups = [];
91
-
92
- if (schema.items && schema.items.enum) {
93
- if (schema.items.enum) {
94
- qualifierGroups.push(
95
- `[${schema.items.enum.map((e) => `\`${e}\``).join(", ")}]`
96
- );
97
- }
98
- }
99
-
100
- if (schema.minLength || schema.maxLength) {
101
- let lengthQualifier = "";
102
- let minLength;
103
- let maxLength;
104
- if (schema.minLength && schema.minLength > 1) {
105
- minLength = `\`>= ${schema.minLength} characters\``;
106
- }
107
- if (schema.minLength && schema.minLength === 1) {
108
- minLength = `\`non-empty\``;
109
- }
110
- if (schema.maxLength) {
111
- maxLength = `\`<= ${schema.maxLength} characters\``;
112
- }
113
-
114
- if (minLength && !maxLength) {
115
- lengthQualifier += minLength;
116
- }
117
- if (maxLength && !minLength) {
118
- lengthQualifier += maxLength;
119
- }
120
- if (minLength && maxLength) {
121
- lengthQualifier += `${minLength} and ${maxLength}`;
122
- }
123
-
124
- qualifierGroups.push(lengthQualifier);
125
- }
126
-
127
- if (
128
- schema.minimum != null ||
129
- schema.maximum != null ||
130
- typeof schema.exclusiveMinimum === "number" ||
131
- typeof schema.exclusiveMaximum === "number"
132
- ) {
133
- let minmaxQualifier = "";
134
- let minimum;
135
- let maximum;
136
- if (typeof schema.exclusiveMinimum === "number") {
137
- minimum = `\`> ${schema.exclusiveMinimum}\``;
138
- } else if (schema.minimum != null && !schema.exclusiveMinimum) {
139
- minimum = `\`>= ${schema.minimum}\``;
140
- } else if (schema.minimum != null && schema.exclusiveMinimum === true) {
141
- minimum = `\`> ${schema.minimum}\``;
142
- }
143
- if (typeof schema.exclusiveMaximum === "number") {
144
- maximum = `\`< ${schema.exclusiveMaximum}\``;
145
- } else if (schema.maximum != null && !schema.exclusiveMaximum) {
146
- maximum = `\`<= ${schema.maximum}\``;
147
- } else if (schema.maximum != null && schema.exclusiveMaximum === true) {
148
- maximum = `\`< ${schema.maximum}\``;
149
- }
150
-
151
- if (minimum && !maximum) {
152
- minmaxQualifier += minimum;
153
- }
154
- if (maximum && !minimum) {
155
- minmaxQualifier += maximum;
156
- }
157
- if (minimum && maximum) {
158
- minmaxQualifier += `${minimum} and ${maximum}`;
159
- }
160
-
161
- qualifierGroups.push(minmaxQualifier);
162
- }
163
-
164
- if (schema.pattern) {
165
- qualifierGroups.push(
166
- `Value must match regular expression \`${schema.pattern}\``
167
- );
168
- }
169
-
170
- // Check if discriminator mapping
171
- const discriminator = schema as any;
172
- if (discriminator.mapping) {
173
- const values = Object.keys(discriminator.mapping);
174
- qualifierGroups.push(`[${values.map((e) => `\`${e}\``).join(", ")}]`);
175
- }
176
-
177
- if (schema.enum) {
178
- qualifierGroups.push(`[${schema.enum.map((e) => `\`${e}\``).join(", ")}]`);
179
- }
180
-
181
- if (schema.minItems) {
182
- qualifierGroups.push(`\`>= ${schema.minItems}\``);
183
- }
184
-
185
- if (schema.maxItems) {
186
- qualifierGroups.push(`\`<= ${schema.maxItems}\``);
187
- }
188
-
189
- if (qualifierGroups.length === 0) {
190
- return undefined;
191
- }
192
-
193
- return message + qualifierGroups.join(", ");
194
- }
@@ -129,7 +129,9 @@ export function create(
129
129
  } else {
130
130
  // Inline props as usual
131
131
  for (const [key, value] of Object.entries(rest)) {
132
- propString += `\n ${key}={${JSON.stringify(value)}}`;
132
+ if (value !== undefined) {
133
+ propString += `\n ${key}={${JSON.stringify(value)}}`;
134
+ }
133
135
  }
134
136
  }
135
137
 
@@ -54,4 +54,36 @@ describe("sampleFromSchema", () => {
54
54
  expect(result).toBe("dog");
55
55
  });
56
56
  });
57
+
58
+ describe("allOf with incompatible types", () => {
59
+ it("should return undefined when allOf contains incompatible types", () => {
60
+ const schema: SchemaObject = {
61
+ allOf: [{ type: "string" }, { type: "integer" }],
62
+ };
63
+ const context = { type: "request" as const };
64
+
65
+ const result = sampleFromSchema(schema, context);
66
+
67
+ expect(result).toBeUndefined();
68
+ });
69
+
70
+ it("should handle incompatible allOf types in a property", () => {
71
+ const schema: SchemaObject = {
72
+ type: "object",
73
+ properties: {
74
+ numero: {
75
+ allOf: [{ type: "string" }, { type: "integer" }],
76
+ },
77
+ name: {
78
+ type: "string",
79
+ },
80
+ },
81
+ };
82
+ const context = { type: "request" as const };
83
+
84
+ const result = sampleFromSchema(schema, context);
85
+
86
+ expect(result.name).toBe("string");
87
+ });
88
+ });
57
89
  });
@@ -95,4 +95,180 @@ describe("openapi", () => {
95
95
  expect(schemaItems[0].id).toBe("without-tags");
96
96
  });
97
97
  });
98
+
99
+ describe("path template and custom verb handling", () => {
100
+ it("binds postman requests for OpenAPI templates and path verbs", async () => {
101
+ const openapiData = {
102
+ openapi: "3.0.0",
103
+ info: {
104
+ title: "Path Template API",
105
+ version: "1.0.0",
106
+ },
107
+ paths: {
108
+ "/api/resource:customVerb": {
109
+ post: {
110
+ summary: "Custom verb endpoint",
111
+ operationId: "customVerbOperation",
112
+ responses: {
113
+ "200": {
114
+ description: "OK",
115
+ },
116
+ },
117
+ },
118
+ },
119
+ "/api/users/{id}": {
120
+ get: {
121
+ summary: "Get user by ID",
122
+ operationId: "getUserById",
123
+ parameters: [
124
+ {
125
+ name: "id",
126
+ in: "path",
127
+ required: true,
128
+ schema: {
129
+ type: "string",
130
+ },
131
+ },
132
+ ],
133
+ responses: {
134
+ "200": {
135
+ description: "OK",
136
+ },
137
+ },
138
+ },
139
+ },
140
+ "/api/users/{userId}/posts/{postId}": {
141
+ get: {
142
+ summary: "Get user post",
143
+ operationId: "getUserPost",
144
+ parameters: [
145
+ {
146
+ name: "userId",
147
+ in: "path",
148
+ required: true,
149
+ schema: {
150
+ type: "string",
151
+ },
152
+ },
153
+ {
154
+ name: "postId",
155
+ in: "path",
156
+ required: true,
157
+ schema: {
158
+ type: "string",
159
+ },
160
+ },
161
+ ],
162
+ responses: {
163
+ "200": {
164
+ description: "OK",
165
+ },
166
+ },
167
+ },
168
+ },
169
+ "/files/{name}.{ext}": {
170
+ get: {
171
+ summary: "Get file by name and extension",
172
+ operationId: "getFileByNameAndExt",
173
+ parameters: [
174
+ {
175
+ name: "name",
176
+ in: "path",
177
+ required: true,
178
+ schema: {
179
+ type: "string",
180
+ },
181
+ },
182
+ {
183
+ name: "ext",
184
+ in: "path",
185
+ required: true,
186
+ schema: {
187
+ type: "string",
188
+ },
189
+ },
190
+ ],
191
+ responses: {
192
+ "200": {
193
+ description: "OK",
194
+ },
195
+ },
196
+ },
197
+ },
198
+ "/jobs/{id}:cancel": {
199
+ post: {
200
+ summary: "Cancel job",
201
+ operationId: "cancelJob",
202
+ parameters: [
203
+ {
204
+ name: "id",
205
+ in: "path",
206
+ required: true,
207
+ schema: {
208
+ type: "string",
209
+ },
210
+ },
211
+ ],
212
+ responses: {
213
+ "200": {
214
+ description: "OK",
215
+ },
216
+ },
217
+ },
218
+ },
219
+ },
220
+ };
221
+
222
+ const options: APIOptions = {
223
+ specPath: "dummy",
224
+ outputDir: "build",
225
+ };
226
+ const sidebarOptions = {} as SidebarOptions;
227
+ const [items] = await processOpenapiFile(
228
+ openapiData as any,
229
+ options,
230
+ sidebarOptions
231
+ );
232
+
233
+ const apiItems = items.filter((item) => item.type === "api");
234
+ expect(apiItems).toHaveLength(5);
235
+
236
+ const customVerbItem = apiItems.find(
237
+ (item) => item.type === "api" && item.id === "custom-verb-operation"
238
+ ) as any;
239
+ expect(customVerbItem.api.path).toBe("/api/resource:customVerb");
240
+ expect(customVerbItem.api.method).toBe("post");
241
+ expect(customVerbItem.api.postman).toBeDefined();
242
+
243
+ const standardItem = apiItems.find(
244
+ (item) => item.type === "api" && item.id === "get-user-by-id"
245
+ ) as any;
246
+ expect(standardItem.api.path).toBe("/api/users/{id}");
247
+ expect(standardItem.api.method).toBe("get");
248
+ expect(standardItem.api.postman).toBeDefined();
249
+
250
+ const multiParamItem = apiItems.find(
251
+ (item) => item.type === "api" && item.id === "get-user-post"
252
+ ) as any;
253
+ expect(multiParamItem.api.path).toBe(
254
+ "/api/users/{userId}/posts/{postId}"
255
+ );
256
+ expect(multiParamItem.api.method).toBe("get");
257
+ expect(multiParamItem.api.postman).toBeDefined();
258
+
259
+ const sameSegmentItem = apiItems.find(
260
+ (item) => item.type === "api" && item.id === "get-file-by-name-and-ext"
261
+ ) as any;
262
+ expect(sameSegmentItem.api.path).toBe("/files/{name}.{ext}");
263
+ expect(sameSegmentItem.api.method).toBe("get");
264
+ expect(sameSegmentItem.api.postman).toBeDefined();
265
+
266
+ const templatedVerbItem = apiItems.find(
267
+ (item) => item.type === "api" && item.id === "cancel-job"
268
+ ) as any;
269
+ expect(templatedVerbItem.api.path).toBe("/jobs/{id}:cancel");
270
+ expect(templatedVerbItem.api.method).toBe("post");
271
+ expect(templatedVerbItem.api.postman).toBeDefined();
272
+ });
273
+ });
98
274
  });
@@ -561,28 +561,44 @@ function createItems(
561
561
  /**
562
562
  * Attach Postman Request objects to the corresponding ApiItems.
563
563
  */
564
+ function pathTemplateToRegex(pathTemplate: string): RegExp {
565
+ const pathWithTemplateTokens = pathTemplate.replace(
566
+ /\{[^}]+\}/g,
567
+ "__OPENAPI_PATH_PARAM__"
568
+ );
569
+ const escapedPathTemplate = pathWithTemplateTokens.replace(
570
+ /[.*+?^${}()|[\]\\]/g,
571
+ "\\$&"
572
+ );
573
+ const templatePattern = escapedPathTemplate.replace(
574
+ /__OPENAPI_PATH_PARAM__/g,
575
+ "[^/]+"
576
+ );
577
+ return new RegExp(`^${templatePattern}$`);
578
+ }
579
+
564
580
  function bindCollectionToApiItems(
565
581
  items: ApiMetadata[],
566
582
  postmanCollection: sdk.Collection
567
583
  ) {
584
+ const apiMatchers = items
585
+ .filter((item): item is ApiPageMetadata => item.type === "api")
586
+ .map((item) => ({
587
+ apiItem: item,
588
+ method: item.api.method.toLowerCase(),
589
+ pathMatcher: pathTemplateToRegex(item.api.path),
590
+ }));
591
+
568
592
  postmanCollection.forEachItem((item: any) => {
569
593
  const method = item.request.method.toLowerCase();
570
- const path = item.request.url
571
- .getPath({ unresolved: true }) // unresolved returns "/:variableName" instead of "/<type>"
572
- .replace(/(?<![a-z0-9-_]+):([a-z0-9-_]+)/gi, "{$1}"); // replace "/:variableName" with "/{variableName}"
573
- const apiItem = items.find((item) => {
574
- if (
575
- item.type === "info" ||
576
- item.type === "tag" ||
577
- item.type === "schema"
578
- ) {
579
- return false;
580
- }
581
- return item.api.path === path && item.api.method === method;
582
- });
594
+ const postmanPath = item.request.url.getPath({ unresolved: true });
595
+ const match = apiMatchers.find(
596
+ ({ method: itemMethod, pathMatcher }) =>
597
+ itemMethod === method && pathMatcher.test(postmanPath)
598
+ );
583
599
 
584
- if (apiItem?.type === "api") {
585
- apiItem.api.postman = item.request;
600
+ if (match) {
601
+ match.apiItem.api.postman = item.request;
586
602
  }
587
603
  });
588
604
  }
@@ -65,6 +65,7 @@ export interface ContactObject {
65
65
  export interface LicenseObject {
66
66
  name: string;
67
67
  url?: string;
68
+ identifier?: string;
68
69
  }
69
70
 
70
71
  export interface ServerObject {
@@ -6,9 +6,8 @@
6
6
  * ========================================================================== */
7
7
 
8
8
  import $RefParser from "@apidevtools/json-schema-ref-parser";
9
- import { bundle, Config } from "@redocly/openapi-core";
9
+ import { bundle, createConfig } from "@redocly/openapi-core";
10
10
  import type { Source, Document } from "@redocly/openapi-core";
11
- import { ResolvedConfig } from "@redocly/openapi-core/lib/config";
12
11
  import chalk from "chalk";
13
12
  // @ts-ignore
14
13
  import { convertObj } from "swagger2openapi";
@@ -116,7 +115,7 @@ async function resolveJsonRefs(specUrlOrObject: object | string) {
116
115
  }
117
116
 
118
117
  export async function loadAndResolveSpec(specUrlOrObject: object | string) {
119
- const config = new Config({} as ResolvedConfig);
118
+ const config = await createConfig({});
120
119
  const bundleOpts = {
121
120
  config,
122
121
  base: process.cwd(),
@@ -137,10 +136,13 @@ export async function loadAndResolveSpec(specUrlOrObject: object | string) {
137
136
  const {
138
137
  bundle: { parsed },
139
138
  } = await bundle(bundleOpts);
139
+ const parsedSpec = parsed as any;
140
140
 
141
141
  //Pre-processing before resolving JSON refs
142
- if (parsed.components) {
143
- for (let [component, type] of Object.entries(parsed.components) as any) {
142
+ if (parsedSpec.components) {
143
+ for (let [component, type] of Object.entries(
144
+ parsedSpec.components
145
+ ) as any) {
144
146
  if (component === "schemas") {
145
147
  for (let [schemaKey, schemaValue] of Object.entries(type) as any) {
146
148
  const title: string | undefined = schemaValue["title"];
@@ -152,7 +154,7 @@ export async function loadAndResolveSpec(specUrlOrObject: object | string) {
152
154
  }
153
155
  }
154
156
 
155
- const resolved = await resolveJsonRefs(parsed);
157
+ const resolved = await resolveJsonRefs(parsedSpec);
156
158
 
157
159
  // Force serialization and replace circular $ref pointers
158
160
  // @ts-ignore