docusaurus-plugin-openapi-docs 0.0.0-1105 → 0.0.0-1107

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.
@@ -481,22 +481,26 @@ function createItems(openapiData, options, sidebarOptions) {
481
481
  /**
482
482
  * Attach Postman Request objects to the corresponding ApiItems.
483
483
  */
484
+ function pathTemplateToRegex(pathTemplate) {
485
+ const pathWithTemplateTokens = pathTemplate.replace(/\{[^}]+\}/g, "__OPENAPI_PATH_PARAM__");
486
+ const escapedPathTemplate = pathWithTemplateTokens.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
487
+ const templatePattern = escapedPathTemplate.replace(/__OPENAPI_PATH_PARAM__/g, "[^/]+");
488
+ return new RegExp(`^${templatePattern}$`);
489
+ }
484
490
  function bindCollectionToApiItems(items, postmanCollection) {
491
+ const apiMatchers = items
492
+ .filter((item) => item.type === "api")
493
+ .map((item) => ({
494
+ apiItem: item,
495
+ method: item.api.method.toLowerCase(),
496
+ pathMatcher: pathTemplateToRegex(item.api.path),
497
+ }));
485
498
  postmanCollection.forEachItem((item) => {
486
499
  const method = item.request.method.toLowerCase();
487
- const path = item.request.url
488
- .getPath({ unresolved: true }) // unresolved returns "/:variableName" instead of "/<type>"
489
- .replace(/(?<![a-z0-9-_]+):([a-z0-9-_]+)/gi, "{$1}"); // replace "/:variableName" with "/{variableName}"
490
- const apiItem = items.find((item) => {
491
- if (item.type === "info" ||
492
- item.type === "tag" ||
493
- item.type === "schema") {
494
- return false;
495
- }
496
- return item.api.path === path && item.api.method === method;
497
- });
498
- if ((apiItem === null || apiItem === void 0 ? void 0 : apiItem.type) === "api") {
499
- apiItem.api.postman = item.request;
500
+ const postmanPath = item.request.url.getPath({ unresolved: true });
501
+ const match = apiMatchers.find(({ method: itemMethod, pathMatcher }) => itemMethod === method && pathMatcher.test(postmanPath));
502
+ if (match) {
503
+ match.apiItem.api.postman = item.request;
500
504
  }
501
505
  });
502
506
  }
@@ -78,4 +78,156 @@ describe("openapi", () => {
78
78
  expect(schemaItems[0].id).toBe("without-tags");
79
79
  });
80
80
  });
81
+ describe("path template and custom verb handling", () => {
82
+ it("binds postman requests for OpenAPI templates and path verbs", async () => {
83
+ const openapiData = {
84
+ openapi: "3.0.0",
85
+ info: {
86
+ title: "Path Template API",
87
+ version: "1.0.0",
88
+ },
89
+ paths: {
90
+ "/api/resource:customVerb": {
91
+ post: {
92
+ summary: "Custom verb endpoint",
93
+ operationId: "customVerbOperation",
94
+ responses: {
95
+ "200": {
96
+ description: "OK",
97
+ },
98
+ },
99
+ },
100
+ },
101
+ "/api/users/{id}": {
102
+ get: {
103
+ summary: "Get user by ID",
104
+ operationId: "getUserById",
105
+ parameters: [
106
+ {
107
+ name: "id",
108
+ in: "path",
109
+ required: true,
110
+ schema: {
111
+ type: "string",
112
+ },
113
+ },
114
+ ],
115
+ responses: {
116
+ "200": {
117
+ description: "OK",
118
+ },
119
+ },
120
+ },
121
+ },
122
+ "/api/users/{userId}/posts/{postId}": {
123
+ get: {
124
+ summary: "Get user post",
125
+ operationId: "getUserPost",
126
+ parameters: [
127
+ {
128
+ name: "userId",
129
+ in: "path",
130
+ required: true,
131
+ schema: {
132
+ type: "string",
133
+ },
134
+ },
135
+ {
136
+ name: "postId",
137
+ in: "path",
138
+ required: true,
139
+ schema: {
140
+ type: "string",
141
+ },
142
+ },
143
+ ],
144
+ responses: {
145
+ "200": {
146
+ description: "OK",
147
+ },
148
+ },
149
+ },
150
+ },
151
+ "/files/{name}.{ext}": {
152
+ get: {
153
+ summary: "Get file by name and extension",
154
+ operationId: "getFileByNameAndExt",
155
+ parameters: [
156
+ {
157
+ name: "name",
158
+ in: "path",
159
+ required: true,
160
+ schema: {
161
+ type: "string",
162
+ },
163
+ },
164
+ {
165
+ name: "ext",
166
+ in: "path",
167
+ required: true,
168
+ schema: {
169
+ type: "string",
170
+ },
171
+ },
172
+ ],
173
+ responses: {
174
+ "200": {
175
+ description: "OK",
176
+ },
177
+ },
178
+ },
179
+ },
180
+ "/jobs/{id}:cancel": {
181
+ post: {
182
+ summary: "Cancel job",
183
+ operationId: "cancelJob",
184
+ parameters: [
185
+ {
186
+ name: "id",
187
+ in: "path",
188
+ required: true,
189
+ schema: {
190
+ type: "string",
191
+ },
192
+ },
193
+ ],
194
+ responses: {
195
+ "200": {
196
+ description: "OK",
197
+ },
198
+ },
199
+ },
200
+ },
201
+ },
202
+ };
203
+ const options = {
204
+ specPath: "dummy",
205
+ outputDir: "build",
206
+ };
207
+ const sidebarOptions = {};
208
+ const [items] = await (0, openapi_1.processOpenapiFile)(openapiData, options, sidebarOptions);
209
+ const apiItems = items.filter((item) => item.type === "api");
210
+ expect(apiItems).toHaveLength(5);
211
+ const customVerbItem = apiItems.find((item) => item.type === "api" && item.id === "custom-verb-operation");
212
+ expect(customVerbItem.api.path).toBe("/api/resource:customVerb");
213
+ expect(customVerbItem.api.method).toBe("post");
214
+ expect(customVerbItem.api.postman).toBeDefined();
215
+ const standardItem = apiItems.find((item) => item.type === "api" && item.id === "get-user-by-id");
216
+ expect(standardItem.api.path).toBe("/api/users/{id}");
217
+ expect(standardItem.api.method).toBe("get");
218
+ expect(standardItem.api.postman).toBeDefined();
219
+ const multiParamItem = apiItems.find((item) => item.type === "api" && item.id === "get-user-post");
220
+ expect(multiParamItem.api.path).toBe("/api/users/{userId}/posts/{postId}");
221
+ expect(multiParamItem.api.method).toBe("get");
222
+ expect(multiParamItem.api.postman).toBeDefined();
223
+ const sameSegmentItem = apiItems.find((item) => item.type === "api" && item.id === "get-file-by-name-and-ext");
224
+ expect(sameSegmentItem.api.path).toBe("/files/{name}.{ext}");
225
+ expect(sameSegmentItem.api.method).toBe("get");
226
+ expect(sameSegmentItem.api.postman).toBeDefined();
227
+ const templatedVerbItem = apiItems.find((item) => item.type === "api" && item.id === "cancel-job");
228
+ expect(templatedVerbItem.api.path).toBe("/jobs/{id}:cancel");
229
+ expect(templatedVerbItem.api.method).toBe("post");
230
+ expect(templatedVerbItem.api.postman).toBeDefined();
231
+ });
232
+ });
81
233
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "docusaurus-plugin-openapi-docs",
3
3
  "description": "OpenAPI plugin for Docusaurus.",
4
- "version": "0.0.0-1105",
4
+ "version": "0.0.0-1107",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "openapi",
@@ -65,5 +65,5 @@
65
65
  "engines": {
66
66
  "node": ">=14"
67
67
  },
68
- "gitHead": "5a573d71d569e71efd1a08fc4abbd4c822533e55"
68
+ "gitHead": "50a1285465efa4833527f43efa0d2a33ea1c03f6"
69
69
  }
@@ -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
  }