@typespec/http-server-csharp
Version:
TypeSpec service code generator for c-sharp
953 lines (939 loc) • 55.4 kB
JavaScript
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) {