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 - scope top-level parameters to Parameters namespace #4216

Merged
merged 6 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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"
---

Fixes issue in tsp-openapi3 that resulted in component schemas and parameters with the same name being merged into a single TypeSpec data type.
6 changes: 4 additions & 2 deletions packages/openapi3/src/cli/actions/convert/convert-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ import { handleInternalCompilerError } from "../../utils.js";
import { ConvertCliArgs } from "./args.js";
import { generateMain } from "./generators/generate-main.js";
import { transform } from "./transforms/transforms.js";
import { createContext } from "./utils/context.js";

export async function convertAction(host: CliHost, args: ConvertCliArgs) {
// attempt to read the file
const fullPath = resolvePath(process.cwd(), args.path);
const model = await parseOpenApiFile(fullPath);
const program = transform(model);
const context = createContext(model);
const program = transform(context);
let mainTsp: string;
try {
mainTsp = generateMain(program);
mainTsp = generateMain(program, context);
} catch (err) {
handleInternalCompilerError(err);
}
Expand Down
6 changes: 4 additions & 2 deletions packages/openapi3/src/cli/actions/convert/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { formatTypeSpec } from "@typespec/compiler";
import { OpenAPI3Document } from "../../../types.js";
import { generateMain } from "./generators/generate-main.js";
import { transform } from "./transforms/transforms.js";
import { createContext } from "./utils/context.js";

export async function convertOpenAPI3Document(document: OpenAPI3Document) {
const program = transform(document);
const content = generateMain(program);
const context = createContext(document);
const program = transform(context);
const content = generateMain(program, context);
try {
return await formatTypeSpec(content, {
printWidth: 100,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { TypeSpecProgram } from "../interfaces.js";
import { Context } from "../utils/context.js";
import { generateDataType } from "./generate-model.js";
import { generateNamespace } from "./generate-namespace.js";
import { generateOperation } from "./generate-operation.js";
import { generateServiceInformation } from "./generate-service-info.js";

export function generateMain(program: TypeSpecProgram): string {
export function generateMain(program: TypeSpecProgram, context: Context): string {
return `
import "@typespec/http";
import "@typespec/openapi";
Expand All @@ -15,12 +16,12 @@ export function generateMain(program: TypeSpecProgram): string {

${generateServiceInformation(program.serviceInfo)}

${program.types.map(generateDataType).join("\n\n")}
${program.types.map((t) => generateDataType(t, context)).join("\n\n")}

${program.operations.map(generateOperation).join("\n\n")}
${program.operations.map((o) => generateOperation(o, context)).join("\n\n")}

${Object.entries(program.namespaces)
.map(([name, namespace]) => generateNamespace(name, namespace))
.map(([name, namespace]) => generateNamespace(name, namespace, context))
.join("\n\n")}
`;
}
Original file line number Diff line number Diff line change
@@ -1,43 +1,38 @@
import { OpenAPI3Schema, Refable } from "../../../../types.js";
import {
TypeSpecAlias,
TypeSpecDataTypes,
TypeSpecEnum,
TypeSpecModel,
TypeSpecModelProperty,
TypeSpecScalar,
TypeSpecUnion,
} from "../interfaces.js";
import { Context } from "../utils/context.js";
import { getDecoratorsForSchema } from "../utils/decorators.js";
import { generateDocs } from "../utils/docs.js";
import { generateDecorators } from "./generate-decorators.js";
import {
generateTypeFromSchema,
getRefScopeAndName,
getTypeSpecPrimitiveFromSchema,
} from "./generate-types.js";
import { getTypeSpecPrimitiveFromSchema } from "./generate-types.js";

export function generateDataType(type: TypeSpecDataTypes): string {
export function generateDataType(type: TypeSpecDataTypes, context: Context): string {
switch (type.kind) {
case "alias":
return generateAlias(type);
return generateAlias(type, context);
case "enum":
return generateEnum(type);
case "model":
return generateModel(type);
return generateModel(type, context);
case "scalar":
return generateScalar(type);
return generateScalar(type, context);
case "union":
return generateUnion(type);
return generateUnion(type, context);
}
}

function generateAlias(alias: TypeSpecAlias): string {
function generateAlias(alias: TypeSpecAlias, context: Context): string {
// Since aliases are not represented in the TypeGraph,
// generate a model so that the model name is present in emitted OpenAPI3.
// May revisit to allow emitting actual alias.
const { scope, name } = getRefScopeAndName(alias.ref);
return `model ${alias.name} is ${[...scope, name].join(".")};`;
const sourceModel = context.getRefName(alias.ref, alias.scope);
return `model ${alias.name} is ${sourceModel};`;
}

function generateEnum(tsEnum: TypeSpecEnum): string {
Expand All @@ -61,22 +56,22 @@ function generateEnum(tsEnum: TypeSpecEnum): string {
return definitions.join("\n");
}

function generateScalar(scalar: TypeSpecScalar): string {
function generateScalar(scalar: TypeSpecScalar, context: Context): string {
const definitions: string[] = [];

if (scalar.doc) {
definitions.push(generateDocs(scalar.doc));
}

definitions.push(...generateDecorators(scalar.decorators));
const type = generateTypeFromSchema(scalar.schema);
const type = context.generateTypeFromRefableSchema(scalar.schema, scalar.scope);

definitions.push(`scalar ${scalar.name} extends ${type};`);

return definitions.join("\n");
}

function generateUnion(union: TypeSpecUnion): string {
function generateUnion(union: TypeSpecUnion, context: Context): string {
const definitions: string[] = [];

if (union.doc) {
Expand All @@ -92,9 +87,13 @@ function generateUnion(union: TypeSpecUnion): string {
if (schema.enum) {
definitions.push(...schema.enum.map((e) => `${JSON.stringify(e)},`));
} else if (schema.oneOf) {
definitions.push(...schema.oneOf.map(generateUnionMember));
definitions.push(
...schema.oneOf.map((member) => context.generateTypeFromRefableSchema(member, union.scope))
);
} else if (schema.anyOf) {
definitions.push(...schema.anyOf.map(generateUnionMember));
definitions.push(
...schema.anyOf.map((member) => context.generateTypeFromRefableSchema(member, union.scope))
);
} else {
// check if it's a primitive type
const primitiveType = getTypeSpecPrimitiveFromSchema(schema);
Expand All @@ -112,11 +111,7 @@ function generateUnion(union: TypeSpecUnion): string {
return definitions.join("\n");
}

function generateUnionMember(member: Refable<OpenAPI3Schema>): string {
return `${generateTypeFromSchema(member)},`;
}

export function generateModel(model: TypeSpecModel): string {
function generateModel(model: TypeSpecModel, context: Context): string {
const definitions: string[] = [];
const modelDeclaration = generateModelDeclaration(model);

Expand All @@ -127,10 +122,25 @@ export function generateModel(model: TypeSpecModel): string {
definitions.push(...generateDecorators(model.decorators));
definitions.push(modelDeclaration.open);

definitions.push(...model.properties.map(generateModelProperty));
definitions.push(
...model.properties.map((prop) => {
// Decorators will be a combination of top-level (parameters) and
// schema-level decorators.
const decorators = generateDecorators([
...prop.decorators,
...getDecoratorsForSchema(prop.schema),
]).join(" ");

const doc = prop.doc ? generateDocs(prop.doc) : "";

return `${doc}${decorators} ${prop.name}${prop.isOptional ? "?" : ""}: ${context.generateTypeFromRefableSchema(prop.schema, model.scope)};`;
})
);

if (model.additionalProperties) {
definitions.push(`...Record<${generateTypeFromSchema(model.additionalProperties)}>;`);
definitions.push(
`...Record<${context.generateTypeFromRefableSchema(model.additionalProperties, model.scope)}>;`
);
}

if (modelDeclaration.close) definitions.push(modelDeclaration.close);
Expand Down Expand Up @@ -158,16 +168,3 @@ function generateModelDeclaration(model: TypeSpecModel): ModelDeclarationOutput

return { open: `model ${modelName} {`, close: "}" };
}

function generateModelProperty(property: TypeSpecModelProperty): string {
// Decorators will be a combination of top-level (parameters) and
// schema-level decorators.
const decorators = generateDecorators([
...property.decorators,
...getDecoratorsForSchema(property.schema),
]).join(" ");

const doc = property.doc ? generateDocs(property.doc) : "";

return `${doc}${decorators} ${property.name}${property.isOptional ? "?" : ""}: ${generateTypeFromSchema(property.schema)};`;
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { TypeSpecNamespace } from "../interfaces.js";
import { Context } from "../utils/context.js";
import { generateDataType } from "./generate-model.js";
import { generateOperation } from "./generate-operation.js";

export function generateNamespace(name: string, namespace: TypeSpecNamespace): string {
export function generateNamespace(
name: string,
namespace: TypeSpecNamespace,
context: Context
): string {
const definitions: string[] = [];
definitions.push(`namespace ${name} {`);

definitions.push(...namespace.types.map(generateDataType));
definitions.push(...namespace.operations.map(generateOperation));
definitions.push(...namespace.types.map((t) => generateDataType(t, context)));
definitions.push(...namespace.operations.map((o) => generateOperation(o, context)));

for (const [namespaceName, nestedNamespace] of Object.entries(namespace.namespaces)) {
definitions.push(generateNamespace(namespaceName, nestedNamespace));
definitions.push(generateNamespace(namespaceName, nestedNamespace, context));
}

definitions.push("}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import {
TypeSpecOperationParameter,
TypeSpecRequestBody,
} from "../interfaces.js";
import { Context } from "../utils/context.js";
import { generateDocs } from "../utils/docs.js";
import { generateDecorators } from "./generate-decorators.js";
import { generateTypeFromSchema, getRefName } from "./generate-types.js";

export function generateOperation(operation: TypeSpecOperation): string {
export function generateOperation(operation: TypeSpecOperation, context: Context): string {
const definitions: string[] = [];

if (operation.doc) {
Expand All @@ -21,8 +21,8 @@ export function generateOperation(operation: TypeSpecOperation): string {

// generate parameters
const parameters: string[] = [
...operation.parameters.map(generateOperationParameter),
...generateRequestBodyParameters(operation.requestBodies),
...operation.parameters.map((p) => generateOperationParameter(operation, p, context)),
...generateRequestBodyParameters(operation.requestBodies, context),
];

const responseTypes = operation.responseTypes.length
Expand All @@ -34,14 +34,13 @@ export function generateOperation(operation: TypeSpecOperation): string {
return definitions.join(" ");
}

function generateOperationParameter(parameter: Refable<TypeSpecOperationParameter>) {
function generateOperationParameter(
operation: TypeSpecOperation,
parameter: Refable<TypeSpecOperationParameter>,
context: Context
) {
if ("$ref" in parameter) {
// check if referencing a model or a property
const refName = getRefName(parameter.$ref);
const paramName = refName.indexOf(".") >= 0 ? refName.split(".").pop() : refName;
// when refName and paramName match, we're referencing a model and can spread
// TODO: Handle optionality
return refName === paramName ? `...${refName}` : `${paramName}: ${refName}`;
return `...${context.getRefName(parameter.$ref, operation.scope)}`;
}

const definitions: string[] = [];
Expand All @@ -53,13 +52,16 @@ function generateOperationParameter(parameter: Refable<TypeSpecOperationParamete
definitions.push(...generateDecorators(parameter.decorators));

definitions.push(
`${parameter.name}${parameter.isOptional ? "?" : ""}: ${generateTypeFromSchema(parameter.schema)}`
`${parameter.name}${parameter.isOptional ? "?" : ""}: ${context.generateTypeFromRefableSchema(parameter.schema, operation.scope)}`
);

return definitions.join(" ");
}

function generateRequestBodyParameters(requestBodies: TypeSpecRequestBody[]): string[] {
function generateRequestBodyParameters(
requestBodies: TypeSpecRequestBody[],
context: Context
): string[] {
if (!requestBodies.length) {
return [];
}
Expand All @@ -74,7 +76,11 @@ function generateRequestBodyParameters(requestBodies: TypeSpecRequestBody[]): st

// Get the set of referenced types
const body = Array.from(
new Set(requestBodies.filter((r) => !!r.schema).map((r) => generateTypeFromSchema(r.schema!)))
new Set(
requestBodies
.filter((r) => !!r.schema)
.map((r) => context.generateTypeFromRefableSchema(r.schema!, []))
)
).join(" | ");

if (body) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TypeSpecServiceInfo } from "../interfaces.js";
import { generateDocs } from "../utils/docs.js";
import { generateNamespaceName } from "../utils/generate-namespace-name.js";

export function generateServiceInformation(serviceInfo: TypeSpecServiceInfo): string {
const definitions: string[] = [];
Expand All @@ -21,7 +22,3 @@ export function generateServiceInformation(serviceInfo: TypeSpecServiceInfo): st

return definitions.join("\n");
}

function generateNamespaceName(name: string): string {
return name.replaceAll(/[^\w^\d_]+/g, "");
}
Loading
Loading