Merge Hotfix back into main (#4167)
This commit is contained in:
Родитель
6567ec21bf
Коммит
b9e465b94e
|
@ -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.
|
||||||
|
|
Загрузка…
Ссылка в новой задаче