Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions demo/examples/tests/paramSerialization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,71 @@ paths:
"200":
description: Successful response

/api/resource:customVerb:
post:
tags:
- params
summary: Custom verb endpoint in path
description: |
Demonstrates a literal custom verb suffix in the path segment.
Example:
```
Result: /api/resource:customVerb
```
responses:
"200":
description: Successful response

/files/{name}.{ext}:
get:
tags:
- params
summary: Path parameters in the same segment
description: |
Demonstrates multiple path parameters in a single path segment.
Example:
```
{name} = "report"
{ext} = "pdf"
Result: /files/report.pdf
```
parameters:
- name: name
in: path
required: true
schema:
type: string
- name: ext
in: path
required: true
schema:
type: string
responses:
"200":
description: Successful response

/jobs/{id}:cancel:
post:
tags:
- params
summary: Path template combined with custom verb
description: |
Demonstrates a path parameter with a verb-like suffix in the same segment.
Example:
```
{id} = "123"
Result: /jobs/123:cancel
```
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: Successful response

/search:
get:
tags:
Expand Down
176 changes: 176 additions & 0 deletions packages/docusaurus-plugin-openapi-docs/src/openapi/openapi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,180 @@ describe("openapi", () => {
expect(schemaItems[0].id).toBe("without-tags");
});
});

describe("path template and custom verb handling", () => {
it("binds postman requests for OpenAPI templates and path verbs", async () => {
const openapiData = {
openapi: "3.0.0",
info: {
title: "Path Template API",
version: "1.0.0",
},
paths: {
"/api/resource:customVerb": {
post: {
summary: "Custom verb endpoint",
operationId: "customVerbOperation",
responses: {
"200": {
description: "OK",
},
},
},
},
"/api/users/{id}": {
get: {
summary: "Get user by ID",
operationId: "getUserById",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: {
type: "string",
},
},
],
responses: {
"200": {
description: "OK",
},
},
},
},
"/api/users/{userId}/posts/{postId}": {
get: {
summary: "Get user post",
operationId: "getUserPost",
parameters: [
{
name: "userId",
in: "path",
required: true,
schema: {
type: "string",
},
},
{
name: "postId",
in: "path",
required: true,
schema: {
type: "string",
},
},
],
responses: {
"200": {
description: "OK",
},
},
},
},
"/files/{name}.{ext}": {
get: {
summary: "Get file by name and extension",
operationId: "getFileByNameAndExt",
parameters: [
{
name: "name",
in: "path",
required: true,
schema: {
type: "string",
},
},
{
name: "ext",
in: "path",
required: true,
schema: {
type: "string",
},
},
],
responses: {
"200": {
description: "OK",
},
},
},
},
"/jobs/{id}:cancel": {
post: {
summary: "Cancel job",
operationId: "cancelJob",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: {
type: "string",
},
},
],
responses: {
"200": {
description: "OK",
},
},
},
},
},
};

const options: APIOptions = {
specPath: "dummy",
outputDir: "build",
};
const sidebarOptions = {} as SidebarOptions;
const [items] = await processOpenapiFile(
openapiData as any,
options,
sidebarOptions
);

const apiItems = items.filter((item) => item.type === "api");
expect(apiItems).toHaveLength(5);

const customVerbItem = apiItems.find(
(item) => item.type === "api" && item.id === "custom-verb-operation"
) as any;
expect(customVerbItem.api.path).toBe("/api/resource:customVerb");
expect(customVerbItem.api.method).toBe("post");
expect(customVerbItem.api.postman).toBeDefined();

const standardItem = apiItems.find(
(item) => item.type === "api" && item.id === "get-user-by-id"
) as any;
expect(standardItem.api.path).toBe("/api/users/{id}");
expect(standardItem.api.method).toBe("get");
expect(standardItem.api.postman).toBeDefined();

const multiParamItem = apiItems.find(
(item) => item.type === "api" && item.id === "get-user-post"
) as any;
expect(multiParamItem.api.path).toBe(
"/api/users/{userId}/posts/{postId}"
);
expect(multiParamItem.api.method).toBe("get");
expect(multiParamItem.api.postman).toBeDefined();

const sameSegmentItem = apiItems.find(
(item) => item.type === "api" && item.id === "get-file-by-name-and-ext"
) as any;
expect(sameSegmentItem.api.path).toBe("/files/{name}.{ext}");
expect(sameSegmentItem.api.method).toBe("get");
expect(sameSegmentItem.api.postman).toBeDefined();

const templatedVerbItem = apiItems.find(
(item) => item.type === "api" && item.id === "cancel-job"
) as any;
expect(templatedVerbItem.api.path).toBe("/jobs/{id}:cancel");
expect(templatedVerbItem.api.method).toBe("post");
expect(templatedVerbItem.api.postman).toBeDefined();
});
});
});
46 changes: 31 additions & 15 deletions packages/docusaurus-plugin-openapi-docs/src/openapi/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,28 +561,44 @@ function createItems(
/**
* Attach Postman Request objects to the corresponding ApiItems.
*/
function pathTemplateToRegex(pathTemplate: string): RegExp {
const pathWithTemplateTokens = pathTemplate.replace(
/\{[^}]+\}/g,
"__OPENAPI_PATH_PARAM__"
);
const escapedPathTemplate = pathWithTemplateTokens.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&"
);
const templatePattern = escapedPathTemplate.replace(
/__OPENAPI_PATH_PARAM__/g,
"[^/]+"
);
return new RegExp(`^${templatePattern}$`);
}

function bindCollectionToApiItems(
items: ApiMetadata[],
postmanCollection: sdk.Collection
) {
const apiMatchers = items
.filter((item): item is ApiPageMetadata => item.type === "api")
.map((item) => ({
apiItem: item,
method: item.api.method.toLowerCase(),
pathMatcher: pathTemplateToRegex(item.api.path),
}));

postmanCollection.forEachItem((item: any) => {
const method = item.request.method.toLowerCase();
const path = item.request.url
.getPath({ unresolved: true }) // unresolved returns "/:variableName" instead of "/<type>"
.replace(/(?<![a-z0-9-_]+):([a-z0-9-_]+)/gi, "{$1}"); // replace "/:variableName" with "/{variableName}"
const apiItem = items.find((item) => {
if (
item.type === "info" ||
item.type === "tag" ||
item.type === "schema"
) {
return false;
}
return item.api.path === path && item.api.method === method;
});
const postmanPath = item.request.url.getPath({ unresolved: true });
const match = apiMatchers.find(
({ method: itemMethod, pathMatcher }) =>
itemMethod === method && pathMatcher.test(postmanPath)
);

if (apiItem?.type === "api") {
apiItem.api.postman = item.request;
if (match) {
match.apiItem.api.postman = item.request;
}
});
}
Expand Down
Loading