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 - update to group items by namespace when separated by dot, and escaped otherwise invalid identifiers #3844

Merged
merged 5 commits into from
Jul 16, 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: feature
packages:
- "@typespec/openapi3"
---

Updates tsp-openapi3 to escape identifiers that would otherwise be invalid, and automatically resolve namespaces for schemas with dots in their names.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TypeSpecProgram } from "../interfaces.js";
import { generateModel } from "./generate-model.js";
import { generateNamespace } from "./generate-namespace.js";
import { generateOperation } from "./generate-operation.js";
import { generateServiceInformation } from "./generate-service-info.js";

Expand All @@ -17,5 +18,9 @@ export function generateMain(program: TypeSpecProgram): string {
${program.models.map(generateModel).join("\n\n")}

${program.operations.map(generateOperation).join("\n\n")}

${Object.entries(program.namespaces)
.map(([name, namespace]) => generateNamespace(name, namespace))
.join("\n\n")}
`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { TypeSpecNamespace } from "../interfaces.js";
import { generateModel } from "./generate-model.js";
import { generateOperation } from "./generate-operation.js";

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

definitions.push(...namespace.models.map(generateModel));
definitions.push(...namespace.operations.map(generateOperation));

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

definitions.push("}");
return definitions.join("\n");
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { OpenAPI3Response, Refable } from "../../../../types.js";
import { Refable } from "../../../../types.js";
import {
TypeSpecOperation,
TypeSpecOperationParameter,
TypeSpecRequestBody,
} from "../interfaces.js";
import { generateResponseModelName } from "../transforms/transform-operation-responses.js";
import { generateDocs } from "../utils/docs.js";
import { generateDecorators } from "./generate-decorators.js";
import { generateTypeFromSchema, getRefName } from "./generate-types.js";
Expand All @@ -26,9 +25,11 @@ export function generateOperation(operation: TypeSpecOperation): string {
...generateRequestBodyParameters(operation.requestBodies),
];

const responseTypes = generateResponses(operation.operationId!, operation.responses);
const responseTypes = operation.responseTypes.length
? operation.responseTypes.join(" | ")
: "void";

definitions.push(`op ${operation.name}(${parameters.join(", ")}): ${responseTypes.join(" | ")};`);
definitions.push(`op ${operation.name}(${parameters.join(", ")}): ${responseTypes};`);

return definitions.join(" ");
}
Expand Down Expand Up @@ -86,39 +87,3 @@ function generateRequestBodyParameters(requestBodies: TypeSpecRequestBody[]): st
function supportsOnlyJson(contentTypes: string[]) {
return contentTypes.length === 1 && contentTypes[0] === "application/json";
}

function generateResponses(
operationId: string,
responses: TypeSpecOperation["responses"]
): string[] {
if (!responses) {
return ["void"];
}

const definitions: string[] = [];

for (const statusCode of Object.keys(responses)) {
const response = responses[statusCode];
definitions.push(...generateResponseForStatus(operationId, statusCode, response));
}

return definitions;
}

function generateResponseForStatus(
operationId: string,
statusCode: string,
response: Refable<OpenAPI3Response>
): string[] {
if ("$ref" in response) {
return [getRefName(response.$ref)];
}

if (!response.content) {
return [generateResponseModelName(operationId, statusCode)];
}

return Object.keys(response.content).map((contentType) =>
generateResponseModelName(operationId, statusCode, contentType)
);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { printIdentifier } from "@typespec/compiler";
import { OpenAPI3Schema, Refable } from "../../../../types.js";
import { getDecoratorsForSchema } from "../utils/decorators.js";
import { generateDecorators } from "./generate-decorators.js";
Expand Down Expand Up @@ -47,8 +48,7 @@ function getTypeFromSchema(schema: OpenAPI3Schema): string {

export function getRefName(ref: string): string {
const name = ref.split("/").pop() ?? "";
// TODO: account for `.` in the name
return name;
return name.split(".").map(printIdentifier).join(".");
}

function getAnyOfType(schema: OpenAPI3Schema): string {
Expand Down
23 changes: 17 additions & 6 deletions packages/openapi3/src/cli/actions/convert/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { Contact, License } from "@typespec/openapi";
import { OpenAPI3Encoding, OpenAPI3Responses, OpenAPI3Schema, Refable } from "../../../types.js";
import { OpenAPI3Encoding, OpenAPI3Schema, Refable } from "../../../types.js";

export interface TypeSpecProgram {
serviceInfo: TypeSpecServiceInfo;
namespaces: Record<string, TypeSpecNamespace>;
models: TypeSpecModel[];
augmentations: TypeSpecAugmentation[];
operations: TypeSpecOperation[];
}

export interface TypeSpecDeclaration {
name: string;
doc?: string;
scope: string[];
}

export interface TypeSpecNamespace {
namespaces: Record<string, TypeSpecNamespace>;
models: TypeSpecModel[];
operations: TypeSpecOperation[];
}

export interface TypeSpecServiceInfo {
name: string;
doc?: string;
Expand All @@ -27,9 +40,7 @@ export interface TypeSpecAugmentation extends TypeSpecDecorator {
target: string;
}

export interface TypeSpecModel {
name: string;
doc?: string;
export interface TypeSpecModel extends TypeSpecDeclaration {
decorators: TypeSpecDecorator[];
properties: TypeSpecModelProperty[];
additionalProperties?: Refable<OpenAPI3Schema>;
Expand Down Expand Up @@ -60,14 +71,14 @@ export interface TypeSpecModelProperty {
schema: Refable<OpenAPI3Schema>;
}

export interface TypeSpecOperation {
export interface TypeSpecOperation extends TypeSpecDeclaration {
name: string;
doc?: string;
decorators: TypeSpecDecorator[];
operationId?: string;
parameters: Refable<TypeSpecOperationParameter>[];
requestBodies: TypeSpecRequestBody[];
responses: OpenAPI3Responses;
responseTypes: string[];
tags: string[];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { printIdentifier } from "@typespec/compiler";
import { OpenAPI3Components, OpenAPI3Parameter } from "../../../../types.js";
import { TypeSpecModel, TypeSpecModelProperty } from "../interfaces.js";
import { getParameterDecorators } from "../utils/decorators.js";
import { getScopeAndName, scopesMatch } from "../utils/get-scope-and-name.js";

/**
* Transforms #/components/parameters into TypeSpec models.
Expand All @@ -17,37 +19,46 @@ export function transformComponentParameters(
if (!parameters) return;

for (const name of Object.keys(parameters)) {
// Determine what the name of the parameter's model is since name may point at
// a nested property.
const modelName = name.indexOf(".") < 0 ? name : name.split(".").shift()!;
const parameter = parameters[name];
transformComponentParameter(models, name, parameter);
}
}

function transformComponentParameter(
models: TypeSpecModel[],
key: string,
parameter: OpenAPI3Parameter
): void {
const { name, scope } = getScopeAndName(key);
// Get the model name this parameter belongs to
const modelName = scope.length > 0 ? scope.pop()! : name;

// Check if model already exists; if not, create it
let model = models.find((m) => m.name === modelName);
if (!model) {
model = {
name: modelName,
decorators: [],
properties: [],
};
models.push(model);
}
// find a matching model, or create one if it doesn't exist
let model = models.find((m) => m.name === modelName && scopesMatch(m.scope, scope));
if (!model) {
model = {
scope,
name: modelName,
decorators: [],
properties: [],
};
models.push(model);
}

const parameter = parameters[name];
const modelParameter = getModelPropertyFromParameter(parameter);
const modelProperty = getModelPropertyFromParameter(parameter);

// Check if the model already has a property of the matching name
const propIndex = model.properties.findIndex((p) => p.name === modelParameter.name);
if (propIndex >= 0) {
model.properties[propIndex] = modelParameter;
} else {
model.properties.push(modelParameter);
}
// Check if the model already has a property of the matching name
const propIndex = model.properties.findIndex((p) => p.name === modelProperty.name);
if (propIndex >= 0) {
model.properties[propIndex] = modelProperty;
} else {
model.properties.push(modelProperty);
}
}

function getModelPropertyFromParameter(parameter: OpenAPI3Parameter): TypeSpecModelProperty {
return {
name: parameter.name,
name: printIdentifier(parameter.name),
isOptional: !parameter.required,
doc: parameter.description ?? parameter.schema.description,
decorators: getParameterDecorators(parameter),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { printIdentifier } from "@typespec/compiler";
import { OpenAPI3Components, OpenAPI3Schema } from "../../../../types.js";
import {
getArrayType,
Expand All @@ -8,6 +9,7 @@ import {
} from "../generators/generate-types.js";
import { TypeSpecModel, TypeSpecModelProperty } from "../interfaces.js";
import { getDecoratorsForSchema } from "../utils/decorators.js";
import { getScopeAndName } from "../utils/get-scope-and-name.js";

/**
* Transforms #/components/schemas into TypeSpec models.
Expand All @@ -24,22 +26,30 @@ export function transformComponentSchemas(

for (const name of Object.keys(schemas)) {
const schema = schemas[name];
const extendsParent = getModelExtends(schema);
const isParent = getModelIs(schema);
models.push({
name: name.replace(/-/g, "_"),
decorators: [...getDecoratorsForSchema(schema)],
doc: schema.description,
properties: getModelPropertiesFromObjectSchema(schema),
additionalProperties:
typeof schema.additionalProperties === "object" ? schema.additionalProperties : undefined,
extends: extendsParent,
is: isParent,
type: schema.type,
});
transformComponentSchema(models, name, schema);
}
}

function transformComponentSchema(
models: TypeSpecModel[],
name: string,
schema: OpenAPI3Schema
): void {
const extendsParent = getModelExtends(schema);
const isParent = getModelIs(schema);
models.push({
...getScopeAndName(name),
decorators: [...getDecoratorsForSchema(schema)],
doc: schema.description,
properties: getModelPropertiesFromObjectSchema(schema),
additionalProperties:
typeof schema.additionalProperties === "object" ? schema.additionalProperties : undefined,
extends: extendsParent,
is: isParent,
type: schema.type,
});
}

function getModelExtends(schema: OpenAPI3Schema): string | undefined {
switch (schema.type) {
case "boolean":
Expand Down Expand Up @@ -88,7 +98,7 @@ function getModelPropertiesFromObjectSchema({
const property = properties[name];

modelProperties.push({
name,
name: printIdentifier(name),
doc: property.description,
schema: property,
isOptional: !required.includes(name),
Expand Down
Loading
Loading