@typespec/http-server-csharp
Version:
TypeSpec service code generator for c-sharp
1,301 lines • 60 kB
JavaScript
import { StringBuilder, code, } from "@typespec/asset-emitter";
import { NoTarget, getFriendlyName, getMinValue, isArrayModelType, isErrorModel, isNullType, isNumericType, isTemplateInstance, isUnknownType, isVoidType, resolveCompilerOptions, resolvePath, serializeValueAsJson, } from "@typespec/compiler";
import { $ } from "@typespec/compiler/typekit";
import { Visibility, createMetadataInfo, getHeaderFieldName, isBody, isBodyRoot, isHeader, isMetadata, isPathParam, isQueryParam, isStatusCode, } from "@typespec/http";
import { getUniqueItems } from "@typespec/json-schema";
import { camelCase, pascalCase } from "change-case";
import { createServer } from "net";
import { getAttributes } from "./attributes.js";
import { BooleanValue, CSharpCollectionType, CSharpType, CollectionType, NameCasingType, NullValue, NumericValue, StringValue, checkOrAddNamespaceToScope, } from "./interfaces.js";
import { CSharpServiceOptions, reportDiagnostic } from "./lib.js";
import { getDoubleType, getEnumType } from "./type-helpers.js";
const _scalars = new Map();
export function getCSharpTypeForScalar(program, scalar) {
if (_scalars.has(scalar))
return _scalars.get(scalar);
if (program.checker.isStdType(scalar)) {
return getCSharpTypeForStdScalars(program, scalar);
}
if (scalar.baseScalar) {
return getCSharpTypeForScalar(program, scalar.baseScalar);
}
reportDiagnostic(program, {
code: "unrecognized-scalar",
format: { typeName: scalar.name },
target: scalar,
});
const result = new CSharpType({
name: "object",
namespace: "System",
isBuiltIn: true,
isValueType: false,
});
_scalars.set(scalar, result);
return result;
}
export const UnknownType = new CSharpType({
name: "JsonNode",
namespace: "System.Text.Json.Nodes",
isValueType: false,
isBuiltIn: false,
isClass: true,
});
export const RecordType = new CSharpType({
name: "JsonObject",
namespace: "System.Text.Json.Nodes",
isBuiltIn: false,
isValueType: false,
isClass: true,
});
export function getCSharpType(program, type, namespace) {
const known = getKnownType(program, type);
if (known !== undefined)
return { type: known };
switch (type.kind) {
case "Boolean":
return { type: standardScalars.get("boolean"), value: new BooleanValue(type.value) };
case "Number":
return { type: standardScalars.get("numeric"), value: new NumericValue(type.value) };
case "String":
return { type: standardScalars.get("string"), value: new StringValue(type.value) };
case "EnumMember":
const enumValue = type.value === undefined ? type.name : type.value;
if (typeof enumValue === "string")
return { type: standardScalars.get("string"), value: new StringValue(enumValue) };
else
return { type: standardScalars.get("numeric"), value: new NumericValue(enumValue) };
case "Intrinsic":
return getCSharpTypeForIntrinsic(program, type);
case "ModelProperty":
return getCSharpType(program, type.type, namespace);
case "Scalar":
return { type: getCSharpTypeForScalar(program, type) };
case "Tuple":
const resolvedItem = coalesceTypes(program, type.values, namespace);
const itemType = resolvedItem.type;
return {
type: new CSharpType({
name: `${itemType.name}[]`,
namespace: itemType.namespace,
isBuiltIn: itemType.isBuiltIn,
isValueType: false,
}),
};
case "UnionVariant":
return getCSharpType(program, type.type, namespace);
case "Union":
return coalesceTypes(program, [...type.variants.values()].map((v) => v.type), namespace);
case "Interface":
return {
type: new CSharpType({
name: ensureCSharpIdentifier(program, type, type.name, NameCasingType.Class),
namespace: `${namespace}`,
isBuiltIn: false,
isValueType: false,
isClass: true,
}),
};
case "Enum":
if (getEnumType(type) === "double")
return { type: getDoubleType() };
return {
type: new CSharpType({
name: ensureCSharpIdentifier(program, type, type.name, NameCasingType.Class),
namespace: `${namespace}`,
isBuiltIn: false,
isValueType: true,
}),
};
case "Model":
if (type.indexer !== undefined && isNumericType(program, type.indexer?.key)) {
const resolvedItem = getCSharpType(program, type.indexer.value, namespace);
if (resolvedItem === undefined)
return undefined;
const { type: itemType, value: _ } = resolvedItem;
const uniqueItems = getUniqueItems(program, type);
const isByte = ["byte", "SByte"].includes(itemType.name);
const collectionType = CSharpServiceOptions.getInstance().collectionType;
const returnTypeCollection = uniqueItems
? CollectionType.ISet
: isByte
? CollectionType.Array
: collectionType;
const returnType = returnTypeCollection === CollectionType.Array
? `${itemType.name}[]`
: `${returnTypeCollection}<${itemType.name}>`;
return {
type: new CSharpCollectionType({
name: returnType,
namespace: itemType.namespace,
isBuiltIn: itemType.isBuiltIn,
isValueType: false,
isClass: itemType.isClass,
isCollection: true,
}, returnTypeCollection, itemType.name),
};
}
if (isRecord(type))
return {
type: RecordType,
};
let name = type.name;
if (isTemplateInstance(type)) {
name = getModelInstantiationName(program, type, name);
}
return {
type: new CSharpType({
name: ensureCSharpIdentifier(program, type, name, NameCasingType.Class),
namespace: `${namespace}`,
isBuiltIn: false,
isValueType: false,
isClass: true,
}),
};
default:
return undefined;
}
}
export function resolveReferenceFromScopes(targetDeclaration, declarationScopes, referenceScopes) {
function getSourceFile(scopes) {
for (const scope of scopes) {
if (scope.kind === "sourceFile") {
return { scope: scope, file: scope.sourceFile };
}
}
return undefined;
}
const decl = getSourceFile(declarationScopes);
const ref = getSourceFile(referenceScopes);
if (targetDeclaration.name && decl) {
const declNs = decl.file.meta["ResolvedNamespace"];
if (!ref)
return declNs ? `${declNs}.${targetDeclaration.name} ` : undefined;
if (checkOrAddNamespaceToScope(declNs, ref.scope)) {
return targetDeclaration.name;
}
return declNs ? `${declNs}.${targetDeclaration.name} ` : undefined;
}
return undefined;
}
export function coalesceTypes(program, types, namespace) {
const visited = new Map();
let candidateType = undefined;
let candidateValue = undefined;
for (const type of types) {
if (!isNullType(type)) {
if (!visited.has(type)) {
const resolvedType = getCSharpType(program, type, namespace);
if (resolvedType === undefined)
return { type: UnknownType };
if (resolvedType.type === UnknownType)
return resolvedType;
if (candidateType === undefined) {
candidateType = resolvedType.type;
candidateValue = resolvedType.value;
}
else {
if (candidateValue !== resolvedType.value)
candidateValue = undefined;
if (candidateType !== resolvedType.type)
return { type: UnknownType };
}
visited.set(type, resolvedType);
}
}
}
return { type: candidateType !== undefined ? candidateType : UnknownType, value: candidateValue };
}
export function getKnownType(program, type) {
return undefined;
}
export function getCSharpTypeForIntrinsic(program, type) {
if (isUnknownType(type)) {
return { type: UnknownType };
}
if (isVoidType(type)) {
return {
type: new CSharpType({
name: "void",
namespace: "System",
isBuiltIn: true,
isValueType: false,
}),
};
}
if (isNullType(type)) {
return {
type: new CSharpType({
name: "object",
namespace: "System",
isBuiltIn: true,
isValueType: false,
}),
value: new NullValue(),
};
}
return undefined;
}
const standardScalars = new Map([
[
"bytes",
new CSharpType({ name: "byte[]", namespace: "System", isBuiltIn: true, isValueType: false }),
],
[
"int8",
new CSharpType({ name: "SByte", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"uint8",
new CSharpType({ name: "Byte", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"int16",
new CSharpType({ name: "Int16", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"uint16",
new CSharpType({ name: "UInt16", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"int16",
new CSharpType({ name: "Int16", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"uint16",
new CSharpType({ name: "UInt16", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"int32",
new CSharpType({ name: "int", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"uint32",
new CSharpType({ name: "UInt32", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"integer",
new CSharpType({ name: "long", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"int64",
new CSharpType({ name: "long", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"uint64",
new CSharpType({ name: "UInt64", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"safeint",
new CSharpType({ name: "long", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"float",
new CSharpType({ name: "double", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"float64",
new CSharpType({ name: "double", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"float32",
new CSharpType({ name: "float", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"string",
new CSharpType({ name: "string", namespace: "System", isBuiltIn: true, isValueType: false }),
],
[
"boolean",
new CSharpType({ name: "bool", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"plainDate",
new CSharpType({ name: "DateTime", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"utcDateTime",
new CSharpType({
name: "DateTimeOffset",
namespace: "System",
isBuiltIn: true,
isValueType: true,
}),
],
[
"offsetDateTime",
new CSharpType({
name: "DateTimeOffset",
namespace: "System",
isBuiltIn: true,
isValueType: true,
}),
],
[
"unixTimestamp32",
new CSharpType({
name: "DateTimeOffset",
namespace: "System",
isBuiltIn: true,
isValueType: true,
}),
],
[
"plainTime",
new CSharpType({ name: "DateTime", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"duration",
new CSharpType({ name: "TimeSpan", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"numeric",
new CSharpType({ name: "object", namespace: "System", isBuiltIn: true, isValueType: false }),
],
[
"url",
new CSharpType({ name: "string", namespace: "System", isBuiltIn: true, isValueType: false }),
],
[
"decimal",
new CSharpType({ name: "decimal", namespace: "System", isBuiltIn: true, isValueType: true }),
],
[
"decimal128",
new CSharpType({ name: "decimal", namespace: "System", isBuiltIn: true, isValueType: true }),
],
]);
export function getCSharpTypeForStdScalars(program, scalar) {
const cached = _scalars.get(scalar);
if (cached !== undefined)
return cached;
const builtIn = standardScalars.get(scalar.name);
if (builtIn !== undefined) {
_scalars.set(scalar, builtIn);
if (scalar.name === "numeric" || scalar.name === "integer" || scalar.name === "float") {
reportDiagnostic(program, {
code: "no-numeric",
format: { sourceType: scalar.name, targetType: builtIn?.getTypeReference() },
target: scalar,
});
}
return builtIn;
}
reportDiagnostic(program, {
code: "unrecognized-scalar",
format: { typeName: scalar.name },
target: scalar,
});
return new CSharpType({
name: "Object",
namespace: "System",
isBuiltIn: true,
isValueType: false,
});
}
export function isValueType(program, type) {
if (type.kind === "Boolean" ||
type.kind === "Number" ||
type.kind === "Enum" ||
type.kind === "EnumMember")
return true;
if (type.kind === "Scalar")
return getCSharpTypeForScalar(program, type).isValueType;
if (type.kind !== "Union")
return false;
return [...type.variants.values()]
.flatMap((v) => v.type)
.every((t) => isNullType(t) || isValueType(program, t));
}
export function formatComment(text, lineLength = 76, lineEnd = "\n") {
function getNextLine(target) {
for (let i = lineLength - 1; i > 0; i--) {
if ([" ", ";"].includes(target.charAt(i))) {
return `${target.substring(0, i)}`;
}
}
for (let i = lineLength - 1; i < target.length; i++) {
if ([" ", ";"].includes(target.charAt(i))) {
return `${target.substring(0, i)}`;
}
}
return `${target.substring(0, lineLength)}`;
}
let remaining = text.replaceAll("\n", " ");
const lines = [];
while (remaining.length > lineLength) {
const currentLine = getNextLine(remaining);
remaining =
remaining.length > currentLine.length ? remaining.substring(currentLine.length + 1) : "";
lines.push(`/// ${currentLine}`);
}
if (remaining.length > 0)
lines.push(`/// ${remaining}`);
return `///<summary>${lineEnd}${lines.join(lineEnd)}${lineEnd}///</summary>`;
}
export function getCSharpIdentifier(name, context = NameCasingType.Class, checkReserved = true) {
if (name === undefined)
return "Placeholder";
name = replaceCSharpReservedWord(name, context);
switch (context) {
case NameCasingType.Namespace:
const parts = [];
for (const part of name.split(".")) {
parts.push(getCSharpIdentifier(part, NameCasingType.Class));
}
return parts.join(".");
case NameCasingType.Parameter:
case NameCasingType.Variable:
return `${camelCase(name)}`;
default:
return `${pascalCase(name)}`;
}
}
export function ensureCSharpIdentifier(program, target, name, context = NameCasingType.Class) {
let location = "";
let includeDot = false;
switch (target.kind) {
case "Enum":
location = `enum ${target.name}`;
break;
case "EnumMember":
location = `enum ${target.enum.name}`;
break;
case "Interface":
location = `interface ${target.name}`;
break;
case "Model":
location = `model ${target.name}`;
break;
case "ModelProperty": {
const model = target.model;
if (!model) {
reportDiagnostic(program, {
code: "missing-type-parent",
format: { type: "ModelProperty", name: target.name },
target: target,
});
}
else {
location = `property '${target.name}' in model ${model?.name}`;
if (!model.name) {
location = `parameter '${target.name}' in operation`;
}
}
break;
}
case "Namespace":
location = `namespace ${target.name}`;
let invalid = false;
for (const part of name.split(".")) {
if (!isValidCSharpIdentifier(part)) {
invalid = true;
}
}
if (invalid) {
reportDiagnostic(program, {
code: "invalid-identifier",
format: { identifier: name, location: location },
target: target.node ?? NoTarget,
});
}
includeDot = true;
break;
case "Operation": {
const parent = target.interface
? `interface ${target.interface.name}`
: `namespace ${target.namespace?.name}`;
location = `operation ${target.name} in ${parent}`;
break;
}
case "Union":
location = `union ${target.name}`;
break;
case "UnionVariant": {
location = `variant ${String(target.name)} in union ${target.union.name}`;
break;
}
}
if (!isValidCSharpIdentifier(name, includeDot)) {
reportDiagnostic(program, {
code: "invalid-identifier",
format: { identifier: name, location: location },
target: target.node ?? NoTarget,
});
return getCSharpIdentifier(transformInvalidIdentifier(name), context);
}
return getCSharpIdentifier(name, context);
}
export function getModelAttributes(program, entity, cSharpName) {
return getAttributes(program, entity, cSharpName);
}
export function getModelDeclarationName(program, model, defaultSuffix) {
if (model.name !== null && model.name.length > 0) {
return ensureCSharpIdentifier(program, model, model.name, NameCasingType.Class);
}
if (model.sourceModel && model.sourceModel.name && model.sourceModel.name.length > 0) {
return ensureCSharpIdentifier(program, model, `${model.sourceModel.name}${defaultSuffix}`, NameCasingType.Class);
}
if (model.sourceModels.length > 0) {
const sourceNames = model.sourceModels
.filter((m) => m.model.name !== undefined && m.model.name.length > 0)
.flatMap((m) => ensureCSharpIdentifier(program, model, m.model.name, NameCasingType.Class));
if (sourceNames.length > 0) {
return `${sourceNames.join()}${defaultSuffix}`;
}
}
return `Model${defaultSuffix}`;
}
export function getModelInstantiationName(program, model, name) {
const friendlyName = getFriendlyName(program, model);
if (friendlyName && friendlyName.length > 0)
return friendlyName;
if (name === undefined || name.length < 1)
name = ensureCSharpIdentifier(program, model, "", NameCasingType.Class);
const names = [name];
if (model.templateMapper !== undefined) {
for (const paramType of model.templateMapper.args) {
if (paramType.entityKind === "Type") {
switch (paramType.kind) {
case "Enum":
case "EnumMember":
case "Model":
case "ModelProperty":
case "Namespace":
case "Scalar":
case "Union":
names.push(getCSharpIdentifier(paramType?.name ?? paramType.kind, NameCasingType.Class));
break;
default:
names.push(getCSharpIdentifier(paramType.kind, NameCasingType.Class));
break;
}
}
}
}
return ensureCSharpIdentifier(program, model, names.join(""), NameCasingType.Class);
}
export class ModelInfo {
visited = [];
getAllProperties(program, model) {
if (this.visited.includes(model))
return undefined;
this.visited.push(model);
const props = [];
for (const prop of model.properties.values())
props.push(prop);
if (model.baseModel) {
const additional = this.getAllProperties(program, model.baseModel);
if (additional !== undefined)
props.concat(additional);
}
return props;
}
filterAllProperties(program, model, filter) {
if (this.visited.includes(model))
return undefined;
this.visited.push(model);
for (const prop of model.properties.values()) {
if (filter(prop))
return prop;
}
if (model.baseModel !== undefined) {
return this.filterAllProperties(program, model.baseModel, filter);
}
return undefined;
}
}
export function getPropertySource(program, property) {
let result = property.model;
while (property.sourceProperty !== undefined) {
const current = property.sourceProperty;
result = current.model;
property = property.sourceProperty;
}
return result;
}
export function getSourceModel(program, model) {
const modelTracker = new Set();
for (const prop of model.properties.values()) {
const source = getPropertySource(program, prop);
if (source === undefined)
return undefined;
modelTracker.add(source);
}
if (modelTracker.size === 1)
return [...modelTracker.values()][0];
return undefined;
}
export class HttpMetadata {
resolveLogicalResponseType(program, response) {
const responseType = response.type;
const metaInfo = createMetadataInfo(program, {
canonicalVisibility: Visibility.Read,
canShareProperty: (p) => true,
});
switch (responseType.kind) {
case "Model":
if (responseType.indexer && responseType.indexer.key.name !== "string")
return responseType;
if (isRecord(responseType))
return responseType;
const bodyProp = new ModelInfo().filterAllProperties(program, responseType, (p) => isBody(program, p) || isBodyRoot(program, p));
if (bodyProp !== undefined)
return metaInfo.getEffectivePayloadType(bodyProp.type, Visibility.Read);
const anyProp = new ModelInfo().filterAllProperties(program, responseType, (p) => !isMetadata(program, p) && !isStatusCode(program, p));
if (anyProp === undefined)
return $(program).intrinsic.void;
if (responseType.name === "") {
return metaInfo.getEffectivePayloadType(responseType, Visibility.Read);
}
break;
}
return responseType;
}
}
export function getOperationAttributes(program, entity) {
return getAttributes(program, entity);
}
export function transformInvalidIdentifier(name) {
const result = new StringBuilder();
for (let i = 0; i < name.length; ++i) {
result.pushLiteralSegment(getValidChar(name.charAt(i), i));
}
return result.segments.join("");
}
export function getOperationVerbDecorator(operation) {
switch (operation.verb) {
case "delete":
return "HttpDelete";
case "get":
return "HttpGet";
case "patch":
return "HttpPatch";
case "post":
return "HttpPost";
case "put":
return "HttpPut";
default:
return "HttpGet";
}
}
export function hasNonMetadataProperties(program, model) {
const props = [...model.properties.values()].filter((p) => !isMetadata(program, p));
return props.length > 0;
}
export async function ensureCleanDirectory(program, targetPath) {
try {
await program.host.stat(targetPath);
await program.host.rm(targetPath, { recursive: true });
}
catch { }
await program.host.mkdirp(targetPath);
}
export function isValidCSharpIdentifier(identifier, isNamespace = false) {
if (!isNamespace)
return identifier?.match(/^[A-Za-z_][\w]*$/) !== null;
return identifier?.match(/^[A-Za-z_][\w.]*$/) !== null;
}
export function replaceCSharpReservedWord(identifier, context) {
function generateReplacement(input) {
return [input, `${pascalCase(input)}Name`];
}
const contextualWords = [
"add",
"allows",
"alias",
"and",
"ascending",
"args",
"async",
"await",
"by",
"descending",
"dynamic",
"equals",
"field",
"file",
"from",
"get",
"global",
"group",
"init",
"into",
"join",
"let",
"managed",
"nameof",
"nint",
"not",
"notnull",
"nuint",
"on",
"or",
"orderby",
"partial",
"record",
"remove",
"required",
"scoped",
"select",
"set",
"unmanaged",
"value",
"var",
"when",
"where",
"with",
"yield",
];
const reservedWords = [
"abstract",
"as",
"base",
"bool",
"boolean",
"break",
"byte",
"case",
"catch",
"char",
"checked",
"class",
"const",
"continue",
"decimal",
"default",
"do",
"double",
"else",
"enum",
"event",
"explicit",
"extern",
"false",
"finally",
"fixed",
"float",
"for",
"foreach",
"goto",
"if",
"implicit",
"in",
"int",
"interface",
"internal",
"is",
"lock",
"long",
"namespace",
"new",
"null",
"object",
"operator",
"out",
"override",
"params",
"private",
"protected",
"public",
"readonly",
"ref",
"return",
"sbyte",
"sealed",
"short",
"sizeof",
"stackalloc",
"static",
"string",
"struct",
"switch",
"this",
"throw",
"true",
"try",
"type",
"typeof",
"uint",
"ulong",
"unchecked",
"unsafe",
"ushort",
"using",
"virtual",
"void",
"volatile",
"while",
];
const reserved = new Map(reservedWords.concat(contextualWords).map((w) => generateReplacement(w)));
const check = reserved.get(identifier.toLowerCase());
if (check !== undefined) {
return getCSharpIdentifier(check, context, false);
}
return identifier;
}
export function getValidChar(target, position) {
if (position === 0) {
if (target.match(/[A-Za-z_]/))
return target;
return `Generated_${target.match(/\w/) ? target : ""}`;
}
if (!target.match(/[\w]/))
return "_";
return target;
}
export function getCSharpStatusCode(entry) {
switch (entry) {
case 200:
return "HttpStatusCode.OK";
case 201:
return "HttpStatusCode.Created";
case 202:
return "HttpStatusCode.Accepted";
case 204:
return "HttpStatusCode.NoContent";
default:
return undefined;
}
}
export function isEmptyResponseModel(program, model) {
if (model.kind !== "Model")
return false;
if (model.properties.size === 0)
return true;
return (model.properties.size === 1 &&
isStatusCode(program, [...model.properties.values()][0]) &&
!isErrorModel(program, model));
}
export function isContentTypeHeader(program, parameter) {
return (isHeader(program, parameter) &&
(parameter.name === "contentType" || getHeaderFieldName(program, parameter) === "Content-type"));
}
export function isValidParameter(program, parameter) {
return (!isContentTypeHeader(program, parameter) &&
(parameter.type.kind !== "Intrinsic" || parameter.type.name !== "never") &&
parameter.model?.name === "");
}
/** Determine whether the given parameter is http metadata */
export function isHttpMetadata(program, property) {
return (isPathParam(program, property) || isHeader(program, property) || isQueryParam(program, property));
}
export function getBusinessLogicCallParameters(parameters) {
const builder = new StringBuilder();
const blParameters = parameters.filter((p) => p.operationKind === "BusinessLogic" || p.operationKind === "All");
let i = 0;
for (const param of blParameters) {
builder.push(code `${getBusinessLogicCallParameter(param)}${++i < blParameters.length ? ", " : ""}`);
}
return builder.reduce();
}
export function getBusinessLogicDeclParameters(parameters) {
const builder = new StringBuilder();
const blParameters = parameters.filter((p) => p.operationKind === "BusinessLogic" || p.operationKind === "All");
let i = 0;
for (const param of blParameters) {
builder.push(code `${getBusinessLogicSignatureParameter(param)}${++i < blParameters.length ? ", " : ""}`);
}
return builder.reduce();
}
export function getHttpDeclParameters(parameters) {
const builder = new StringBuilder();
const blParameters = parameters.filter((p) => p.operationKind === "Http" || p.operationKind === "All");
let i = 0;
for (const param of blParameters) {
builder.push(code `${getHttpSignatureParameter(param)}${++i < blParameters.length ? ", " : ""}`);
}
return builder.reduce();
}
export function getBusinessLogicCallParameter(param) {
const builder = new StringBuilder();
builder.push(code `${param.callName}`);
return builder.reduce();
}
export function getBusinessLogicSignatureParameter(param) {
const builder = new StringBuilder();
builder.push(code `${param.typeName}${param.optional || param.nullable ? "? " : " "}${param.name}`);
return builder.reduce();
}
export function getHttpSignatureParameter(param) {
const builder = new StringBuilder();
builder.push(code `${getHttpParameterDecorator(param)}${getBusinessLogicSignatureParameter(param)}${param.defaultValue === undefined ? "" : code ` = ${typeof param.defaultValue === "boolean" ? code `${param.defaultValue.toString()}` : code `${param.defaultValue}`}`}`);
return builder.reduce();
}
export function getHttpParameterDecorator(parameter) {
switch (parameter.httpParameterKind) {
case "query":
return code `[FromQuery${parameter.httpParameterName ? code `(Name="${parameter.httpParameterName}")` : ""}] `;
case "header":
return code `[FromHeader${parameter.httpParameterName ? code `(Name="${parameter.httpParameterName}")` : ""}] `;
default:
return "";
}
}
export function getParameterKind(parameter) {
switch (parameter.type) {
case "path":
return "path";
case "cookie":
case "header":
return "header";
case "query":
return "query";
}
}
export function canHaveDefault(program, type) {
switch (type.kind) {
case "Boolean":
case "EnumMember":
case "Enum":
case "Number":
case "String":
case "Scalar":
case "StringTemplate":
return true;
case "ModelProperty":
return canHaveDefault(program, type.type);
default:
return false;
}
}
export class CSharpOperationHelpers {
constructor(inEmitter) {
this.emitter = inEmitter;
this.#anonymousModels = new Map();
this.#opCache = new Map();
}
emitter;
#anonymousModels;
#opCache;
getResponse(program, operation) {
return new CSharpType({
name: "void",
namespace: "System",
isBuiltIn: true,
isValueType: true,
});
}
getParameters(program, operation) {
function safeConcat(...names) {
return names
.filter((n) => n !== undefined && n !== null && n.length > 0)
.flatMap((s) => getCSharpIdentifier(s, NameCasingType.Class))
.join();
}
const cached = this.#opCache.get(operation.operation);
if (cached)
return cached;
const bodyParam = operation.parameters.body;
const isExplicitBodyParam = bodyParam?.property !== undefined;
const result = [];
if (!cached && operation.verb === "get" && operation.parameters.body !== undefined) {
reportDiagnostic(program, {
code: "get-request-body",
target: operation.operation,
format: {},
});
this.#opCache.set(operation.operation, result);
return result;
}
const validParams = operation.parameters.parameters.filter((p) => isValidParameter(program, p.param));
const requiredParams = validParams.filter((p) => p.type === "path" || (!p.param.optional && p.param.defaultValue === undefined));
const optionalParams = validParams.filter((p) => p.type !== "path" && (p.param.optional || p.param.defaultValue !== undefined));
for (const parameter of requiredParams) {
let { typeReference: paramType, defaultValue: paramValue } = this.getTypeInfo(program, parameter.param.type);
// cSharp does not allow array defaults in operation parameters
if (!canHaveDefault(program, parameter.param)) {
paramValue = undefined;
}
const paramName = ensureCSharpIdentifier(program, parameter.param, parameter.param.name, NameCasingType.Parameter);
result.push({
isExplicitBody: false,
name: paramName,
callName: paramName,
optional: false,
typeName: paramType,
defaultValue: paramValue,
httpParameterKind: getParameterKind(parameter),
httpParameterName: parameter.name,
nullable: false,
operationKind: "All",
});
}
const overrideParameters = getExplicitBodyParameters(program, operation);
if (overrideParameters !== undefined) {
for (const overrideParam of overrideParameters) {
result.push(overrideParam);
}
}
else if (bodyParam !== undefined && isExplicitBodyParam) {
let { typeReference: bodyType, defaultValue: bodyValue, nullableType: isNullable, } = this.getTypeInfo(program, bodyParam.type);
if (!canHaveDefault(program, bodyParam.type)) {
bodyValue = undefined;
}
result.push({
isExplicitBody: true,
httpParameterKind: "body",
name: "body",
callName: "body",
typeName: bodyType,
nullable: isNullable,
defaultValue: bodyValue,
optional: bodyParam.property?.optional ?? false,
operationKind: "All",
});
}
else if (bodyParam !== undefined) {
switch (bodyParam.type.kind) {
case "Model":
let tsBody = bodyParam.type;
if (!bodyParam.type.name) {
tsBody = program.checker.cloneType(bodyParam.type, {
name: safeConcat(operation.operation.interface?.name, operation.operation.name, "Request"),
});
}
const { typeReference: bodyType } = this.getTypeInfo(program, tsBody);
const bodyName = ensureCSharpIdentifier(program, bodyParam.type, "body", NameCasingType.Parameter);
result.push({
isExplicitBody: false,
httpParameterKind: "body",
name: bodyName,
callName: bodyName,
typeName: bodyType,
nullable: false,
defaultValue: undefined,
optional: false,
operationKind: "Http",
});
for (const [propName, propDef] of bodyParam.type.properties) {
let { typeReference: csType, defaultValue: csValue, nullableType: isNullable, } = this.getTypeInfo(program, propDef.type, propDef);
// cSharp does not allow array defaults in operation parameters
if (!canHaveDefault(program, propDef)) {
csValue = undefined;
}
const paramName = ensureCSharpIdentifier(program, propDef, propName, NameCasingType.Parameter);
const refName = ensureCSharpIdentifier(program, propDef, propName, NameCasingType.Property);
result.push({
isExplicitBody: false,
httpParameterKind: "body",
name: paramName,
callName: `body.${refName}`,
typeName: csType,
nullable: isNullable,
defaultValue: csValue,
optional: propDef.optional,
operationKind: "BusinessLogic",
});
}
break;
case "ModelProperty":
{
let { typeReference: csType, defaultValue: csValue, nullableType: isNullable, } = this.getTypeInfo(program, bodyParam.type.type, bodyParam.type);
if (!canHaveDefault(program, bodyParam.type)) {
csValue = undefined;
}
const optName = ensureCSharpIdentifier(program, bodyParam.type.type, bodyParam.type.name, NameCasingType.Parameter);
result.push({
isExplicitBody: true,
httpParameterKind: "body",
name: optName,
callName: optName,
typeName: csType,
nullable: isNullable,
defaultValue: csValue,
optional: bodyParam.type.optional,
operationKind: "All",
});
}
break;
default: {
let { typeReference: csType, defaultValue: csValue, nullableType: isNullable, } = this.getTypeInfo(program, bodyParam.type);
if (!canHaveDefault(program, bodyParam.type)) {
csValue = undefined;
}
result.push({
isExplicitBody: true,
httpParameterKind: "body",
name: "body",
callName: "body",
typeName: csType,
nullable: isNullable,
defaultValue: csValue,
optional: false,
operationKind: "All",
});
}
}
}
for (const parameter of optionalParams) {
const { typeReference: paramType, defaultValue: paramValue, nullableType: isNullable, } = this.getTypeInfo(program, parameter.param.type, parameter.param);
const optName = ensureCSharpIdentifier(program, parameter.param, parameter.param.name, NameCasingType.Parameter);
result.push({
isExplicitBody: false,
name: optName,
callName: optName,
optional: true,
typeName: paramType,
defaultValue: paramValue,
httpParameterKind: getParameterKind(parameter),
httpParameterName: parameter.name,
nullable: isNullable,
operationKind: "All",
});
}
return result;
}
getDefaultValue(program, tsType, defaultValue) {
if (defaultValue === undefined)
return undefined;
switch (tsType.kind) {
case "Enum":
if (defaultValue.valueKind === "EnumValue") {
const retVal = this.getTypeInfo(program, tsType);
return `${retVal.typeReference}.${ensureCSharpIdentifier(program, defaultValue.value, defaultValue.value.name, NameCasingType.Property)}`;
}
return JSON.stringify(serializeValueAsJson(this.emitter.getProgram(), defaultValue, tsType));
case "Union":
const { typeReference: typeRef } = this.getUnionInfo(program, tsType);
if (defaultValue.valueKind === "StringValue" && isStringEnumType(program, tsType)) {
const matches = [...tsType.variants].filter((v) => typeof v[0] === "string" &&
v[1].type.kind === "String" &&
v[1].type.value === defaultValue.value);
if (matches.length === 1) {
return `${typeRef}.${ensureCSharpIdentifier(program, matches[0][1], matches[0][0], NameCasingType.Property)}`;
}
return undefined;
}
if (defaultValue.valueKind === "StringValue") {
return JSON.stringify(serializeValueAsJson(this.emitter.getProgram(), defaultValue, tsType));
}
return undefined;
default:
return JSON.stringify(serializeValueAsJson(this.emitter.getProgram(), defaultValue, tsType));
}
}
getTypeInfo(program, tsType, modelProperty) {
const myEmitter = this.emitter;
function extractStringValue(type, span) {
switch (type.kind) {
case "String":
return type.value;
case "Boolean":
return `${type.value}`;
case "Number":
return type.valueAsString;
case "StringTemplateSpan":
if (type.isInterpolated) {
return extractStringValue(type.type, span);
}
else {
return type.type.value;
}
case "ModelProperty":
return extractStringValue(type.type, span);
case "EnumMember":
if (type.value === undefined)
return type.name;
if (typeof type.value === "string")
return type.value;
if (typeof type.value === "number")
return `${type.value}`;
}
reportDiagnostic(myEmitter.getProgram(), {
code: "invalid-interpolation",
target: span,
format: {},
});
return "";
}
switch (tsType.kind) {
case "String":
return {
typeReference: code `string`,
defaultValue: `"${tsType.value}"`,
nullableType: false,
};
case "StringTemplate":
const template = tsType;
if (template.stringValue !== undefined)
return {
typeReference: code `string`,
defaultValue: `"${template.stringValue}"`,
nullableType: false,
};
const spanResults = [];
for (const span of template.spans) {
spanResults.push(extractStringValue(span, span));
}
return {
typeReference: code `string`,
defaultValue: `"${spanResults.join("")}"`,
nullableType: false,
};
case "Boolean":
return {
typeReference: code `bool`,
defaultValue: `${tsType.value === true ? true : false}`,
nullableType: false,
};
case "Number":
const [type, value] = findNumericType(tsType);
return { typeReference: code `${type}`, defaultValue: `${value}`, nullableType: false };
case "Tuple":
const defaults = [];
const [csharpType, isObject] = coalesceTsTypes(program, tsType.values);
if (isObject)
return { typeReference: "object[]", defaultValue: undefined, nullableType: false };
for (const value of tsType.values) {
const { defaultValue: itemDefault } = this.getTypeInfo(program, value);
defaults.push(itemDefault);
}
const collectionType = CSharpServiceOptions.getInstance().collectionType;
switch (collectionType) {
case CollectionType.IEnumerable:
return {
typeReference: code `IEnumerable<${csharpType.getTypeReference(myEmitter.getContext()?.scope)}>`,
defaultValue: `new List<${csharpType.getTypeReference(myEmitter.getContext()?.scope)}> {${defaults.join(", ")}}`,
nullableType: csharpType.isNullable,
};
case CollectionType.Array:
default:
return {
typeReference: code `${csharpType.getTypeReference(myEmitter.getContext()?.scope)}[]`,
defaultValue: `[${defaults.join(", ")}]`,
nullableType: csharpType.isNullable,
};
}
case "Model":
let modelResult;
const hasUniqueItems = modelProperty
? getUniqueItems(program, modelProperty) !== undefined
: false;
const cachedResult = this.#anonymousModels.get(tsType);
if (cachedResult && cachedResult.hasUniqueItems === hasUniqueItems) {
return cachedResult;
}
if (isRecord(tsType)) {
modelResult = {
typeReference: code `${RecordType.getTypeReference(myEmitter.getContext().scope)}`,
nullableType: false,
hasUniqueItems: hasUniqueItems,
};
}
else if (isArrayModelType(program, tsType)) {
const typeReference = code `${this.emitter.emitTypeReference(tsType.indexer.value)}`;
modelResult = isByteType(tsType.indexer.value)
? {
typeReference: code `${typeReference}[]`,
nullableType: false,
hasUniqueItems: hasUniqueItems,
}
: {
typeReference: hasUniqueItems
? code `ISet<${typeReference}>`
: code `${this.emitter.emitTypeReference(tsType)}`,
nullableType: false,
hasUniqueItems: hasUniqueItems,
};
}
else {
modelResult = {
typeReference: code `${this.emitter.emitTypeReference(ts