UNPKG

@typespec/http-server-csharp

Version:

TypeSpec service code generator for c-sharp

1,301 lines 59.9 kB
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(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(tsType, thi