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

tsp-openapi3 - fixes query decorator arg generation #4873

Merged
merged 4 commits into from
Oct 31, 2024
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:
- "@typespec/openapi3"
---

Updates tsp-openapi3 query decorator generation to use the value arguments.
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { TypeSpecDecorator } from "../interfaces.js";
import { TSValue, TypeSpecDecorator } from "../interfaces.js";

function generateDecorator({ name, args }: TypeSpecDecorator): string {
const hasArgs = args.length;
const stringifiedArguments = hasArgs ? `(${args.map((a) => JSON.stringify(a)).join(", ")})` : "";
const stringifiedArguments = hasArgs
? `(${args.map((a) => (isTSValue(a) ? a.value : JSON.stringify(a))).join(", ")})`
: "";

return `@${name}${stringifiedArguments}`;
}
Expand All @@ -11,3 +13,9 @@ export function generateDecorators(decorators: TypeSpecDecorator[]): string[] {
const uniqueDecorators = new Set<string>(decorators.map(generateDecorator));
return Array.from(uniqueDecorators);
}

function isTSValue(value: unknown): value is TSValue {
return Boolean(
value && typeof value === "object" && "__kind" in value && value["__kind"] === "value",
);
}
11 changes: 10 additions & 1 deletion packages/openapi3/src/cli/actions/convert/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,16 @@ export interface TypeSpecServiceInfo {

export interface TypeSpecDecorator {
name: string;
args: (object | number | string)[];
args: (object | number | string | TSValue)[];
}

export interface TSValue {
__kind: "value";
/**
* String representation of the value.
* This will be directly substituted into the generated code.
*/
value: string;
}

export interface TypeSpecAugmentation extends TypeSpecDecorator {
Expand Down
66 changes: 55 additions & 11 deletions packages/openapi3/src/cli/actions/convert/utils/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ExtensionKey } from "@typespec/openapi";
import { Extensions, OpenAPI3Parameter, OpenAPI3Schema, Refable } from "../../../../types.js";
import { TypeSpecDecorator } from "../interfaces.js";
import { TSValue, TypeSpecDecorator } from "../interfaces.js";

const validLocations = ["header", "query", "path"];
const extensionDecoratorName = "extension";
Expand Down Expand Up @@ -45,35 +45,79 @@ function getLocationDecorator(parameter: OpenAPI3Parameter): TypeSpecDecorator |
};

let format: string | undefined;
let decoratorArgs: TypeSpecDecorator["args"][0] | undefined;
switch (parameter.in) {
case "header":
format = getHeaderFormat(parameter.style);
break;
case "query":
format = getQueryFormat(parameter.explode, parameter.style);
decoratorArgs = getQueryArgs(parameter);
break;
}

if (format) {
decorator.args.push({ format });
}
if (decoratorArgs) {
decorator.args.push(decoratorArgs);
}

return decorator;
}

function getQueryFormat(explode?: boolean, style?: string): string | undefined {
if (explode) {
return "form";
} else if (style === "form") {
return "simple";
} else if (style === "spaceDelimited") {
return "ssv";
} else if (style === "pipeDelimited") {
return "pipes";
function getQueryArgs(parameter: OpenAPI3Parameter): TSValue | undefined {
const queryOptions = getNormalizedQueryOptions(parameter);
if (Object.keys(queryOptions).length) {
return {
__kind: "value",
value: `#{${Object.entries(queryOptions)
.map(([key, value]) => {
return `${key}: ${JSON.stringify(value)}`;
})
.join(", ")}}`,
};
}

return;
}

type QueryOptions = { explode?: boolean; format?: string };

function getNormalizedQueryOptions({
explode,
style = "",
}: {
explode?: boolean;
style?: string;
}): QueryOptions {
const queryOptions: QueryOptions = {};
// In OpenAPI 3, default style is 'form', and explode is true when 'form' is the style
if (typeof explode !== "boolean") {
if (style === "form" || !style) {
explode = true;
} else {
explode = false;
}
}

// Format only emits additional data if set to one of the following:
// ssv (spaceDelimited), pipes (pipeDelimited)
if (style === "spaceDelimited") {
queryOptions.format = "ssv";
} else if (style === "pipeDelimited") {
queryOptions.format = "pipes";
}

// In TypeSpec, default explode is "false"
if (!explode && queryOptions.format) {
queryOptions.explode = false;
} else if (explode) {
queryOptions.explode = true;
}

return queryOptions;
}

function getHeaderFormat(style?: string): string | undefined {
return style === "simple" ? "simple" : undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ model `Escaped-Model` {
model `get-thingDefaultResponse` {}

@route("/{escaped-property}") @get op `get-thing`(
@query `weird@param`?: `Foo-Bar`,
@query(#{ explode: true }) `weird@param`?: `Foo-Bar`,
...Parameters.`Escaped-Model`.`escaped-property`,
@bodyRoot body: `Escaped-Model`,
): `get-thingDefaultResponse`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ model Operations_putThing200ApplicationJsonResponse {

@minValue(0)
@maxValue(10)
@query
@query(#{ explode: true })
count: int32,
): Operations_getThing200ApplicationJsonResponse;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ op listPets(
/**
* How many items to return at one time (max 100)
*/
@query limit?: int32,
@query(#{ explode: true }) limit?: int32,
): listPets200ApplicationJsonResponse | listPetsDefaultApplicationJsonResponse;

@tag("pets")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,10 +539,7 @@ op findPetsByStatus(
/**
* Status values that need to be considered for filter
*/
@query({
format: "form",
})
status?: "available" | "pending" | "sold" = "available",
@query(#{ explode: true }) status?: "available" | "pending" | "sold" = "available",
): findPetsByStatus200ApplicationXmlResponse | findPetsByStatus200ApplicationJsonResponse | findPetsByStatus400Response;

/**
Expand All @@ -556,10 +553,7 @@ op findPetsByTags(
/**
* Tags to filter by
*/
@query({
format: "form",
})
tags?: string[],
@query(#{ explode: true }) tags?: string[],
): findPetsByTags200ApplicationXmlResponse | findPetsByTags200ApplicationJsonResponse | findPetsByTags400Response;

@tag("pet")
Expand Down Expand Up @@ -606,12 +600,12 @@ op updatePetWithForm(
/**
* Name of pet that needs to be updated
*/
@query name?: string,
@query(#{ explode: true }) name?: string,

/**
* Status of pet that needs to be updated
*/
@query status?: string,
@query(#{ explode: true }) status?: string,
): updatePetWithForm405Response;

@tag("pet")
Expand All @@ -627,7 +621,7 @@ op uploadFile(
/**
* Additional Metadata
*/
@query additionalMetadata?: string,
@query(#{ explode: true }) additionalMetadata?: string,

@header contentType: "application/octet-stream",
@bodyRoot body: bytes,
Expand Down Expand Up @@ -722,12 +716,12 @@ op loginUser(
/**
* The user name for login
*/
@query username?: string,
@query(#{ explode: true }) username?: string,

/**
* The password for login in clear text
*/
@query password?: string,
@query(#{ explode: true }) password?: string,
): loginUser200ApplicationXmlResponse | loginUser200ApplicationJsonResponse | loginUser400Response;

@tag("user")
Expand Down
Loading
Loading