diff --git a/.chronus/changes/tsp-openapi3-namespacing-2024-6-15-22-37-45.md b/.chronus/changes/tsp-openapi3-namespacing-2024-6-15-22-37-45.md new file mode 100644 index 0000000000..6d1cca42b0 --- /dev/null +++ b/.chronus/changes/tsp-openapi3-namespacing-2024-6-15-22-37-45.md @@ -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. \ No newline at end of file diff --git a/packages/openapi3/src/cli/actions/convert/generators/generate-main.ts b/packages/openapi3/src/cli/actions/convert/generators/generate-main.ts index 8c7e93e05b..27169dabbc 100644 --- a/packages/openapi3/src/cli/actions/convert/generators/generate-main.ts +++ b/packages/openapi3/src/cli/actions/convert/generators/generate-main.ts @@ -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"; @@ -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")} `; } diff --git a/packages/openapi3/src/cli/actions/convert/generators/generate-namespace.ts b/packages/openapi3/src/cli/actions/convert/generators/generate-namespace.ts new file mode 100644 index 0000000000..9df6673062 --- /dev/null +++ b/packages/openapi3/src/cli/actions/convert/generators/generate-namespace.ts @@ -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"); +} diff --git a/packages/openapi3/src/cli/actions/convert/generators/generate-operation.ts b/packages/openapi3/src/cli/actions/convert/generators/generate-operation.ts index 0adf8b4030..7686c56950 100644 --- a/packages/openapi3/src/cli/actions/convert/generators/generate-operation.ts +++ b/packages/openapi3/src/cli/actions/convert/generators/generate-operation.ts @@ -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"; @@ -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(" "); } @@ -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 -): 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) - ); -} diff --git a/packages/openapi3/src/cli/actions/convert/generators/generate-types.ts b/packages/openapi3/src/cli/actions/convert/generators/generate-types.ts index bcbc8cfcc5..1060ebdf63 100644 --- a/packages/openapi3/src/cli/actions/convert/generators/generate-types.ts +++ b/packages/openapi3/src/cli/actions/convert/generators/generate-types.ts @@ -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"; @@ -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 { diff --git a/packages/openapi3/src/cli/actions/convert/interfaces.ts b/packages/openapi3/src/cli/actions/convert/interfaces.ts index 5afd6f0244..7ff4f33b86 100644 --- a/packages/openapi3/src/cli/actions/convert/interfaces.ts +++ b/packages/openapi3/src/cli/actions/convert/interfaces.ts @@ -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; models: TypeSpecModel[]; augmentations: TypeSpecAugmentation[]; operations: TypeSpecOperation[]; } +export interface TypeSpecDeclaration { + name: string; + doc?: string; + scope: string[]; +} + +export interface TypeSpecNamespace { + namespaces: Record; + models: TypeSpecModel[]; + operations: TypeSpecOperation[]; +} + export interface TypeSpecServiceInfo { name: string; doc?: string; @@ -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; @@ -60,14 +71,14 @@ export interface TypeSpecModelProperty { schema: Refable; } -export interface TypeSpecOperation { +export interface TypeSpecOperation extends TypeSpecDeclaration { name: string; doc?: string; decorators: TypeSpecDecorator[]; operationId?: string; parameters: Refable[]; requestBodies: TypeSpecRequestBody[]; - responses: OpenAPI3Responses; + responseTypes: string[]; tags: string[]; } diff --git a/packages/openapi3/src/cli/actions/convert/transforms/transform-component-parameters.ts b/packages/openapi3/src/cli/actions/convert/transforms/transform-component-parameters.ts index 2b607ad3b3..e51a150639 100644 --- a/packages/openapi3/src/cli/actions/convert/transforms/transform-component-parameters.ts +++ b/packages/openapi3/src/cli/actions/convert/transforms/transform-component-parameters.ts @@ -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. @@ -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), diff --git a/packages/openapi3/src/cli/actions/convert/transforms/transform-component-schemas.ts b/packages/openapi3/src/cli/actions/convert/transforms/transform-component-schemas.ts index cd249486ad..9cc114f6fb 100644 --- a/packages/openapi3/src/cli/actions/convert/transforms/transform-component-schemas.ts +++ b/packages/openapi3/src/cli/actions/convert/transforms/transform-component-schemas.ts @@ -1,3 +1,4 @@ +import { printIdentifier } from "@typespec/compiler"; import { OpenAPI3Components, OpenAPI3Schema } from "../../../../types.js"; import { getArrayType, @@ -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. @@ -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": @@ -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), diff --git a/packages/openapi3/src/cli/actions/convert/transforms/transform-namespaces.ts b/packages/openapi3/src/cli/actions/convert/transforms/transform-namespaces.ts new file mode 100644 index 0000000000..411f0a8431 --- /dev/null +++ b/packages/openapi3/src/cli/actions/convert/transforms/transform-namespaces.ts @@ -0,0 +1,77 @@ +import { + TypeSpecModel, + TypeSpecNamespace, + TypeSpecOperation, + TypeSpecProgram, +} from "../interfaces.js"; + +type TypeSpecProgramDeclarations = Pick; +export function transformNamespaces( + models: TypeSpecModel[], + operations: TypeSpecOperation[] +): TypeSpecProgramDeclarations { + // There can only be 1 file namespace - so if scopes is empty then entity belongs at root level + const programDecs: TypeSpecProgramDeclarations = { + models: [], + operations: [], + namespaces: {}, + }; + + expandModels(programDecs, models); + expandOperations(programDecs, operations); + + return programDecs; +} + +function expandModels(programDecs: TypeSpecProgramDeclarations, models: TypeSpecModel[]): void { + for (const model of models) { + const { scope } = model; + const namespace = getNamespace(programDecs, scope) ?? createNamespace(programDecs, scope); + namespace.models.push(model); + } +} + +function expandOperations( + programDecs: TypeSpecProgramDeclarations, + operations: TypeSpecOperation[] +): void { + for (const operation of operations) { + const { scope } = operation; + const namespace = getNamespace(programDecs, scope) ?? createNamespace(programDecs, scope); + namespace.operations.push(operation); + } +} + +function getNamespace( + programDecs: TypeSpecProgramDeclarations, + scope: string[] +): TypeSpecNamespace | undefined { + if (!scope.length) return programDecs; + + let namespace: TypeSpecNamespace = programDecs; + for (const fragment of scope) { + if (!namespace) return; + namespace = namespace.namespaces[fragment]; + } + + return namespace; +} + +function createNamespace( + programDecs: TypeSpecProgramDeclarations, + scope: string[] +): TypeSpecNamespace { + let namespace: TypeSpecNamespace = programDecs; + for (const fragment of scope) { + if (!namespace.namespaces[fragment]) { + namespace.namespaces[fragment] = { + namespaces: {}, + models: [], + operations: [], + }; + } + namespace = namespace.namespaces[fragment]; + } + + return namespace; +} diff --git a/packages/openapi3/src/cli/actions/convert/transforms/transform-operation-responses.ts b/packages/openapi3/src/cli/actions/convert/transforms/transform-operation-responses.ts index 30944fbebc..c7479312d1 100644 --- a/packages/openapi3/src/cli/actions/convert/transforms/transform-operation-responses.ts +++ b/packages/openapi3/src/cli/actions/convert/transforms/transform-operation-responses.ts @@ -1,5 +1,5 @@ +import { printIdentifier } from "@typespec/compiler"; import { - OpenAPI3Document, OpenAPI3Header, OpenAPI3Responses, OpenAPI3Schema, @@ -9,54 +9,19 @@ import { import { TypeSpecDecorator, TypeSpecModel, TypeSpecModelProperty } from "../interfaces.js"; import { convertHeaderName } from "../utils/convert-header-name.js"; import { getDecoratorsForSchema, getExtensions } from "../utils/decorators.js"; -import { supportedHttpMethods } from "../utils/supported-http-methods.js"; - -type OperationResponseInfo = { - operationId: string; - operationResponses: OpenAPI3Responses; -}; +import { getScopeAndName } from "../utils/get-scope-and-name.js"; /** * Transforms #/paths/{route}/{httpMethod}/responses into TypeSpec models. - * Populates the provided `models` array in-place. - * @param models - * @param document + * @param operationId - Used to generate model names with scopes. + * @param operationResponses - The responses object for an operation. */ -export function transformAllOperationResponses( - models: TypeSpecModel[], - document: OpenAPI3Document -): void { - const allOperationResponses = collectOpenAPI3OperationResponses(document); - for (const { operationId, operationResponses } of allOperationResponses) { - transformOperationResponses(models, operationId, operationResponses); - } -} - -function collectOpenAPI3OperationResponses(document: OpenAPI3Document): OperationResponseInfo[] { - const allOperationResponses: OperationResponseInfo[] = []; - const paths = document.paths ?? {}; - for (const route of Object.keys(paths)) { - const path = paths[route]; - for (const verb of supportedHttpMethods) { - const operation = path[verb]; - if (!operation) continue; - - const operationResponses: OpenAPI3Responses = operation.responses ?? {}; - allOperationResponses.push({ - operationId: operation.operationId!, - operationResponses, - }); - } - } - - return allOperationResponses; -} - -function transformOperationResponses( - models: TypeSpecModel[], +export function collectOperationResponses( operationId: string, operationResponses: OpenAPI3Responses -) { +): TypeSpecModel[] { + const models: TypeSpecModel[] = []; + const rootDecorators: TypeSpecDecorator[] = getExtensions(operationResponses); for (const statusCode of Object.keys(operationResponses)) { const response = operationResponses[statusCode]; @@ -64,7 +29,7 @@ function transformOperationResponses( if ("$ref" in response) { //TODO: Support for referencing #/components/responseBodies - return; + continue; } // These headers will be applied to all of the models for this operation/statusCode @@ -86,10 +51,13 @@ function transformOperationResponses( decorators.push({ name: "error", args: [] }); } + const scopeAndName = getScopeAndName(operationId!); + if (!response.content) { // This is common when there is no actual request body, just a statusCode, e.g. for errors models.push({ - name: generateResponseModelName(operationId, statusCode), + scope: scopeAndName.scope, + name: generateResponseModelName(scopeAndName.rawName, statusCode), decorators, properties: commonProperties, doc: response.description, @@ -121,7 +89,8 @@ function transformOperationResponses( } models.push({ - name: generateResponseModelName(operationId, statusCode, contentType), + scope: scopeAndName.scope, + name: generateResponseModelName(scopeAndName.rawName, statusCode, contentType), decorators, properties, doc: response.description, @@ -129,6 +98,8 @@ function transformOperationResponses( } } } + + return models; } function convertHeaderToProperty( @@ -216,7 +187,7 @@ export function generateResponseModelName( if (contentType) { modelName += convertContentType(contentType); } - return modelName + "Response"; + return printIdentifier(modelName + "Response"); } function convertContentType(contentType: string): string { diff --git a/packages/openapi3/src/cli/actions/convert/transforms/transform-paths.ts b/packages/openapi3/src/cli/actions/convert/transforms/transform-paths.ts index 72375ae097..f56609a7d9 100644 --- a/packages/openapi3/src/cli/actions/convert/transforms/transform-paths.ts +++ b/packages/openapi3/src/cli/actions/convert/transforms/transform-paths.ts @@ -1,3 +1,4 @@ +import { printIdentifier } from "@typespec/compiler"; import { OpenAPI3Parameter, OpenAPI3PathItem, @@ -5,19 +6,26 @@ import { Refable, } from "../../../../types.js"; import { + TypeSpecModel, TypeSpecOperation, TypeSpecOperationParameter, TypeSpecRequestBody, } from "../interfaces.js"; import { getExtensions, getParameterDecorators } from "../utils/decorators.js"; +import { getScopeAndName } from "../utils/get-scope-and-name.js"; import { supportedHttpMethods } from "../utils/supported-http-methods.js"; +import { collectOperationResponses } from "./transform-operation-responses.js"; /** * Transforms each operation defined under #/paths/{route}/{httpMethod} into a TypeSpec operation. + * @params models - The array of models to populate with any new models generated from the operation. * @param paths * @returns */ -export function transformPaths(paths: Record): TypeSpecOperation[] { +export function transformPaths( + models: TypeSpecModel[], + paths: Record +): TypeSpecOperation[] { const operations: TypeSpecOperation[] = []; for (const route of Object.keys(paths)) { @@ -29,8 +37,12 @@ export function transformPaths(paths: Record): TypeSpe const parameters = operation.parameters?.map(transformOperationParameter) ?? []; const tags = operation.tags?.map((t) => t) ?? []; + const operationResponses = operation.responses ?? {}; + const responseModels = collectOperationResponses(operation.operationId!, operationResponses); + models.push(...responseModels); + operations.push({ - name: operation.operationId!, + ...getScopeAndName(operation.operationId!), decorators: [ ...getExtensions(operation), { name: "route", args: [route] }, @@ -40,7 +52,7 @@ export function transformPaths(paths: Record): TypeSpe doc: operation.description, operationId: operation.operationId, requestBodies: transformRequestBodies(operation.requestBody), - responses: operation.responses ?? {}, + responseTypes: responseModels.map((m) => m.name), tags: tags, }); } @@ -57,7 +69,7 @@ function transformOperationParameter( } return { - name: parameter.name, + name: printIdentifier(parameter.name), doc: parameter.description, decorators: getParameterDecorators(parameter), isOptional: !parameter.required, diff --git a/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts b/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts index 2bbdda34cf..ba60aae37c 100644 --- a/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts +++ b/packages/openapi3/src/cli/actions/convert/transforms/transforms.ts @@ -2,18 +2,18 @@ import { OpenAPI3Document } from "../../../../types.js"; import { TypeSpecModel, TypeSpecProgram } from "../interfaces.js"; import { transformComponentParameters } from "./transform-component-parameters.js"; import { transformComponentSchemas } from "./transform-component-schemas.js"; -import { transformAllOperationResponses } from "./transform-operation-responses.js"; +import { transformNamespaces } from "./transform-namespaces.js"; import { transformPaths } from "./transform-paths.js"; import { transformServiceInfo } from "./transform-service-info.js"; export function transform(openapi: OpenAPI3Document): TypeSpecProgram { const models = collectModels(openapi); + const operations = transformPaths(models, openapi.paths); return { serviceInfo: transformServiceInfo(openapi.info), - models, + ...transformNamespaces(models, operations), augmentations: [], - operations: transformPaths(openapi.paths), }; } @@ -24,8 +24,6 @@ function collectModels(document: OpenAPI3Document): TypeSpecModel[] { transformComponentSchemas(models, components?.schemas); // get models from `#/components/parameters transformComponentParameters(models, components?.parameters); - // get models from #/paths/{route}/{httpMethod}/responses - transformAllOperationResponses(models, document); return models; } diff --git a/packages/openapi3/src/cli/actions/convert/utils/get-scope-and-name.ts b/packages/openapi3/src/cli/actions/convert/utils/get-scope-and-name.ts new file mode 100644 index 0000000000..b0bdf1c9f2 --- /dev/null +++ b/packages/openapi3/src/cli/actions/convert/utils/get-scope-and-name.ts @@ -0,0 +1,14 @@ +import { printIdentifier } from "@typespec/compiler"; + +type ScopeAndName = { scope: string[]; name: string; rawName: string }; +export function getScopeAndName(originalName: string): ScopeAndName { + const path = originalName.split("."); + const name = path.pop()!; + + return { scope: path.map(printIdentifier), name: printIdentifier(name), rawName: name }; +} + +export function scopesMatch(a: string[], b: string[]): boolean { + if (a.length !== b.length) return false; + return a.every((scope, i) => scope === b[i]); +} diff --git a/packages/openapi3/test/tsp-openapi3/output/escaped-identifiers/main.tsp b/packages/openapi3/test/tsp-openapi3/output/escaped-identifiers/main.tsp new file mode 100644 index 0000000000..fb55c6c963 --- /dev/null +++ b/packages/openapi3/test/tsp-openapi3/output/escaped-identifiers/main.tsp @@ -0,0 +1,33 @@ +import "@typespec/http"; +import "@typespec/openapi"; +import "@typespec/openapi3"; + +using Http; +using OpenAPI; + +@service({ + title: "Sample API", +}) +@info({ + version: "1.0.0", +}) +namespace SampleAPI; + +scalar `Foo-Bar` extends string; + +model `Escaped-Model` { + id: string; + @path `escaped-property`: string; +} + +/** + * Success + */ +@defaultResponse +model `get-thingDefaultResponse` {} + +@route("/{escaped-property}") @get op `get-thing`( + @query `weird@param`?: `Foo-Bar`, + `escaped-property`: `Escaped-Model`.`escaped-property`, + @bodyRoot body: `Escaped-Model`, +): `get-thingDefaultResponse`; diff --git a/packages/openapi3/test/tsp-openapi3/output/nested/main.tsp b/packages/openapi3/test/tsp-openapi3/output/nested/main.tsp new file mode 100644 index 0000000000..bf804521a4 --- /dev/null +++ b/packages/openapi3/test/tsp-openapi3/output/nested/main.tsp @@ -0,0 +1,67 @@ +import "@typespec/http"; +import "@typespec/openapi"; +import "@typespec/openapi3"; + +using Http; +using OpenAPI; + +@service({ + title: "Nested sample", +}) +@info({ + version: "0.0.0", +}) +namespace Nestedsample; + +namespace SubA { + model Thing { + id: int64; + } + namespace SubSubA { + model Thing { + name: string; + } + /** + * The request has succeeded. + */ + model doSomething200ApplicationJsonResponse { + @statusCode statusCode: 200; + @bodyRoot body: string; + } + @route("/sub/a/subsub") @post op doSomething( + @bodyRoot body: SubA.SubSubA.Thing, + ): doSomething200ApplicationJsonResponse; + } +} + +namespace SubB { + model Thing { + id: int64; + } + /** + * The request has succeeded. + */ + model doSomething200ApplicationJsonResponse { + @statusCode statusCode: 200; + @bodyRoot body: string; + } + @route("/sub/b") @post op doSomething( + @bodyRoot body: SubB.Thing, + ): doSomething200ApplicationJsonResponse; +} + +namespace SubC { + /** + * The request has succeeded. + */ + model anotherOp200ApplicationJsonResponse { + @statusCode statusCode: 200; + @bodyRoot body: string; + } + @route("/") @post op anotherOp( + @bodyRoot body: { + thing: SubA.Thing; + thing2: SubA.Thing; + }, + ): anotherOp200ApplicationJsonResponse; +} diff --git a/packages/openapi3/test/tsp-openapi3/specs/escaped-identifiers/service.json b/packages/openapi3/test/tsp-openapi3/specs/escaped-identifiers/service.json new file mode 100644 index 0000000000..e5c1849828 --- /dev/null +++ b/packages/openapi3/test/tsp-openapi3/specs/escaped-identifiers/service.json @@ -0,0 +1,72 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Sample API", + "version": "1.0.0" + }, + "tags": [], + "paths": { + "/{escaped-property}": { + "get": { + "operationId": "get-thing", + "parameters": [ + { + "name": "weird@param", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/Foo-Bar" + } + }, + { + "$ref": "#/components/parameters/Escaped-Model.escaped-property" + } + ], + "responses": { + "default": { + "description": "Success" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Escaped-Model" + } + } + } + } + } + } + }, + "components": { + "parameters": { + "Escaped-Model.escaped-property": { + "name": "escaped-property", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + }, + "schemas": { + "Foo-Bar": { + "type": "string" + }, + "Escaped-Model": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "string" + }, + "escaped-property": { + "type": "string" + } + } + } + } + } +} diff --git a/packages/openapi3/test/tsp-openapi3/specs/nested/service.yml b/packages/openapi3/test/tsp-openapi3/specs/nested/service.yml new file mode 100644 index 0000000000..fc3775a7c0 --- /dev/null +++ b/packages/openapi3/test/tsp-openapi3/specs/nested/service.yml @@ -0,0 +1,90 @@ +openapi: 3.0.0 +info: + title: Nested sample + version: 0.0.0 +tags: [] +paths: + /: + post: + operationId: SubC.anotherOp + parameters: [] + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + thing: + $ref: "#/components/schemas/SubA.Thing" + thing2: + $ref: "#/components/schemas/SubA.Thing" + required: + - thing + - thing2 + /sub/a/subsub: + post: + operationId: SubA.SubSubA.doSomething + parameters: [] + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SubA.SubSubA.Thing" + /sub/b: + post: + operationId: SubB.doSomething + parameters: [] + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SubB.Thing" +components: + schemas: + SubA.SubSubA.Thing: + type: object + required: + - name + properties: + name: + type: string + SubA.Thing: + type: object + required: + - id + properties: + id: + type: integer + format: int64 + SubB.Thing: + type: object + required: + - id + properties: + id: + type: integer + format: int64