UNPKG

@typespec/http-server-csharp

Version:

TypeSpec service code generator for c-sharp

929 lines (922 loc) 61.7 kB
import { CodeTypeEmitter, StringBuilder, code, createAssetEmitter, } from "@typespec/asset-emitter"; import { getDoc, getNamespaceFullName, getService, isErrorModel, isNeverType, isNullType, isNumericType, isTemplateDeclaration, isVoidType, resolvePath, serializeValueAsJson, } from "@typespec/compiler"; import { createRekeyableMap } from "@typespec/compiler/utils"; import { getHeaderFieldName, getHttpOperation, getHttpPart, isHeader, isStatusCode, } from "@typespec/http"; import { getUniqueItems } from "@typespec/json-schema"; 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, CollectionType, NameCasingType, checkOrAddNamespaceToScope, } from "./interfaces.js"; import { CSharpServiceOptions, 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, getImports, getModelAttributes, getModelDeclarationName, getModelInstantiationName, getOpenApiConfig, getOperationVerbDecorator, getStatusCode, isEmptyResponseModel, isRecord, isStringEnumType, isValueType, resolveReferenceFromScopes, } 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; CSharpServiceOptions.getInstance().initialize(context.options); 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"; #nsKey = "ResolvedNamespace"; #namespaces = new Map(); #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()); #getOrAddNamespaceForType(type) { const program = this.emitter.getProgram(); switch (type.kind) { case "Boolean": case "String": case "StringTemplate": case "Number": case "Scalar": case "Intrinsic": case "FunctionParameter": case "ScalarConstructor": case "StringTemplateSpan": case "TemplateParameter": case "Tuple": return undefined; case "EnumMember": return this.#getOrAddNamespaceForType(type.enum); case "ModelProperty": case "UnionVariant": return this.#getOrAddNamespaceForType(type.type); case "Model": if (isRecord(type) && type.indexer) return this.#getOrAddNamespaceForType(type.indexer.value); if (type.indexer !== undefined && isNumericType(program, type.indexer?.key)) return this.#getOrAddNamespaceForType(type.indexer.value); return this.#getOrAddNamespace(type.namespace); case "Union": if (isStringEnumType(program, type)) return this.#getOrAddNamespace(type.namespace); const unionType = this.#coalesceTsUnion(type); if (unionType === undefined) return undefined; return this.#getOrAddNamespaceForType(unionType); default: return this.#getOrAddNamespace(type.namespace); } } #coalesceTsUnion(union) { let result = undefined; for (const [_, variant] of union.variants) { if (!isNullType(variant.type)) { if (result !== undefined && result !== variant.type) return undefined; result = variant.type; } } return result; } #getOrAddNamespace(typespecNamespace) { if (!typespecNamespace?.name) return this.#getDefaultNamespace(); let resolvedNamespace = this.#namespaces.get(typespecNamespace); if (resolvedNamespace !== undefined) return resolvedNamespace; const nsName = getNamespaceFullName(typespecNamespace); resolvedNamespace = ensureCSharpIdentifier(this.emitter.getProgram(), typespecNamespace, nsName, NameCasingType.Namespace); this.#namespaces.set(typespecNamespace, resolvedNamespace); return resolvedNamespace; } #getDefaultNamespace() { return "TypeSpec.Service"; } arrayDeclaration(array, name, elementType) { return this.collectionDeclaration(elementType, array); } arrayLiteral(array, elementType) { this.emitter.emitType(elementType, this.emitter.getContext()); const csType = getCSharpType(this.emitter.getProgram(), array, this.#getOrAddNamespaceForType(elementType)); if (csType?.type) { return code `${csType.type.getTypeReference(this.emitter.getContext()?.scope)}`; } return this.collectionDeclaration(elementType, array); } collectionDeclaration(elementType, array) { const isUniqueItems = getUniqueItems(this.emitter.getProgram(), array); const collectionType = isUniqueItems ? CollectionType.ISet : CSharpServiceOptions.getInstance().collectionType; switch (collectionType) { case CollectionType.ISet: return code `ISet<${this.emitter.emitTypeReference(elementType, this.emitter.getContext())}>`; case CollectionType.IEnumerable: return code `IEnumerable<${this.emitter.emitTypeReference(elementType, this.emitter.getContext())}>`; case CollectionType.Array: default: return code `${this.emitter.emitTypeReference(elementType, this.emitter.getContext())}[]`; } } booleanLiteral(boolean) { return 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 ` 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.#getOrAddNamespace(en.namespace); 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": case "null": return this.emitter.result.rawCode(code `${UnknownType.getTypeReference(this.emitter.getContext().scope)}`); 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(this.emitter.getContext().scope)}`); } } #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); if (model.baseModel && model.baseModel.namespace !== model.namespace) { const resolvedNs = this.#getOrAddNamespaceForType(model.baseModel); if (resolvedNs) checkOrAddNamespaceToScope(resolvedNs, this.emitter.getContext().scope); } const baseModelRef = model.baseModel ? code `: ${this.emitter.emitTypeReference(model.baseModel, this.emitter.getContext())}` : ""; 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 ` 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) { function getSourceModels(source, visited = new Set()) { const result = []; if (visited.has(source)) return []; result.push(source); visited.add(source); for (const candidate of source.sourceModels) { result.push(...getSourceModels(candidate.model, visited)); } return result; } function getModelNamespace(model) { const sourceModels = getSourceModels(model); if (sourceModels.length === 0) return undefined; return sourceModels .filter((m) => m.namespace !== undefined) .flatMap((s) => s.namespace) .reduce((c, n) => (c === n ? c : undefined)); } if (this.#isMultipartModel(model)) return this.emitter.getContext(); const modelName = ensureCSharpIdentifier(this.emitter.getProgram(), model, name); const modelFile = this.emitter.createSourceFile(`generated/models/${modelName}.cs`); modelFile.meta[this.#sourceTypeKey] = CSharpSourceType.Model; const ns = model.namespace ?? getModelNamespace(model); const modelNamespace = this.#getOrAddNamespace(ns); return this.#createModelContext(modelNamespace, modelFile, modelName); } modelInstantiationContext(model) { if (this.#isMultipartModel(model)) return this.emitter.getContext(); 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.#getOrAddNamespace(model.namespace); 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) { const valueType = this.#getSimpleType(recordType); return code `Dictionary<string, ${valueType ? valueType?.getTypeReference() : this.emitter.emitTypeReference(recordType)}>`; } const context = this.emitter.getContext(); const className = context.instantiationName ?? name; return this.modelDeclaration(model, className); } #getSimpleType(type) { switch (type.kind) { case "Model": if (!isRecord(type)) return undefined; return getCSharpType(this.emitter.getProgram(), type)?.type; case "Boolean": case "Intrinsic": case "ModelProperty": case "Number": case "Scalar": case "String": case "StringTemplate": case "StringTemplateSpan": case "Tuple": return getCSharpType(this.emitter.getProgram(), type)?.type; default: return undefined; } } #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 = new Map(getModelAttributes(this.emitter.getProgram(), property, propertyName).map((a) => [ a.type.name, a, ])); const modelName = this.emitter.getContext()["name"]; if (modelName === propertyName || (this.isDuplicateExceptionName(propertyName) && property.model && isErrorModel(this.emitter.getProgram(), property.model))) { propertyName = `${propertyName}Prop`; const attr = getEncodedNameAttribute(this.emitter.getProgram(), property, propertyName); if (!attributes.has(attr.type.name)) attributes.set(attr.type.name, attr); } const defaultValue = this.#opHelpers.getDefaultValue(this.emitter.getProgram(), property.type, property.defaultValue) ?? typeDefault; const attributeList = [...attributes.values()]; return this.emitter.result .rawCode(code `${doc ? `${formatComment(doc)}\n` : ""}${`${attributeList.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}${attributeList?.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, property); } #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 ` 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; sourceFile.meta["nullable"] = true; const ifaceNamespace = this.#getOrAddNamespace(iface.namespace); sourceFile.imports.set("System", ["System"]); sourceFile.imports.set("System.Net", ["System.Net"]); sourceFile.imports.set("System.Text.Json", ["System.Text.Json"]); sourceFile.imports.set("System.Text.Json.Serialization", ["System.Text.Json.Serialization"]); sourceFile.imports.set("System.Threading.Tasks", ["System.Threading.Tasks"]); sourceFile.imports.set("Microsoft.AspNetCore.Mvc", ["Microsoft.AspNetCore.Mvc"]); if (this.#hasMultipartOperation(iface)) { sourceFile.imports.set("Microsoft.AspNetCore.WebUtilities", [ "Microsoft.AspNetCore.WebUtilities", ]); } return this.#createModelContext(ifaceNamespace, sourceFile, ifaceName); } interfaceDeclarationOperations(iface) { // add in operations const builder = new StringBuilder(); const context = this.emitter.getContext(); const name = `${ensureCSharpIdentifier(this.emitter.getProgram(), iface, iface.name, NameCasingType.Class)}`; const ifaceNamespace = this.#getOrAddNamespace(iface.namespace); const namespace = ifaceNamespace; const mock = { className: name, interfaceName: `I${name}`, methods: [], namespace: namespace, usings: [], }; 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))) { const [_, responseType] = this.#resolveOperationResponse(response, httpOp.operation); returnTypes.push(responseType); } const returnInfo = coalesceTypes(this.emitter.getProgram(), returnTypes, 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); } mock.usings.push(...getImports(context.scope)); 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 || ""); } #resolveOperationResponse(response, operation) { function getName(sourceType, part) { return ensureCSharpIdentifier(emitter.getProgram(), sourceType, part, NameCasingType.Class); } let responseType = new HttpMetadata().resolveLogicalResponseType(this.emitter.getProgram(), response); if (responseType.kind === "Model" && !responseType.name) { const modelName = `${getName(responseType, operation.interface.name)}${getName(responseType, operation.name)}Response}`; const returnedType = this.emitter .getProgram() .checker.cloneType(responseType, { name: modelName }); responseType = returnedType; } this.emitter.emitType(responseType); const context = this.emitter.getContext(); const result = getCSharpType(this.emitter.getProgram(), responseType, context.namespace); return [result?.type || UnknownType, responseType]; } #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) { function isValid(program, response) { return (!isErrorModel(program, response.type) && getCSharpStatusCode(response.statusCodes) !== undefined); } const builder = new StringBuilder(); for (const response of operation.responses) { const [responseType, _] = this.#resolveOperationResponse(response, operation.operation); if (isValid(this.emitter.getProgram(), response)) { builder.push(code `${builder.segments.length > 0 ? "\n" : ""}${this.#emitOperationResponseDecorator(response, responseType)}`); } } return builder.reduce(); } #emitOperationResponseDecorator(response, result) { return this.emitter.result.rawCode(code `[ProducesResponseType((int)${getCSharpStatusCode(response.statusCodes)}, Type = typeof(${this.#emitResponseType(result)}))]`); } #emitResponseType(type) { const context = this.emitter.getContext(); return type.getTypeReference(context.scope); } unionDeclaration(union, name) { const baseType = coalesceUnionTypes(this.emitter.getProgram(), union); if (isStringEnumType(this.emitter.getProgram(), union)) { 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 ` 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(this.emitter.getContext().scope)}`); } unionDeclarationContext(union) { if (isStringEnumType(this.emitter.getProgram(), union)) { 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.#getOrAddNamespace(union.namespace); return this.#createEnumContext(unionNamespace, unionFile, unionName); } 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 ` [JsonStringEnumMemberName("${variant.type.value}")] ${ensureCSharpIdentifier(this.emitter.getProgram(), variant, nameHint, NameCasingType.Property)}`); if (i < union.variants.size) result.pushLiteralSegment(", "); } } return this.emitter.result.rawCode(result.reduce()); } unionVariantContext(union) { return super.unionVariantContext(union); } #createModelContext(namespace, file, name) { file.imports.set("System", ["System"]); file.imports.set("System.Text.Json", ["System.Text.Json"]); file.imports.set("System.Text.Json.Serialization", ["System.Text.Json.Serialization"]); file.imports.set("TypeSpec.Helpers.JsonConverters", ["TypeSpec.Helpers.JsonConverters"]); file.imports.set("TypeSpec.Helpers", ["TypeSpec.Helpers"]); file.meta[this.#nsKey] = namespace; const context = { namespace: namespace, name: name, file: file, scope: file.globalScope, }; return context; } #createEnumContext(namespace, file, name) { file.imports.set("System.Text.Json", ["System.Text.Json"]); file.imports.set("System.Text.Json.Serialization", ["System.Text.Json.Serialization"]); return { namespace: namespace, name: name, file: file, scope: file.globalScope, }; } #createOrGetResourceContext(name, operation, resource) { name = ensureCSharpIdentifier(this.emitter.getProgram(), operation, name, NameCasingType.Class); const namespace = this.#getOrAddNamespace(operation.namespace); let context = controllers.get(`${namespace}.${name}`); if (context !== undefined) return context; const sourceFile = this.emitter.createSourceFile(`generated/controllers/${name}Controller.cs`); sourceFile.meta[this.#sourceTypeKey] = CSharpSourceType.Controller; sourceFile.meta["resourceName"] = name; sourceFile.meta["resource"] = `${name}Controller`; sourceFile.meta["namespace"] = namespace; sourceFile.imports.set("System", ["System"]); sourceFile.imports.set("System.Net", ["System.Net"]); sourceFile.imports.set("System.Threading.Tasks", ["System.Threading.Tasks"]); sourceFile.imports.set("System.Text.Json", ["System.Text.Json"]); sourceFile.imports.set("System.Text.Json.Serialization", ["System.Text.Json.Serialization"]); sourceFile.imports.set("Microsoft.AspNetCore.Mvc", ["Microsoft.AspNetCore.Mvc"]); sourceFile.imports.set(namespace, [namespace]); sourceFile.meta[this.#nsKey] = namespace; context = { namespace: namespace, file: sourceFile, resourceName: name, scope: sourceFile.globalScope, resourceType: resource, }; controllers.set(`${namespace}.${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) { const resolved = resolveReferenceFromScopes(targetDeclaration, pathDown, pathUp); return resolved ?? 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,