Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor parameter handling in swagger emitter #1248

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@azure-tools/typespec-autorest"
---

Fix issue what allowed `multi` format on a header
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@
"schema": {
"type": "integer",
"format": "int64"
},
"default": 123
}
}
],
"responses": {
Expand Down
196 changes: 150 additions & 46 deletions packages/typespec-autorest/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,16 +131,20 @@ import { getExamples, getRef } from "./decorators.js";
import { sortWithJsonSchema } from "./json-schema-sorter/sorter.js";
import { createDiagnostic, reportDiagnostic } from "./lib.js";
import {
OpenAPI2BodyParameter,
OpenAPI2Document,
OpenAPI2FileSchema,
OpenAPI2FormDataParameter,
OpenAPI2HeaderDefinition,
OpenAPI2HeaderParameter,
OpenAPI2OAuth2FlowType,
OpenAPI2Operation,
OpenAPI2Parameter,
OpenAPI2ParameterBase,
OpenAPI2ParameterType,
OpenAPI2PathItem,
OpenAPI2PathParameter,
OpenAPI2QueryParameter,
OpenAPI2Response,
OpenAPI2Schema,
OpenAPI2SchemaProperty,
Expand Down Expand Up @@ -1208,7 +1212,7 @@ export async function getOpenAPIForService(
type: Type,
schemaContext: SchemaContext,
paramName: string
): Omit<OpenAPI2FormDataParameter, "in" | "name"> | undefined {
): PrimitiveItems | undefined {
if (isBytes(type)) {
return { type: "file" };
}
Expand Down Expand Up @@ -1240,66 +1244,166 @@ export async function getOpenAPIForService(
}
}

function getOpenAPI2Parameter<T extends OpenAPI2ParameterType>(
function getOpenAPI2ParameterBase(
param: ModelProperty,
kind: T,
schemaContext: SchemaContext,
name?: string,
bodySchema?: any
): OpenAPI2Parameter & { in: T } {
const ph: any = {
name: string | undefined
): OpenAPI2ParameterBase {
const base: OpenAPI2ParameterBase = {
name: name ?? param.name,
in: kind,
required: !param.optional,
description: getDoc(program, param),
};
if (param.name !== ph.name) {
ph["x-ms-client-name"] = param.name;
}
if (param.defaultValue) {
ph.default = getDefaultValue(param.defaultValue);
if (param.name !== base.name) {
base["x-ms-client-name"] = param.name;
}

if (ph.in === "body") {
compilerAssert(bodySchema, "bodySchema argument is required to populate body parameter");
ph.schema = bodySchema;
} else if (ph.in === "formData") {
Object.assign(ph, getFormDataSchema(param.type, schemaContext, ph.name));
attachExtensions(param, base);

return base;
}

function getOpenAPI2BodyParameter(
param: ModelProperty,
name?: string,
bodySchema?: any
): OpenAPI2BodyParameter {
return {
in: "body",
...getOpenAPI2ParameterBase(param, name),
schema: bodySchema,
};
}

function getOpenAPI2FormDataParameter(
param: ModelProperty,
schemaContext: SchemaContext,
name?: string
): OpenAPI2FormDataParameter {
const base = getOpenAPI2ParameterBase(param, name);
return {
in: "formData",
...base,
...(getFormDataSchema(param.type, schemaContext, base.name) as any),
default: param.defaultValue && getDefaultValue(param.defaultValue),
};
}

function getSimpleParameterSchema(
param: ModelProperty,
schemaContext: SchemaContext,
name: string
): Pick<
OpenAPI2QueryParameter | OpenAPI2HeaderParameter | OpenAPI2PathParameter,
"type" | "items"
> {
if (param.type.kind === "Model" && isArrayModelType(program, param.type)) {
const itemSchema = getSchemaForPrimitiveItems(param.type.indexer.value, schemaContext, name);
const schema = itemSchema && {
...itemSchema,
};
delete (schema as any).description;
return { type: "array", items: schema };
} else {
const collectionFormat = (
kind === "query"
? getQueryParamOptions(program, param)
: kind === "header"
? getHeaderFieldOptions(program, param)
: undefined
)?.format;
if (collectionFormat === "multi" && !["query", "header", "formData"].includes(ph.in)) {
reportDiagnostic(program, { code: "invalid-multi-collection-format", target: param });
}
if (collectionFormat) {
ph.collectionFormat = collectionFormat;
}

if (param.type.kind === "Model" && isArrayModelType(program, param.type)) {
ph.type = "array";
const schema = {
...getSchemaForPrimitiveItems(param.type.indexer.value, schemaContext, ph.name),
};
delete (schema as any).description;
ph.items = schema;
} else {
Object.assign(ph, getSchemaForPrimitiveItems(param.type, schemaContext, ph.name));
}
return getSchemaForPrimitiveItems(param.type, schemaContext, name) as any;
}
}

attachExtensions(param, ph);
function getOpenAPI2QueryParameter(
param: ModelProperty,
schemaContext: SchemaContext,
name?: string
): OpenAPI2QueryParameter {
const base = getOpenAPI2ParameterBase(param, name);
let collectionFormat = getQueryParamOptions(program, param).format;
if (collectionFormat && !["csv", "ssv", "tsv", "pipes", "multi"].includes(collectionFormat)) {
collectionFormat = undefined;
reportDiagnostic(program, { code: "invalid-multi-collection-format", target: param });
}

return {
in: "query",
collectionFormat: collectionFormat as any,
default: param.defaultValue && getDefaultValue(param.defaultValue),
...base,
...getSimpleParameterSchema(param, schemaContext, base.name),
};
}

function getOpenAPI2PathParameter(
param: ModelProperty,
schemaContext: SchemaContext,
name?: string
): OpenAPI2PathParameter {
const base = getOpenAPI2ParameterBase(param, name);

return {
in: "path",
default: param.defaultValue && getDefaultValue(param.defaultValue),
...base,
...getSimpleParameterSchema(param, schemaContext, base.name),
};
}
function getOpenAPI2HeaderParameter(
param: ModelProperty,
schemaContext: SchemaContext,
name?: string
): OpenAPI2HeaderParameter {
const base = getOpenAPI2ParameterBase(param, name);
let collectionFormat = getHeaderFieldOptions(program, param).format;
if (collectionFormat && !["csv", "ssv", "tsv", "pipes"].includes(collectionFormat)) {
collectionFormat = undefined;
reportDiagnostic(program, { code: "invalid-multi-collection-format", target: param });
}
return {
in: "header",
default: param.defaultValue && getDefaultValue(param.defaultValue),
...base,
collectionFormat: collectionFormat as any,
...getSimpleParameterSchema(param, schemaContext, base.name),
};
}

function getOpenAPI2ParameterInternal<T extends OpenAPI2Parameter["in"]>(
param: ModelProperty,
kind: T,
schemaContext: SchemaContext,
name?: string,
bodySchema?: any
): OpenAPI2Parameter & { in: T } {
switch (kind) {
case "body":
return getOpenAPI2BodyParameter(param, name, bodySchema) as any;
case "formData":
return getOpenAPI2FormDataParameter(param, schemaContext, name) as any;
case "query":
return getOpenAPI2QueryParameter(param, schemaContext, name) as any;
case "path":
return getOpenAPI2PathParameter(param, schemaContext, name) as any;
case "header":
return getOpenAPI2HeaderParameter(param, schemaContext, name) as any;
default:
const _assertNever: never = kind;
compilerAssert(false, "Unreachable");
}
}

function getOpenAPI2Parameter<T extends OpenAPI2Parameter["in"]>(
param: ModelProperty,
kind: T,
schemaContext: SchemaContext,
name?: string,
bodySchema?: any
): OpenAPI2Parameter & { in: T } {
const value = getOpenAPI2ParameterInternal(param, kind, schemaContext, name, bodySchema);
// Apply decorators to a copy of the parameter definition. We use
// Object.assign here because applyIntrinsicDecorators returns a new object
// based on the target object and we need to apply its changes back to the
// original parameter.
Object.assign(ph, applyIntrinsicDecorators(param, { type: ph.type, format: ph.format }));
return ph;
Object.assign(
value,
applyIntrinsicDecorators(param, { type: (value as any).type, format: (value as any).format })
);
return value;
}

function populateParameter(
Expand Down
16 changes: 11 additions & 5 deletions packages/typespec-autorest/src/openapi2-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,10 +301,10 @@ export type OpenAPI2ParameterType = OpenAPI2Parameter["in"];

export interface OpenAPI2HeaderDefinition {
type: "string" | "number" | "integer" | "boolean" | "array";
items?: OpenAPI2Schema;
collectionFormat?: "csv" | "ssv" | "tsv" | "pipes";
description?: string;
format?: string;
items?: PrimitiveItems;
}

export type OpenAPI2Parameter =
Expand All @@ -315,6 +315,10 @@ export type OpenAPI2Parameter =
| OpenAPI2PathParameter;

export interface OpenAPI2ParameterBase extends Extensions {
name: string;
description?: string;
required?: boolean;

/**
* Provide a different name to be used in the client.
*/
Expand All @@ -323,11 +327,8 @@ export interface OpenAPI2ParameterBase extends Extensions {
}

export interface OpenAPI2BodyParameter extends OpenAPI2ParameterBase {
name: string;
in: "body";
schema: OpenAPI2Schema;
description?: string;
required?: boolean;
allowEmptyValue?: boolean;
example?: unknown;

Expand All @@ -338,6 +339,7 @@ export interface OpenAPI2HeaderParameter extends OpenAPI2HeaderDefinition, OpenA
name: string;
in: "header";
required?: boolean;
default?: unknown;
}

export interface OpenAPI2FormDataParameter extends OpenAPI2ParameterBase {
Expand All @@ -359,7 +361,7 @@ export interface OpenAPI2FormDataParameter extends OpenAPI2ParameterBase {
"x-ms-client-flatten"?: boolean;
}

export interface PrimitiveItems extends OpenAPI2ParameterBase {
export interface PrimitiveItems {
type: "string" | "number" | "integer" | "boolean" | "array" | "file";
format?: string;
items?: PrimitiveItems;
Expand All @@ -376,7 +378,9 @@ export interface OpenAPI2PathParameter extends OpenAPI2ParameterBase {
required?: boolean;
format?: string;
enum?: string[];
items?: PrimitiveItems;
"x-ms-skip-url-encoding"?: boolean;
default?: unknown;
}

export interface OpenAPI2QueryParameter extends OpenAPI2ParameterBase {
Expand All @@ -389,6 +393,8 @@ export interface OpenAPI2QueryParameter extends OpenAPI2ParameterBase {
required?: boolean;
format?: string;
enum?: string[];
items?: PrimitiveItems;
default?: unknown;
}

export type HttpMethod = "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace";
Expand Down
4 changes: 2 additions & 2 deletions packages/typespec-autorest/test/parameters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,11 +355,11 @@ describe("typespec-autorest: parameters", () => {
});

it("array of enum is kept inline", async () => {
deepStrictEqual(await testParameter(`@${kind}({format: "multi"})`, "Foo[]"), {
deepStrictEqual(await testParameter(`@${kind}({format: "csv"})`, "Foo[]"), {
in: kind,
name: "arg1",
required: true,
collectionFormat: "multi",
collectionFormat: "csv",
type: "array",
items: {
type: "string",
Expand Down
Loading