UNPKG

@typespec/http-server-csharp

Version:

TypeSpec service code generator for c-sharp

953 lines (939 loc) 55.4 kB
import { CodeTypeEmitter, StringBuilder, code, createAssetEmitter, } from "@typespec/asset-emitter"; import { getDoc, getNamespaceFullName, getService, isErrorModel, isNeverType, isNullType, isTemplateDeclaration, isVoidType, resolvePath, serializeValueAsJson, } from "@typespec/compiler"; import { createRekeyableMap } from "@typespec/compiler/utils"; import { getHeaderFieldName, getHttpOperation, getHttpPart, isHeader, isStatusCode, } from "@typespec/http"; import { getResourceOperation } from "@typespec/rest"; import { execFile } from "child_process"; import path from "path"; import { getEncodedNameAttribute } from "./attributes.js"; import { GeneratedFileHeader, GeneratedFileHeaderWithNullable, getSerializationSourceFiles, } from "./boilerplate.js"; import { getProjectDocs } from "./doc.js"; import { CSharpSourceType, CSharpType, NameCasingType, } from "./interfaces.js"; import { reportDiagnostic } from "./lib.js"; import { getProjectHelpers } from "./project.js"; import { getBusinessLogicImplementations, getScaffoldingHelpers, } from "./scaffolding.js"; import { getEnumType, getRecordType, isKnownReferenceType } from "./type-helpers.js"; import { CSharpOperationHelpers, HttpMetadata, ModelInfo, UnknownType, coalesceTypes, coalesceUnionTypes, ensureCSharpIdentifier, ensureCleanDirectory, formatComment, getBusinessLogicCallParameters, getBusinessLogicDeclParameters, getCSharpIdentifier, getCSharpStatusCode, getCSharpType, getCSharpTypeForIntrinsic, getCSharpTypeForScalar, getFreePort, getHttpDeclParameters, getModelAttributes, getModelDeclarationName, getModelInstantiationName, getOpenApiConfig, getOperationVerbDecorator, getStatusCode, isEmptyResponseModel, isValueType, } from "./utils.js"; export async function $onEmit(context) { let _unionCounter = 0; const controllers = new Map(); const NoResourceContext = "RPCOperations"; const doNotEmit = context.program.compilerOptions.dryRun || false; function getFileWriter(program) { return async (path) => !!(await program.host.stat(resolvePath(path)).catch((_) => false)); } class CSharpCodeEmitter extends CodeTypeEmitter { #metadateMap = new Map(); #generatedFileHeaderWithNullable = GeneratedFileHeaderWithNullable; #generatedFileHeader = GeneratedFileHeader; #sourceTypeKey = "sourceType"; #baseNamespace = undefined; #emitterOutputType = context.options["output-type"]; #emitMocks = context.options["emit-mocks"]; #useSwagger = context.options["use-swaggerui"] || false; #openapiPath = context.options["openapi-path"] || "openapi/openapi.yaml"; #mockRegistrations = new Map(); #opHelpers = new CSharpOperationHelpers(this.emitter); #fileExists = getFileWriter(this.emitter.getProgram()); arrayDeclaration(array, name, elementType) { return this.emitter.result.declaration(ensureCSharpIdentifier(this.emitter.getProgram(), array, name), code `${this.emitter.emitTypeReference(elementType)}[]`); } arrayLiteral(array, elementType) { return this.emitter.result.rawCode(code `${this.emitter.emitTypeReference(elementType)}[]`); } booleanLiteral(boolean) { return this.emitter.result.rawCode(code `${boolean.value === true ? "true" : "false"}`); } unionLiteral(union) { const csType = coalesceUnionTypes(this.emitter.getProgram(), union); return this.emitter.result.rawCode(csType ? csType.getTypeReference(this.emitter.getContext()?.scope) : "object"); } declarationName(declarationType) { switch (declarationType.kind) { case "Enum": if (!declarationType.name) return `Enum${_unionCounter++}`; return getCSharpIdentifier(declarationType.name, NameCasingType.Class); case "Interface": if (!declarationType.name) return `Interface${_unionCounter++}`; return getCSharpIdentifier(declarationType.name, NameCasingType.Class); case "Model": if (!declarationType.name) return getModelDeclarationName(this.emitter.getProgram(), declarationType, `${_unionCounter++}`); return getCSharpIdentifier(declarationType.name, NameCasingType.Class); case "Operation": return getCSharpIdentifier(declarationType.name, NameCasingType.Class); case "Union": if (!declarationType.name) return `Union${_unionCounter++}`; return getCSharpIdentifier(declarationType.name, NameCasingType.Class); case "Scalar": default: return getCSharpIdentifier(declarationType.name, NameCasingType.Variable); } } enumDeclaration(en, name) { if (getEnumType(en) === "double") return ""; const program = this.emitter.getProgram(); const enumName = ensureCSharpIdentifier(program, en, name); const namespace = this.emitter.getContext().namespace; const doc = getDoc(this.emitter.getProgram(), en); const attributes = getModelAttributes(program, en, enumName); this.#metadateMap.set(en, new CSharpType({ name: enumName, namespace: namespace })); return this.emitter.result.declaration(enumName, code `${this.#generatedFileHeader} ${this.#emitUsings()} namespace ${namespace} { ${doc ? `${formatComment(doc)}` : ""} ${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")} public enum ${enumName} { ${this.emitter.emitEnumMembers(en)} } } `); } enumDeclarationContext(en) { if (getEnumType(en) === "double") return this.emitter.getContext(); const enumName = ensureCSharpIdentifier(this.emitter.getProgram(), en, en.name); const enumFile = this.emitter.createSourceFile(`generated/models/${enumName}.cs`); enumFile.meta[this.#sourceTypeKey] = CSharpSourceType.Model; const enumNamespace = `${this.#getOrSetBaseNamespace(en)}.Models`; return this.#createEnumContext(enumNamespace, enumFile, enumName); } enumMembers(en) { const enumType = getEnumType(en); const result = new StringBuilder(); let i = 0; for (const [name, member] of en.members) { i++; const memberName = ensureCSharpIdentifier(this.emitter.getProgram(), member, name); this.#metadateMap.set(member, { name: memberName }); if (enumType === "string") { result.push(code ` [JsonStringEnumMemberName("${member.value ? member.value : name}")] ${ensureCSharpIdentifier(this.emitter.getProgram(), member, name)}`); if (i < en.members.size) result.pushLiteralSegment(",\n"); } else if (member.value !== undefined) { result.push(code ` ${ensureCSharpIdentifier(this.emitter.getProgram(), member, name)} = ${member.value.toString()}`); if (i < en.members.size) result.pushLiteralSegment(",\n"); } } return this.emitter.result.rawCode(result.reduce()); } intrinsic(intrinsic, name) { switch (intrinsic.name) { case "unknown": return this.emitter.result.rawCode(code `System.Text.Json.Nodes.JsonNode`); case "null": return this.emitter.result.rawCode(code `System.Text.Json.Nodes.JsonNode`); case "ErrorType": case "never": reportDiagnostic(this.emitter.getProgram(), { code: "invalid-intrinsic", target: intrinsic, format: { typeName: intrinsic.name }, }); return ""; case "void": const type = getCSharpTypeForIntrinsic(this.emitter.getProgram(), intrinsic); return this.emitter.result.rawCode(`${type?.type.getTypeReference()}`); } } #emitUsings(file) { const builder = new StringBuilder(); if (file === undefined) { file = this.emitter.getContext().file; } for (const ns of [...file.imports.keys()]) builder.pushLiteralSegment(`using ${ns};`); return builder.segments.join("\n"); } modelDeclaration(model, name) { const parts = this.#getMultipartParts(model); if (parts.length > 0) { parts.forEach((p) => this.emitter.emitType(p)); return ""; } const isErrorType = isErrorModel(this.emitter.getProgram(), model); const baseModelRef = model.baseModel ? code `: ${this.emitter.emitTypeReference(model.baseModel)}` : ""; const baseClass = baseModelRef || (isErrorType ? ": HttpServiceException" : ""); const namespace = this.emitter.getContext().namespace; const className = ensureCSharpIdentifier(this.emitter.getProgram(), model, name); const doc = getDoc(this.emitter.getProgram(), model); const attributes = getModelAttributes(this.emitter.getProgram(), model, className); const exceptionConstructor = isErrorType ? this.getModelExceptionConstructor(this.emitter.getProgram(), model, name, className) : ""; this.#metadateMap.set(model, new CSharpType({ name: className, namespace: namespace })); const decl = this.emitter.result.declaration(className, code `${this.#generatedFileHeader} ${this.#emitUsings()} namespace ${namespace} { ${doc ? `${formatComment(doc)}\n` : ""}${`${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}${attributes?.length > 0 ? "\n" : ""}`}public partial class ${className} ${baseClass} { ${exceptionConstructor ? `${exceptionConstructor}\n` : ""}${this.emitter.emitModelProperties(model)} } } `); return decl; } getModelExceptionConstructor(program, model, modelName, className) { if (!isErrorModel(program, model)) return undefined; const constructor = this.getExceptionConstructorData(program, model, modelName); const isParent = !!model.derivedModels?.length; return `public ${className}(${constructor.properties}) : base(${constructor.statusCode?.value ?? `400`}${constructor.header ? `, \n\t\t headers: new(){${constructor.header}}` : ""}${constructor.value ? `, \n\t\t value: new{${constructor.value}}` : ""}) { ${constructor.body ? `\n${constructor.body}` : ""}\n\t}${isParent ? `\npublic ${className}(int statusCode, object? value = null, Dictionary<string, string>? headers = default): base(statusCode, value, headers) {}\n` : ""}`; } isDuplicateExceptionName(name) { const exceptionPropertyNames = [ "value", "headers", "stacktrace", "source", "message", "innerexception", "hresult", "data", "targetsite", "helplink", ]; return exceptionPropertyNames.includes(name.toLowerCase()); } getExceptionConstructorData(program, model, modelName) { const allProperties = new ModelInfo().getAllProperties(program, model) ?? []; const propertiesWithDefaults = allProperties.map((prop) => { const { defaultValue: typeDefault } = this.#findPropertyType(prop); const defaultValue = prop.defaultValue ? code `${JSON.stringify(serializeValueAsJson(program, prop.defaultValue, prop))}` : typeDefault; return { prop, defaultValue }; }); const sortedProperties = propertiesWithDefaults .filter(({ prop }) => !isStatusCode(program, prop)) .sort(({ prop: a, defaultValue: aDefault }, { prop: b, defaultValue: bDefault }) => { if (!a.optional && !aDefault && (b.optional || bDefault)) return -1; if (!b.optional && !bDefault && (a.optional || aDefault)) return 1; return 0; }); const properties = []; const body = []; const header = []; const value = []; const statusCode = getStatusCode(program, model); if (statusCode?.requiresConstructorArgument) { properties.push(`int ${statusCode.value}`); } for (const { prop, defaultValue } of sortedProperties) { let propertyName = ensureCSharpIdentifier(program, prop, prop.name); if (modelName === propertyName || this.isDuplicateExceptionName(propertyName)) { propertyName = `${propertyName}Prop`; } const type = getCSharpType(program, prop.type); properties.push(`${type?.type.name} ${prop.name}${defaultValue ? ` = ${defaultValue}` : `${prop.optional ? " = default" : ""}`}`); body.push(`\t\t${propertyName} = ${prop.name};`); if (isHeader(program, prop)) { const headerName = getHeaderFieldName(program, prop); header.push(`{"${headerName}", ${prop.name}}`); } else { value.push(`${prop.name} = ${prop.name}`); } } return { properties: properties.join(", "), body: body.join("\n"), header: header.join(", "), value: value.join(","), statusCode: statusCode, }; } modelDeclarationContext(model, name) { if (this.#isMultipartModel(model)) return {}; const modelName = ensureCSharpIdentifier(this.emitter.getProgram(), model, name); const modelFile = this.emitter.createSourceFile(`generated/models/${modelName}.cs`); modelFile.meta[this.#sourceTypeKey] = CSharpSourceType.Model; const modelNamespace = `${this.#getOrSetBaseNamespace(model)}.Models`; return this.#createModelContext(modelNamespace, modelFile, modelName); } modelInstantiationContext(model) { if (this.#isMultipartModel(model)) return {}; const modelName = getModelInstantiationName(this.emitter.getProgram(), model, model.name); const sourceFile = this.emitter.createSourceFile(`generated/models/${modelName}.cs`); sourceFile.meta[this.#sourceTypeKey] = CSharpSourceType.Model; const modelNamespace = `${this.#getOrSetBaseNamespace(model)}.Models`; const context = this.#createModelContext(modelNamespace, sourceFile, model.name); context.instantiationName = modelName; return context; } modelInstantiation(model, name) { const parts = this.#getMultipartParts(model); if (parts.length > 0) { parts.forEach((p) => this.emitter.emitType(p)); return ""; } const program = this.emitter.getProgram(); const recordType = getRecordType(program, model); if (recordType !== undefined) { return code `Dictionary<string, ${this.emitter.emitTypeReference(recordType)}>`; } const context = this.emitter.getContext(); const className = context.instantiationName ?? name; return this.modelDeclaration(model, className); } #getMultipartParts(model) { const parts = [...model.properties.values()] .flatMap((p) => getHttpPart(this.emitter.getProgram(), p.type)?.type) .filter((t) => t !== undefined); if (model.baseModel) { return parts.concat(this.#getMultipartParts(model.baseModel)); } return parts; } #isMultipartModel(model) { const multipartTypes = this.#getMultipartParts(model); return multipartTypes.length > 0; } modelProperties(model) { const result = new StringBuilder(); for (const [_, prop] of model.properties) { if (!isVoidType(prop.type) && !isNeverType(prop.type) && !isNullType(prop.type) && !isErrorModel(this.emitter.getProgram(), prop.type)) { result.push(code `${this.emitter.emitModelProperty(prop)}`); } } return result.reduce(); } modelLiteralContext(model) { const name = this.emitter.emitDeclarationName(model) || ""; return this.modelDeclarationContext(model, name); } modelLiteral(model) { const modelName = this.emitter.getContext().name; reportDiagnostic(this.emitter.getProgram(), { code: "anonymous-model", target: model, format: { emittedName: modelName }, }); return this.modelDeclaration(model, modelName); } #isInheritedProperty(property) { const visited = []; function isInherited(model, propertyName) { if (visited.includes(model)) return false; visited.push(model); if (model.properties.has(propertyName)) return true; if (model.baseModel === undefined) return false; return isInherited(model.baseModel, propertyName); } const model = property.model; if (model === undefined || model.baseModel === undefined) return false; return isInherited(model.baseModel, property.name); } modelPropertyLiteral(property) { if (isStatusCode(this.emitter.getProgram(), property)) return ""; let propertyName = ensureCSharpIdentifier(this.emitter.getProgram(), property, property.name); const { typeReference: typeName, defaultValue: typeDefault, nullableType: nullable, } = this.#findPropertyType(property); const doc = getDoc(this.emitter.getProgram(), property); const attributes = getModelAttributes(this.emitter.getProgram(), property, propertyName); const modelName = this.emitter.getContext()["name"]; if (modelName === propertyName || (this.isDuplicateExceptionName(propertyName) && property.model && isErrorModel(this.emitter.getProgram(), property.model))) { propertyName = `${propertyName}Prop`; attributes.push(getEncodedNameAttribute(this.emitter.getProgram(), property, propertyName)); } const defaultValue = property.defaultValue ? code `${JSON.stringify(serializeValueAsJson(this.emitter.getProgram(), property.defaultValue, property))}` : typeDefault; return this.emitter.result .rawCode(code `${doc ? `${formatComment(doc)}\n` : ""}${`${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}${attributes?.length > 0 ? "\n" : ""}`}public ${this.#isInheritedProperty(property) ? "new " : ""}${typeName}${isValueType(this.emitter.getProgram(), property.type) && (property.optional || nullable) ? "?" : ""} ${propertyName} { get; ${typeDefault ? "}" : "set; }"}${defaultValue ? ` = ${defaultValue};\n` : "\n"} `); } #findPropertyType(property) { return this.#opHelpers.getTypeInfo(this.emitter.getProgram(), property.type); } #isMultipartRequest(operation) { const body = operation.parameters.body; if (body === undefined) return false; return body.bodyKind === "multipart"; } #hasMultipartOperation(iface) { for (const [_, operation] of iface.operations) { const [httpOp, _] = getHttpOperation(this.emitter.getProgram(), operation); if (this.#isMultipartRequest(httpOp)) return true; } return false; } modelPropertyReference(property) { return code `${this.emitter.emitTypeReference(property.type)}`; } numericLiteral(number) { return this.emitter.result.rawCode(code `${number.value.toString()}`); } interfaceDeclaration(iface, name) { // interface declaration const ifaceName = `I${ensureCSharpIdentifier(this.emitter.getProgram(), iface, name, NameCasingType.Class)}`; const namespace = this.emitter.getContext().namespace; const doc = getDoc(this.emitter.getProgram(), iface); const attributes = getModelAttributes(this.emitter.getProgram(), iface, ifaceName); this.#metadateMap.set(iface, new CSharpType({ name: ifaceName, namespace: namespace })); const decl = this.emitter.result.declaration(ifaceName, code `${this.#generatedFileHeaderWithNullable} ${this.#emitUsings()} namespace ${namespace} { ${doc ? `${formatComment(doc)}\n` : ""}${`${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}${attributes?.length > 0 ? "\n" : ""}`}public interface ${ifaceName} { ${this.emitter.emitInterfaceOperations(iface)} } } `); return decl; } interfaceDeclarationContext(iface, name) { // set up interfaces file for declaration const ifaceName = `I${ensureCSharpIdentifier(this.emitter.getProgram(), iface, name, NameCasingType.Class)}`; const sourceFile = this.emitter.createSourceFile(`generated/operations/${ifaceName}.cs`); sourceFile.meta[this.#sourceTypeKey] = CSharpSourceType.Interface; const ifaceNamespace = this.#getOrSetBaseNamespace(iface); const modelNamespace = `${ifaceNamespace}.Models`; const context = this.#createModelContext(ifaceNamespace, sourceFile, ifaceName); context.file.imports.set("System", ["System"]); context.file.imports.set("System.Net", ["System.Net"]); context.file.imports.set("System.Text.Json", ["System.Text.Json"]); context.file.imports.set("System.Text.Json.Serialization", [ "System.Text.Json.Serialization", ]); context.file.imports.set("System.Threading.Tasks", ["System.Threading.Tasks"]); context.file.imports.set("Microsoft.AspNetCore.Mvc", ["Microsoft.AspNetCore.Mvc"]); if (this.#hasMultipartOperation(iface)) { context.file.imports.set("Microsoft.AspNetCore.WebUtilities", [ "Microsoft.AspNetCore.WebUtilities", ]); } context.file.imports.set(modelNamespace, [modelNamespace]); return context; } interfaceDeclarationOperations(iface) { // add in operations const builder = new StringBuilder(); const metadata = new HttpMetadata(); const context = this.emitter.getContext(); const name = `${ensureCSharpIdentifier(this.emitter.getProgram(), iface, iface.name, NameCasingType.Class)}`; const ifaceNamespace = this.#getOrSetBaseNamespace(iface); const namespace = `${ifaceNamespace}`; const mock = { className: name, interfaceName: `I${name}`, methods: [], namespace: namespace, usings: [`${ifaceNamespace}.Models`], }; for (const [name, operation] of iface.operations) { const doc = getDoc(this.emitter.getProgram(), operation); const returnTypes = []; const [httpOp, _] = getHttpOperation(this.emitter.getProgram(), operation); for (const response of httpOp.responses.filter((r) => !isErrorModel(this.emitter.getProgram(), r.type))) { returnTypes.push(metadata.resolveLogicalResponseType(this.emitter.getProgram(), response)); } const returnInfo = coalesceTypes(this.emitter.getProgram(), returnTypes, context.namespace); const returnType = returnInfo?.type || UnknownType; const opName = ensureCSharpIdentifier(this.emitter.getProgram(), operation, name, NameCasingType.Method); const parameters = this.#opHelpers.getParameters(this.emitter.getProgram(), httpOp); const opImpl = { methodName: `${opName}Async`, methodParams: `${getBusinessLogicDeclParameters(parameters)}`, returnType: returnType, returnTypeName: `${returnType.name === "void" ? "Task" : `Task<${returnType.getTypeReference(context.scope)}>`}`, instantiatedReturnType: returnType.name === "void" ? undefined : `${returnType.getTypeReference(context.scope)}`, }; const opDecl = this.emitter.result.declaration(opName, code `${doc ? `${formatComment(doc)}\n` : ""}${opImpl.returnTypeName} ${opImpl.methodName}( ${opImpl.methodParams});`); mock.methods.push(opImpl); builder.push(code `${opDecl.value}\n`); this.emitter.emitInterfaceOperation(operation); } this.#mockRegistrations.set(mock.interfaceName, mock); return builder.reduce(); } interfaceOperationDeclarationContext(operation) { const resource = getResourceOperation(this.emitter.getProgram(), operation); const controllerName = operation.interface !== undefined ? operation.interface.name : resource === undefined ? NoResourceContext : resource.resourceType.name; return this.#createOrGetResourceContext(controllerName, operation, resource?.resourceType); } interfaceOperationDeclaration(operation, name) { const operationName = ensureCSharpIdentifier(this.emitter.getProgram(), operation, name, NameCasingType.Method); const doc = getDoc(this.emitter.getProgram(), operation); const [httpOperation, _] = getHttpOperation(this.emitter.getProgram(), operation); const multipart = this.#isMultipartRequest(httpOperation); const parameters = this.#opHelpers.getParameters(this.emitter.getProgram(), httpOperation); const declParams = getHttpDeclParameters(parameters); if (multipart) { const context = this.emitter.getContext(); context.file.imports.set("Microsoft.AspNetCore.WebUtilities", [ "Microsoft.AspNetCore.WebUtilities", ]); context.file.imports.set("Microsoft.AspNetCore.Http.Extensions", [ "Microsoft.AspNetCore.Http.Extensions", ]); } const responseInfo = this.#getOperationResponse(httpOperation); const status = responseInfo?.statusCode ?? 200; const response = responseInfo?.resultType ?? new CSharpType({ name: "void", namespace: "System", isBuiltIn: true, isValueType: false, }); const hasResponseValue = response.name !== "void"; const resultString = `${status === 204 ? "NoContent" : "Ok"}`; if (!this.#isMultipartRequest(httpOperation)) { return this.emitter.result.declaration(operation.name, code ` ${doc ? `${formatComment(doc)}` : ""} [${getOperationVerbDecorator(httpOperation)}] [Route("${httpOperation.path}")] ${this.emitter.emitOperationReturnType(operation)} public virtual async Task<IActionResult> ${operationName}(${declParams}) { ${hasResponseValue ? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)}); return ${resultString}(result);` : `await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)}); return ${resultString}();`} }`); } else { return this.emitter.result.declaration(operation.name, code ` ${doc ? `${formatComment(doc)}` : ""} [${getOperationVerbDecorator(httpOperation)}] [Route("${httpOperation.path}")] [Consumes("multipart/form-data")] ${this.emitter.emitOperationReturnType(operation)} public virtual async Task<IActionResult> ${operationName}(${declParams}) { var boundary = Request.GetMultipartBoundary(); if (boundary == null) { return BadRequest("Request missing multipart boundary"); } var reader = new MultipartReader(boundary, Request.Body); ${hasResponseValue ? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)}); return ${resultString}(result);` : `await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)}); return ${resultString}();`} }`); } } operationDeclarationContext(operation, name) { const resource = getResourceOperation(this.emitter.getProgram(), operation); const controllerName = (operation.interface?.name ?? resource === undefined) ? NoResourceContext : resource.resourceType.name; return this.#createOrGetResourceContext(controllerName, operation, resource?.resourceType); } operationDeclaration(operation, name) { const operationName = ensureCSharpIdentifier(this.emitter.getProgram(), operation, name, NameCasingType.Method); const doc = getDoc(this.emitter.getProgram(), operation); const [httpOperation, _] = getHttpOperation(this.emitter.getProgram(), operation); const parameters = this.#opHelpers.getParameters(this.emitter.getProgram(), httpOperation); const declParams = getHttpDeclParameters(parameters); const responseInfo = this.#getOperationResponse(httpOperation); const status = responseInfo?.statusCode ?? 200; const response = responseInfo?.resultType ?? new CSharpType({ name: "void", namespace: "System", isBuiltIn: true, isValueType: false, }); const hasResponseValue = response.name !== "void"; const resultString = `${status === 204 ? "NoContent" : "Ok"}`; return this.emitter.result.declaration(operation.name, code ` ${doc ? `${formatComment(doc)}` : ""} [${getOperationVerbDecorator(httpOperation)}] [Route("${httpOperation.path}")] ${this.emitter.emitOperationReturnType(operation)} public virtual async Task<IActionResult> ${operationName}(${declParams}) { ${hasResponseValue ? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)}); return ${resultString}(result);` : `await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)}); return ${resultString}();`} }`); } operationReturnType(operation, returnType) { const [httpOperation, _] = getHttpOperation(this.emitter.getProgram(), operation); return this.#emitOperationResponses(httpOperation); } stringTemplate(stringTemplate) { return this.emitter.result.rawCode(stringTemplate.stringValue || ""); } #getOperationResponse(operation) { const validResponses = operation.responses.filter((r) => !isErrorModel(this.emitter.getProgram(), r.type) && getCSharpStatusCode(r.statusCodes) !== undefined); if (validResponses.length < 1) return undefined; const response = validResponses[0]; const csharpStatusCode = getCSharpStatusCode(response.statusCodes); if (csharpStatusCode === undefined) return undefined; const responseType = new HttpMetadata().resolveLogicalResponseType(this.emitter.getProgram(), response); const context = this.emitter.getContext(); const result = getCSharpType(this.emitter.getProgram(), responseType, context.namespace); const resultType = result?.type || UnknownType; return { csharpStatusCode, resultType, statusCode: response.statusCodes, }; } #emitOperationResponses(operation) { const builder = new StringBuilder(); let i = 0; const validResponses = operation.responses.filter((r) => !isErrorModel(this.emitter.getProgram(), r.type) && getCSharpStatusCode(r.statusCodes) !== undefined); for (const response of validResponses) { i++; builder.push(code `${this.#emitOperationResponseDecorator(response)}`); if (i < validResponses.length) { builder.pushLiteralSegment("\n"); } } for (const response of operation.responses) { if (!isEmptyResponseModel(this.emitter.getProgram(), response.type)) this.emitter.emitType(response.type); } return builder.reduce(); } #emitOperationResponseDecorator(response) { const responseType = new HttpMetadata().resolveLogicalResponseType(this.emitter.getProgram(), response); return this.emitter.result.rawCode(code `[ProducesResponseType((int)${getCSharpStatusCode(response.statusCodes)}, Type = typeof(${this.#emitResponseType(responseType)}))]`); } #emitResponseType(type) { const context = this.emitter.getContext(); const result = getCSharpType(this.emitter.getProgram(), type, context.namespace); const resultType = result?.type || UnknownType; return resultType.getTypeReference(context.scope); } unionDeclaration(union, name) { const baseType = coalesceUnionTypes(this.emitter.getProgram(), union); if (baseType.isBuiltIn && baseType.name === "string") { const program = this.emitter.getProgram(); const unionName = ensureCSharpIdentifier(program, union, name); const namespace = this.emitter.getContext().namespace; const doc = getDoc(this.emitter.getProgram(), union); const attributes = getModelAttributes(program, union, unionName); this.#metadateMap.set(union, new CSharpType({ name: unionName, namespace: namespace })); return this.emitter.result.declaration(unionName, code `${this.#generatedFileHeader} ${this.#emitUsings()} namespace ${namespace} { ${doc ? `${formatComment(doc)}` : ""} ${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")} public enum ${unionName} { ${this.emitter.emitUnionVariants(union)}; } } `); } return this.emitter.result.rawCode(code `${baseType.getTypeReference()}`); } unionDeclarationContext(union) { const baseType = coalesceUnionTypes(this.emitter.getProgram(), union); if (baseType.isBuiltIn && baseType.name === "string") { const unionName = ensureCSharpIdentifier(this.emitter.getProgram(), union, union.name || "Union"); const unionFile = this.emitter.createSourceFile(`generated/models/${unionName}.cs`); unionFile.meta[this.#sourceTypeKey] = CSharpSourceType.Model; const unionNamespace = `${this.#getOrSetBaseNamespace(union)}.Models`; return { namespace: unionNamespace, file: unionFile, scope: unionFile.globalScope, }; } else { return this.emitter.getContext(); } } unionInstantiation(union, name) { return super.unionInstantiation(union, name); } unionInstantiationContext(union, name) { return super.unionInstantiationContext(union, name); } unionVariants(union) { const result = new StringBuilder(); let i = 0; for (const [name, variant] of union.variants) { i++; if (variant.type.kind === "String") { const nameHint = name || variant.type.value; const memberName = ensureCSharpIdentifier(this.emitter.getProgram(), variant, nameHint); this.#metadateMap.set(variant, { name: memberName }); result.push(code `${ensureCSharpIdentifier(this.emitter.getProgram(), variant, nameHint)} = "${variant.type.value}"`); if (i < union.variants.size) result.pushLiteralSegment(", "); } } return this.emitter.result.rawCode(result.reduce()); } unionVariantContext(union) { return super.unionVariantContext(union); } #createModelContext(namespace, file, name) { const context = { namespace: namespace, name: name, file: file, scope: file.globalScope, }; context.file.imports.set("System", ["System"]); context.file.imports.set("System.Text.Json", ["System.Text.Json"]); context.file.imports.set("System.Text.Json.Serialization", [ "System.Text.Json.Serialization", ]); context.file.imports.set("TypeSpec.Helpers.JsonConverters", [ "TypeSpec.Helpers.JsonConverters", ]); context.file.imports.set("TypeSpec.Helpers", ["TypeSpec.Helpers"]); return context; } #createEnumContext(namespace, file, name) { const context = { namespace: namespace, name: name, file: file, scope: file.globalScope, }; context.file.imports.set("System.Text.Json", ["System.Text.Json"]); context.file.imports.set("System.Text.Json.Serialization", [ "System.Text.Json.Serialization", ]); return context; } #createOrGetResourceContext(name, operation, resource) { name = ensureCSharpIdentifier(this.emitter.getProgram(), operation, name, NameCasingType.Class); let context = controllers.get(name); if (context !== undefined) return context; const sourceFile = this.emitter.createSourceFile(`generated/controllers/${name}Controller.cs`); const namespace = this.#getOrSetBaseNamespace(operation); const modelNamespace = `${namespace}.Models`; sourceFile.meta[this.#sourceTypeKey] = CSharpSourceType.Controller; sourceFile.meta["resourceName"] = name; sourceFile.meta["resource"] = `${name}Controller`; sourceFile.meta["namespace"] = namespace; context = { namespace: sourceFile.meta["namespace"], file: sourceFile, resourceName: name, scope: sourceFile.globalScope, resourceType: resource, }; context.file.imports.set("System", ["System"]); context.file.imports.set("System.Net", ["System.Net"]); context.file.imports.set("System.Threading.Tasks", ["System.Threading.Tasks"]); context.file.imports.set("System.Text.Json", ["System.Text.Json"]); context.file.imports.set("System.Text.Json.Serialization", [ "System.Text.Json.Serialization", ]); context.file.imports.set("Microsoft.AspNetCore.Mvc", ["Microsoft.AspNetCore.Mvc"]); context.file.imports.set(modelNamespace, [modelNamespace]); context.file.imports.set(namespace, [namespace]); controllers.set(name, context); return context; } // eslint-disable-next-line no-unused-private-class-members #getNamespaceFullName(namespace) { return namespace ? ensureCSharpIdentifier(this.emitter.getProgram(), namespace, getNamespaceFullName(namespace)) : "TypeSpec"; } reference(targetDeclaration, pathUp, pathDown, commonScope) { //if (targetDeclaration.name) return targetDeclaration.name; return super.reference(targetDeclaration, pathUp, pathDown, commonScope); } scalarInstantiation(scalar, name) { const scalarType = getCSharpTypeForScalar(this.emitter.getProgram(), scalar); return scalarType.getTypeReference(this.emitter.getContext().scope); } scalarDeclaration(scalar, name) { const scalarType = getCSharpTypeForScalar(this.emitter.getProgram(), scalar); return scalarType.getTypeReference(this.emitter.getContext().scope); } sourceFile(sourceFile) { if (sourceFile.meta.emitted) { return sourceFile.meta.emitted; } const emittedSourceFile = { path: sourceFile.path, contents: "", }; switch (sourceFile.meta[this.#sourceTypeKey]) { case CSharpSourceType.Controller: emittedSourceFile.contents = this.#emitControllerContents(sourceFile); break; default: emittedSourceFile.contents = this.#emitCodeContents(sourceFile); break; } return emittedSourceFile; } #emitCodeContents(file) { const contents = new StringBuilder(); for (const decl of file.globalScope.declarations) { contents.push(decl.value); } return contents.segments.join("\n") + "\n"; } #emitControllerContents(file) { const namespace = file.meta.namespace; const contents = new StringBuilder(); contents.push(`${this.#generatedFileHeaderWithNullable}\n\n`); contents.push(code `${this.#emitUsings(file)}\n`); contents.push("\n"); contents.push(`namespace ${namespace}.Controllers\n`); contents.push("{\n"); contents.push("[ApiController]\n"); contents.push(`public partial class ${file.meta["resource"]}: ControllerBase\n`); contents.push("{\n"); contents.push("\n"); contents.push(`public ${file.meta["resource"]}(I${file.meta.resourceName} operations)\n`); contents.push("{\n"); contents.push(` ${file.meta.resourceName}Impl = operations;\n`); contents.push("}"); contents.push("\n"); contents.push(code `internal virtual I${file.meta.resourceName} ${file.meta.resourceName}Impl { get;}\n`); for (const decl of file.globalScope.declarations) { contents.push(decl.value + "\n"); } contents.push("\n}"); contents.push("\n}"); return contents.segments.join("\n") + "\n"; } stringLiteral(string) { return this.emitter.result.rawCode(code `"${string.value}"`); } tupleLiteral(tuple) { return this.emitter.result.rawCode(code `{ ${this.emitter.emitTupleLiteralValues(tuple)} }`); } tupleLiteralValues(tuple) { const result = new StringBuilder(); for (const tupleValue of tuple.values) { result.push(code `${this.emitter.emitType(tupleValue)}`); } return this.emitter.result.rawCode(result.segments.join(",\n")); } createModelScope(baseScope, namespace) { let current = baseScope; for (const part of namespace.split(".")) { current = this.emitter.createScope({}, getCSharpIdentifier(part), current); } return current; } // TODO: remove? // eslint-disable-next-line no-unused-private-class-members #getTemplateParameters(model) { if (!model.templateMapper) return ""; let i = 0; const params = new StringBuilder(); const args = model.templateMapper.args .flatMap((parm) => parm) .filter((arg) => arg !== null && isKnownReferenceType(this.emitter.getProgram(), arg)); for (const parameter of args) { i++; params.push(code `${this.emitter.emitTypeReference(parameter)}`); if (i < args.length) { params.pushLiteralSegment(","); } } if (params.segments.length > 0) return params.reduce(); return ""; } async writeOutput(sourceFiles) { sourceFiles.push(...getSerializationSourceFiles(this.emitter).flatMap((l) => l.source)); sourceFiles.push(...getProjectDocs(this.emitter, this.#useSwagger, this.#mockRegistrations).flatMap((l) => l.source)); if (this.#emitMocks === "mocks-and-project-files" || this.#emitMocks === "mocks-only") { if (this.#mockRegistrations.size > 0) { const mocks = getBusinessLogicImplementations(this.emitter, this.#mockRegistrations, this.#useSwagger, this.#openapiPath); sourceFiles.push(...mocks.flatMap((l) => l.source)); } } async function shouldWrite(source, exists) { return (!source.meta.conditional || options.overwrite === true || !(await exists(source.path))); } const emittedSourceFiles = []; for (const source of sourceFiles) { switch (this.#emitterOutputType) { case "models": { switch (source.meta[this.#sourceTypeKey]) { case CSharpSourceType.Controller: // do nothing break; default: if (await shouldWrite(source, this.#fileExists)) { emittedSourceFiles.push(source); } break; } } break; default: if (await shouldWrite(source, this.#fileExists)) { emittedSourceFiles.push(source); } break; } } return super.writeOutput(emittedSourceFiles); } #getOrSetBaseNamespace(type) { if (this.#baseNamespace === undefined) { if (type.namespace !== undefined) { this.#baseNamespace = `${type.namespace ? ensureCSharpIdentifier(this.emitter.getProgram(), type.namespace, getNamespaceFullName(type.namespace)) : "TypeSpec"}.Service`; } else { this.#baseNamespace = "TypeSpec.Service"; } } return this.#baseNamespace; } } function processNameSpace(program, target, service) { if (!service) service = getService(program, target); if (service) {