This commit is contained in:
Timothee Guerin 2024-08-13 10:06:15 -07:00 коммит произвёл GitHub
Родитель 6567ec21bf
Коммит b9e465b94e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
22 изменённых файлов: 119 добавлений и 29 удалений

6
.github/workflows/consistency.yml поставляемый
Просмотреть файл

@ -24,13 +24,13 @@ jobs:
- uses: ./.github/actions/setup - uses: ./.github/actions/setup
- run: git pull --force --no-tags origin main:main - run: git pull --force --no-tags origin ${{ github.event.pull_request.base.ref }}:${{ github.event.pull_request.base.ref }}
name: Get main ref name: Get ${{ github.event.pull_request.base.ref }} ref for ${{ github.ref}}, evt ${{ github.event_name }}
- run: pnpm install - run: pnpm install
name: Install dependencies name: Install dependencies
- run: npx chronus verify - run: npx chronus verify --since ${{ github.event.pull_request.base.ref }}
name: Check changelog name: Check changelog
if: | if: |
!startsWith(github.head_ref, 'publish/') && !startsWith(github.head_ref, 'publish/') &&

Просмотреть файл

@ -7,6 +7,7 @@ pr:
branches: branches:
include: include:
- main - main
- release/*
extends: extends:
template: /eng/common/pipelines/templates/1es-redirect.yml template: /eng/common/pipelines/templates/1es-redirect.yml

Просмотреть файл

@ -1,5 +1,12 @@
# Change Log - @typespec/http # Change Log - @typespec/http
## 0.59.1
### Bug Fixes
- [#4155](https://github.com/microsoft/typespec/pull/4155) HotFix: Uri template not correctly built when using `@autoRoute`
## 0.59.0 ## 0.59.0
### Bug Fixes ### Bug Fixes

Просмотреть файл

@ -1,6 +1,6 @@
{ {
"name": "@typespec/http", "name": "@typespec/http",
"version": "0.59.0", "version": "0.59.1",
"author": "Microsoft Corporation", "author": "Microsoft Corporation",
"description": "TypeSpec HTTP protocol binding", "description": "TypeSpec HTTP protocol binding",
"homepage": "https://github.com/microsoft/typespec", "homepage": "https://github.com/microsoft/typespec",

Просмотреть файл

@ -15,6 +15,7 @@ import {
HttpOperation, HttpOperation,
HttpOperationParameter, HttpOperationParameter,
HttpOperationParameters, HttpOperationParameters,
HttpOperationPathParameter,
PathParameterOptions, PathParameterOptions,
RouteOptions, RouteOptions,
RoutePath, RoutePath,
@ -223,24 +224,30 @@ const styleToOperator: Record<PathParameterOptions["style"], string> = {
fragment: "#", fragment: "#",
}; };
function addOperationTemplateToUriTemplate(uriTemplate: string, params: HttpOperationParameter[]) { export function getUriTemplatePathParam(param: HttpOperationPathParameter) {
const pathParams = params
.filter((x) => x.type === "path")
.map((param) => {
const operator = param.allowReserved ? "+" : styleToOperator[param.style]; const operator = param.allowReserved ? "+" : styleToOperator[param.style];
return `{${operator}${param.name}${param.explode ? "*" : ""}}`; return `{${operator}${param.name}${param.explode ? "*" : ""}}`;
}); }
export function addQueryParamsToUriTemplate(uriTemplate: string, params: HttpOperationParameter[]) {
const queryParams = params.filter((x) => x.type === "query"); const queryParams = params.filter((x) => x.type === "query");
const pathPart = joinPathSegments([uriTemplate, ...pathParams]);
return ( return (
pathPart + uriTemplate +
(queryParams.length > 0 (queryParams.length > 0
? `{?${queryParams.map((x) => escapeUriTemplateParamName(x.name)).join(",")}}` ? `{?${queryParams.map((x) => escapeUriTemplateParamName(x.name)).join(",")}}`
: "") : "")
); );
} }
function addOperationTemplateToUriTemplate(uriTemplate: string, params: HttpOperationParameter[]) {
const pathParams = params.filter((x) => x.type === "path").map(getUriTemplatePathParam);
const queryParams = params.filter((x) => x.type === "query");
const pathPart = joinPathSegments([uriTemplate, ...pathParams]);
return addQueryParamsToUriTemplate(pathPart, queryParams);
}
function escapeUriTemplateParamName(name: string) { function escapeUriTemplateParamName(name: string) {
return name.replaceAll(":", "%3A"); return name.replaceAll(":", "%3A");
} }

Просмотреть файл

@ -1,5 +1,12 @@
# Change Log - @typespec/openapi3 # Change Log - @typespec/openapi3
## 0.59.1
### Bug Fixes
- [#4168](https://github.com/microsoft/typespec/pull/4168) Fix: query params are `explode: true` by default in OpenAPI 3.0
## 0.59.0 ## 0.59.0
### Bug Fixes ### Bug Fixes

Просмотреть файл

@ -1,6 +1,6 @@
{ {
"name": "@typespec/openapi3", "name": "@typespec/openapi3",
"version": "0.59.0", "version": "0.59.1",
"author": "Microsoft Corporation", "author": "Microsoft Corporation",
"description": "TypeSpec library for emitting OpenAPI 3.0 from the TypeSpec REST protocol binding and converting OpenAPI3 to TypeSpec", "description": "TypeSpec library for emitting OpenAPI 3.0 from the TypeSpec REST protocol binding and converting OpenAPI3 to TypeSpec",
"homepage": "https://typespec.io", "homepage": "https://typespec.io",

Просмотреть файл

@ -1456,8 +1456,9 @@ function createOAPIEmitter(
function getQueryParameterAttributes(parameter: HttpOperationParameter & { type: "query" }) { function getQueryParameterAttributes(parameter: HttpOperationParameter & { type: "query" }) {
const attributes: { style?: string; explode?: boolean } = {}; const attributes: { style?: string; explode?: boolean } = {};
if (parameter.explode) { if (parameter.explode !== true) {
attributes.explode = true; // For query parameters(style: form) the default is explode: true https://spec.openapis.org/oas/v3.0.2#fixed-fields-9
attributes.explode = false;
} }
switch (parameter.format) { switch (parameter.format) {
@ -1465,9 +1466,10 @@ function createOAPIEmitter(
return { style: "spaceDelimited", explode: false }; return { style: "spaceDelimited", explode: false };
case "pipes": case "pipes":
return { style: "pipeDelimited", explode: false }; return { style: "pipeDelimited", explode: false };
case undefined:
case "csv": case "csv":
case "simple": case "simple":
return { explode: false };
case undefined:
case "multi": case "multi":
case "form": case "form":
return attributes; return attributes;

Просмотреть файл

@ -661,6 +661,7 @@ describe("openapi3: metadata", () => {
"Parameters.q": { "Parameters.q": {
name: "q", name: "q",
in: "query", in: "query",
explode: false,
required: true, required: true,
schema: { type: "string" }, schema: { type: "string" },
}, },
@ -717,6 +718,7 @@ describe("openapi3: metadata", () => {
name: "q", name: "q",
in: "query", in: "query",
required: true, required: true,
explode: false,
schema: { type: "string" }, schema: { type: "string" },
}, },
{ {

Просмотреть файл

@ -31,18 +31,32 @@ describe("query parameters", () => {
strictEqual(param.name, "$select"); strictEqual(param.name, "$select");
}); });
describe("set explode: true", () => { describe("doesn't set explode if explode: true (Openapi3.0 inverse default)", () => {
it("with option", async () => { it("with option", async () => {
const param = await getQueryParam(`op test(@query(#{explode: true}) myParam: string): void;`); const param = await getQueryParam(`op test(@query(#{explode: true}) myParam: string): void;`);
expect(param).toMatchObject({ expect(param).not.toHaveProperty("explode");
explode: true,
});
}); });
it("with uri template", async () => { it("with uri template", async () => {
const param = await getQueryParam(`@route("{?myParam*}") op test(myParam: string): void;`); const param = await getQueryParam(`@route("{?myParam*}") op test(myParam: string): void;`);
expect(param).not.toHaveProperty("explode");
});
});
describe("set explode: false if explode is not set", () => {
it("with option", async () => {
const param = await getQueryParam(
`op test(@query(#{explode: false}) myParam: string): void;`
);
expect(param).toMatchObject({ expect(param).toMatchObject({
explode: true, explode: false,
});
});
it("with uri template", async () => {
const param = await getQueryParam(`@route("{?myParam}") op test(myParam: string): void;`);
expect(param).toMatchObject({
explode: false,
}); });
}); });
}); });
@ -66,7 +80,6 @@ describe("query parameters", () => {
in: "query", in: "query",
name: "$multi", name: "$multi",
required: true, required: true,
explode: true,
schema: { schema: {
type: "array", type: "array",
items: { items: {
@ -77,6 +90,7 @@ describe("query parameters", () => {
deepStrictEqual(params[1], { deepStrictEqual(params[1], {
in: "query", in: "query",
name: "$csv", name: "$csv",
explode: false,
schema: { schema: {
type: "array", type: "array",
items: { items: {
@ -134,6 +148,7 @@ describe("query parameters", () => {
deepStrictEqual(res.paths["/"].get.parameters[0], { deepStrictEqual(res.paths["/"].get.parameters[0], {
in: "query", in: "query",
name: "id", name: "id",
explode: false,
required: true, required: true,
schema: { schema: {
type: "string", type: "string",

Просмотреть файл

@ -70,6 +70,7 @@ describe("openapi3: shared routes", () => {
{ {
in: "query", in: "query",
name: "resourceGroup", name: "resourceGroup",
explode: false,
required: false, required: false,
schema: { schema: {
type: "string", type: "string",
@ -78,6 +79,7 @@ describe("openapi3: shared routes", () => {
{ {
in: "query", in: "query",
name: "foo", name: "foo",
explode: false,
required: true, required: true,
schema: { schema: {
type: "string", type: "string",
@ -86,6 +88,7 @@ describe("openapi3: shared routes", () => {
{ {
in: "query", in: "query",
name: "subscription", name: "subscription",
explode: false,
required: false, required: false,
schema: { schema: {
type: "string", type: "string",
@ -130,6 +133,7 @@ describe("openapi3: shared routes", () => {
{ {
in: "query", in: "query",
name: "filter", name: "filter",
explode: false,
required: true, required: true,
schema: { schema: {
type: "string", type: "string",
@ -176,6 +180,7 @@ describe("openapi3: shared routes", () => {
name: "filter", name: "filter",
in: "query", in: "query",
required: false, required: false,
explode: false,
schema: { schema: {
type: "string", type: "string",
enum: ["resourceGroup"], enum: ["resourceGroup"],

Просмотреть файл

@ -1,5 +1,12 @@
# Change Log - @typespec/rest # Change Log - @typespec/rest
## 0.59.1
### Bug Fixes
- [#4155](https://github.com/microsoft/typespec/pull/4155) HotFix: Uri template not correctly built when using `@autoRoute`
## 0.59.0 ## 0.59.0
### Bump dependencies ### Bump dependencies

Просмотреть файл

@ -1,6 +1,6 @@
{ {
"name": "@typespec/rest", "name": "@typespec/rest",
"version": "0.59.0", "version": "0.59.1",
"author": "Microsoft Corporation", "author": "Microsoft Corporation",
"description": "TypeSpec REST protocol binding", "description": "TypeSpec REST protocol binding",
"homepage": "https://typespec.io", "homepage": "https://typespec.io",

Просмотреть файл

@ -12,11 +12,13 @@ import {
Type, Type,
} from "@typespec/compiler"; } from "@typespec/compiler";
import { import {
addQueryParamsToUriTemplate,
DefaultRouteProducer, DefaultRouteProducer,
getOperationParameters, getOperationParameters,
getOperationVerb, getOperationVerb,
getRoutePath, getRoutePath,
getRouteProducer, getRouteProducer,
getUriTemplatePathParam,
HttpOperation, HttpOperation,
HttpOperationParameter, HttpOperationParameter,
HttpOperationParameters, HttpOperationParameters,
@ -119,7 +121,7 @@ function autoRouteProducer(
); );
for (const httpParam of parameters.parameters) { for (const httpParam of parameters.parameters) {
const { type, param, name } = httpParam; const { type, param } = httpParam;
if (type === "path") { if (type === "path") {
addSegmentFragment(program, param, segments); addSegmentFragment(program, param, segments);
@ -137,7 +139,7 @@ function autoRouteProducer(
segments.push(`/${param.type.value}`); segments.push(`/${param.type.value}`);
continue; // Skip adding to the parameter list continue; // Skip adding to the parameter list
} else { } else {
segments.push(`/{${name}}`); segments.push(`/${getUriTemplatePathParam(httpParam)}`);
} }
} }
} }
@ -155,8 +157,10 @@ function autoRouteProducer(
// Add the operation's action segment if present // Add the operation's action segment if present
addActionFragment(program, operation, segments); addActionFragment(program, operation, segments);
const pathPart = joinPathSegments(segments);
return diagnostics.wrap({ return diagnostics.wrap({
uriTemplate: joinPathSegments(segments), uriTemplate: addQueryParamsToUriTemplate(pathPart, filteredParameters),
parameters: { parameters: {
...parameters, ...parameters,
parameters: filteredParameters, parameters: filteredParameters,

Просмотреть файл

@ -2,7 +2,7 @@ import { ModelProperty, Operation } from "@typespec/compiler";
import { expectDiagnostics } from "@typespec/compiler/testing"; import { expectDiagnostics } from "@typespec/compiler/testing";
import { isSharedRoute } from "@typespec/http"; import { isSharedRoute } from "@typespec/http";
import { deepStrictEqual, strictEqual } from "assert"; import { deepStrictEqual, strictEqual } from "assert";
import { describe, it } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
compileOperations, compileOperations,
createRestTestRunner, createRestTestRunner,
@ -521,3 +521,29 @@ describe("rest: routes", () => {
]); ]);
}); });
}); });
describe("uri template", () => {
async function getOp(code: string) {
const ops = await getOperations(code);
return ops[0];
}
describe("build uriTemplate from parameter", () => {
it.each([
["@path one: string", "/foo/{one}"],
["@path(#{allowReserved: true}) one: string", "/foo/{+one}"],
["@path(#{explode: true}) one: string", "/foo/{one*}"],
[`@path(#{style: "matrix"}) one: string`, "/foo/{;one}"],
[`@path(#{style: "label"}) one: string`, "/foo/{.one}"],
[`@path(#{style: "fragment"}) one: string`, "/foo/{#one}"],
[`@path(#{style: "path"}) one: string`, "/foo/{/one}"],
["@path(#{allowReserved: true, explode: true}) one: string", "/foo/{+one*}"],
["@query one: string", "/foo{?one}"],
// cspell:ignore Atwo
[`@query("one:two") one: string`, "/foo{?one%3Atwo}"],
])("%s -> %s", async (param, expectedUri) => {
const op = await getOp(`@route("/foo") interface Test {@autoRoute op foo(${param}): void;}`);
expect(op.uriTemplate).toEqual(expectedUri);
});
});
});

Просмотреть файл

@ -270,6 +270,7 @@ components:
schema: schema:
type: integer type: integer
format: int32 format: int32
explode: false
ListRequestBase.page_token: ListRequestBase.page_token:
name: page_token name: page_token
in: query in: query
@ -281,6 +282,7 @@ components:
returned from the previous call to `ListShelves` method. returned from the previous call to `ListShelves` method.
schema: schema:
type: string type: string
explode: false
MergeShelvesRequest.name: MergeShelvesRequest.name:
name: name name: name
in: path in: path

Просмотреть файл

@ -14,6 +14,7 @@ paths:
schema: schema:
type: string type: string
nullable: true nullable: true
explode: false
responses: responses:
'200': '200':
description: The request has succeeded. description: The request has succeeded.

Просмотреть файл

@ -14,6 +14,7 @@ paths:
schema: schema:
type: string type: string
default: defaultQueryString default: defaultQueryString
explode: false
responses: responses:
'200': '200':
description: The request has succeeded. description: The request has succeeded.

Просмотреть файл

@ -23,6 +23,7 @@ paths:
format: int32 format: int32
minimum: 0 minimum: 0
maximum: 10 maximum: 10
explode: false
responses: responses:
'200': '200':
description: The request has succeeded. description: The request has succeeded.

Просмотреть файл

@ -15,6 +15,7 @@ paths:
required: false required: false
schema: schema:
type: string type: string
explode: false
responses: responses:
'200': '200':
description: The request has succeeded. description: The request has succeeded.
@ -103,6 +104,7 @@ paths:
required: true required: true
schema: schema:
type: string type: string
explode: false
responses: responses:
'200': '200':
description: The request has succeeded. description: The request has succeeded.

Просмотреть файл

@ -459,6 +459,7 @@ paths:
required: true required: true
schema: schema:
type: string type: string
explode: false
responses: responses:
'200': '200':
description: The request has succeeded. description: The request has succeeded.

Просмотреть файл

@ -86,7 +86,6 @@ paths:
type: array type: array
items: items:
type: string type: string
explode: true
responses: responses:
'200': '200':
description: The request has succeeded. description: The request has succeeded.