Skip to content

Commit bbf7efe

Browse files
Copilotbaywet
andauthored
feat(openapi3): add @pageSize decorator support for x-ms-list-page-size extension (#9618)
OpenAPI parameters with `x-ms-list-page-size: true` were not being converted to TypeSpec's `@pageSize` decorator during import. ## Changes - **Modified `getParameterDecorators()`** in `decorators.ts` to detect `x-ms-list-page-size` extension and emit `@pageSize` decorator when `true` - **Added tests** covering true/false/absent cases and full document rendering ## Example OpenAPI parameter: ```yaml parameters: - name: page_size in: query schema: type: integer format: int32 x-ms-list-page-size: true ``` Now converts to: ```typescript @extension("x-ms-list-page-size", true) @pageSize @query page_size: int32 ``` Note: The `@extension` decorator was already being emitted; this adds the semantic `@pageSize` decorator alongside it. <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> ---- *This section details on the original issue you should resolve* <issue_title>Add support for importing the pageSize decorator based on the relevant OpenAPI extension</issue_title> <issue_description>### Clear and concise description of the problem Related microsoft/openai-openapi-pr#568 Based on the following OpenAPI description, we should get the following TypeSpec definition imported ```yaml openapi: 3.0.0 info: title: Widget Service version: 0.0.0 tags: - name: Widgets paths: /widgets: get: operationId: Widgets_list description: List widgets parameters: - name: page_size in: query required: true schema: type: integer format: int32 #this is the extension that matters x-ms-list-page-size: true explode: false responses: '200': description: The request has succeeded. content: application/json: schema: type: array items: $ref: '#/components/schemas/Widget' default: description: An unexpected error response. content: application/json: schema: $ref: '#/components/schemas/Error' tags: - Widgets post: operationId: Widgets_create description: Create a widget parameters: [] responses: '200': description: The request has succeeded. content: application/json: schema: $ref: '#/components/schemas/Widget' default: description: An unexpected error response. content: application/json: schema: $ref: '#/components/schemas/Error' tags: - Widgets requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/Widget' /widgets/{id}: get: operationId: Widgets_read description: Read widgets parameters: - name: id in: path required: true schema: type: string readOnly: true responses: '200': description: The request has succeeded. content: application/json: schema: $ref: '#/components/schemas/Widget' default: description: An unexpected error response. content: application/json: schema: $ref: '#/components/schemas/Error' tags: - Widgets patch: operationId: Widgets_update description: Update a widget parameters: - name: id in: path required: true schema: type: string readOnly: true responses: '200': description: The request has succeeded. content: application/json: schema: $ref: '#/components/schemas/Widget' default: description: An unexpected error response. content: application/json: schema: $ref: '#/components/schemas/Error' tags: - Widgets requestBody: required: true content: application/merge-patch+json: schema: $ref: '#/components/schemas/WidgetMergePatchUpdate' delete: operationId: Widgets_delete description: Delete a widget parameters: - name: id in: path required: true schema: type: string readOnly: true responses: '204': description: 'There is no content to send for this request, but the headers may be useful. ' default: description: An unexpected error response. content: application/json: schema: $ref: '#/components/schemas/Error' tags: - Widgets /widgets/{id}/analyze: post: operationId: Widgets_analyze description: Analyze a widget parameters: - name: id in: path required: true schema: type: string readOnly: true responses: '200': description: The request has succeeded. content: text/plain: schema: type: string default: description: An unexpected error response. content: application/json: schema: $ref: '#/components/schemas/Error' tags: - Widgets components: schemas: Error: type: object required: - code - message properties: code: type: integer format: int32 message: ... </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes #9616 <!-- START COPILOT CODING AGENT TIPS --> --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> Co-authored-by: Vincent Biret <vibiret@microsoft.com>
1 parent 5e73d5b commit bbf7efe

File tree

3 files changed

+166
-0
lines changed

3 files changed

+166
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- "@typespec/openapi3"
5+
---
6+
7+
importer - Add support for `@pageSize` decorator based on x-ms-list-page-size extension

packages/openapi3/src/cli/actions/convert/utils/decorators.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@ export function getParameterDecorators(parameter: OpenAPI3Parameter | OpenAPIPar
9191
decorators.push(...getDecoratorsForSchema(parameter.schema));
9292
}
9393

94+
// Add @pageSize decorator if x-ms-list-page-size extension is true
95+
const xmsListPageSize = (parameter as any)["x-ms-list-page-size"];
96+
if (xmsListPageSize === true) {
97+
decorators.push({
98+
name: "pageSize",
99+
args: [],
100+
});
101+
}
102+
94103
// Handle x-ms-list-continuation-token extension on parameter itself
95104
const xmsListContinuationToken = (parameter as any)["x-ms-list-continuation-token"];
96105
if (xmsListContinuationToken === true) {

packages/openapi3/test/tsp-openapi3/parameters.test.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,3 +728,153 @@ describe("query", () => {
728728
});
729729
});
730730
});
731+
732+
describe("x-ms-list-page-size extension", () => {
733+
it("adds @pageSize decorator when x-ms-list-page-size is true", async () => {
734+
const serviceNamespace = await tspForOpenAPI3({
735+
parameters: {
736+
PageSize: {
737+
name: "page_size",
738+
in: "query",
739+
required: true,
740+
schema: {
741+
type: "integer",
742+
format: "int32",
743+
},
744+
"x-ms-list-page-size": true,
745+
} as any,
746+
},
747+
});
748+
749+
const parametersNamespace = serviceNamespace.namespaces.get("Parameters");
750+
assert(parametersNamespace, "Parameters namespace not found");
751+
752+
const models = parametersNamespace.models;
753+
const PageSize = models.get("PageSize");
754+
assert(PageSize, "PageSize model not found");
755+
expect(PageSize.properties.size).toBe(1);
756+
757+
const pageSizeProperty = PageSize.properties.get("page_size");
758+
assert(pageSizeProperty, "page_size property not found");
759+
760+
// Should have @query, @extension, and @pageSize decorators
761+
expectDecorators(
762+
pageSizeProperty.decorators,
763+
[
764+
{ name: "extension", args: ["x-ms-list-page-size", true] },
765+
{ name: "pageSize" },
766+
{ name: "query" },
767+
],
768+
{ strict: false },
769+
);
770+
});
771+
772+
it("renders complete TypeSpec with @pageSize decorator", async () => {
773+
const tsp = await renderTypeSpecForOpenAPI3({
774+
paths: {
775+
"/widgets": {
776+
get: {
777+
operationId: "listWidgets",
778+
description: "List widgets",
779+
parameters: [
780+
{
781+
name: "page_size",
782+
in: "query",
783+
required: true,
784+
schema: {
785+
type: "integer",
786+
format: "int32",
787+
},
788+
"x-ms-list-page-size": true,
789+
explode: false,
790+
} as any,
791+
],
792+
responses: {
793+
"200": {
794+
description: "The request has succeeded.",
795+
content: {
796+
"application/json": {
797+
schema: {
798+
type: "array",
799+
items: {
800+
type: "object",
801+
properties: {
802+
id: { type: "string" },
803+
},
804+
},
805+
},
806+
},
807+
},
808+
},
809+
},
810+
},
811+
},
812+
},
813+
});
814+
815+
// Should include @pageSize and @extension decorators
816+
expect(tsp).toContain("@pageSize");
817+
expect(tsp).toContain('@extension("x-ms-list-page-size", true)');
818+
});
819+
820+
it("does not add @pageSize decorator when x-ms-list-page-size is false", async () => {
821+
const serviceNamespace = await tspForOpenAPI3({
822+
parameters: {
823+
PageSize: {
824+
name: "page_size",
825+
in: "query",
826+
required: true,
827+
schema: {
828+
type: "integer",
829+
format: "int32",
830+
},
831+
"x-ms-list-page-size": false,
832+
} as any,
833+
},
834+
});
835+
836+
const parametersNamespace = serviceNamespace.namespaces.get("Parameters");
837+
assert(parametersNamespace, "Parameters namespace not found");
838+
839+
const models = parametersNamespace.models;
840+
const PageSize = models.get("PageSize");
841+
assert(PageSize, "PageSize model not found");
842+
843+
const pageSizeProperty = PageSize.properties.get("page_size");
844+
assert(pageSizeProperty, "page_size property not found");
845+
846+
// Should only have @query and @extension decorators, not @pageSize
847+
const hasPageSize = pageSizeProperty.decorators.some((d) => d.definition?.name === "@pageSize");
848+
expect(hasPageSize).toBe(false);
849+
});
850+
851+
it("does not add @pageSize decorator when x-ms-list-page-size is absent", async () => {
852+
const serviceNamespace = await tspForOpenAPI3({
853+
parameters: {
854+
PageSize: {
855+
name: "page_size",
856+
in: "query",
857+
required: true,
858+
schema: {
859+
type: "integer",
860+
format: "int32",
861+
},
862+
},
863+
},
864+
});
865+
866+
const parametersNamespace = serviceNamespace.namespaces.get("Parameters");
867+
assert(parametersNamespace, "Parameters namespace not found");
868+
869+
const models = parametersNamespace.models;
870+
const PageSize = models.get("PageSize");
871+
assert(PageSize, "PageSize model not found");
872+
873+
const pageSizeProperty = PageSize.properties.get("page_size");
874+
assert(pageSizeProperty, "page_size property not found");
875+
876+
// Should only have @query decorator, not @pageSize
877+
const hasPageSize = pageSizeProperty.decorators.some((d) => d.definition?.name === "@pageSize");
878+
expect(hasPageSize).toBe(false);
879+
});
880+
});

0 commit comments

Comments
 (0)