@typespec/http-server-csharp
Version:
TypeSpec service code generator for c-sharp
1,271 lines • 50.1 kB
JavaScript
import { StringBuilder, code } from "@typespec/asset-emitter";
import { NoTarget, getFriendlyName, getMinValue, isErrorModel, isNullType, isNumericType, isTemplateInstance, isUnknownType, isVoidType, resolveCompilerOptions, resolvePath, } from "@typespec/compiler";
import { Visibility, createMetadataInfo, getHeaderFieldName, isBody, isBodyRoot, isHeader, isMetadata, isPathParam, isQueryParam, isStatusCode, } from "@typespec/http";
import { camelCase, pascalCase } from "change-case";
import { createServer } from "net";
import { getAttributes } from "./attributes.js";
import { BooleanValue, CSharpType, NameCasingType, NullValue, NumericValue, StringValue, } from "./interfaces.js";
import { 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",
isValueType: false,
isBuiltIn: 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 || "Models",
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}.Models`,
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;
return {
type: new CSharpType({
name: `${itemType.name}[]`,
namespace: itemType.namespace,
isBuiltIn: itemType.isBuiltIn,
isValueType: false,
isClass: itemType.isClass,
isCollection: true,
}),
};
}
if (isRecord(type))
return {
type: new CSharpType({
name: "JsonObject",
namespace: "System.Text.Json.Nodes",
isBuiltIn: false,
isValueType: false,
isClass: false,
}),
};
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}.Models`,
isBuiltIn: false,
isValueType: false,
isClass: true,
}),
};
default:
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) {
if (name === undefined)
return "Placeholder";
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 = "";
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;
const nsName = new StringBuilder();
for (const part of name.split(".")) {
if (!isValidCSharpIdentifier(part)) {
invalid = true;
nsName.pushLiteralSegment(transformInvalidIdentifier(part));
}
}
if (invalid) {
reportDiagnostic(program, {
code: "invalid-identifier",
format: { identifier: name, location: location },
target: target.node ?? NoTarget,
});
return nsName.segments.join(".");
}
return name;
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)) {
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.checker.voidType;
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) {
return identifier?.match(/^[A-Za-z_][\w]*$/) !== null;
}
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;
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 (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);
// 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);
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);
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",
});
}
this.#opCache.set(operation.operation, result);
return result;
}
getTypeInfo(program, tsType) {
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);
}
return {
typeReference: code `${csharpType.getTypeReference()}[]`,
defaultValue: `[${defaults.join(", ")}]`,
nullableType: csharpType.isNullable,
};
case "Model":
let modelResult;
const cachedResult = this.#anonymousModels.get(tsType);
if (cachedResult) {
return cachedResult;
}
if (isRecord(tsType)) {
modelResult = {
typeReference: code `System.Text.Json.Nodes.JsonObject`,
nullableType: false,
};
}
else {
modelResult = {
typeReference: code `${this.emitter.emitTypeReference(tsType)}`,
nullableType: false,
};
}
this.#anonymousModels.set(tsType, modelResult);
return modelResult;
case "ModelProperty":
return this.getTypeInfo(program, tsType.type);
case "Enum":
if (getEnumType(tsType) === "double")
return { typeReference: getDoubleType().getTypeReference(), nullableType: false };
return {
typeReference: code `${this.emitter.emitTypeReference(tsType)}`,
nullableType: false,
};
case "EnumMember":
if (typeof tsType.value === "number") {
const stringValue = tsType.value.toString();
if (stringValue.includes(".") || stringValue.includes("e"))
return { typeReference: "double", defaultValue: stringValue, nullableType: false };
return { typeReference: "int", defaultValue: stringValue, nullableType: false };
}
if (typeof tsType.value === "string") {
return { typeReference: "string", defaultValue: tsType.value, nullableType: false };
}
return { typeReference: code `object`, nullableType: false };
case "Union":
return this.getUnionInfo(program, tsType);
case "UnionVariant":
return this.getTypeInfo(program, tsType.type);
default:
return {
typeReference: code `${this.emitter.emitTypeReference(tsType)}`,
nullableType: false,
};
}
}
getUnionInfo(program, union) {
const propResult = getNonNullableTsType(union);
if (propResult === undefined) {
return {
typeReference: code `${this.emitter.emitTypeReference(union)}`,
nullableType: [...union.variants.values()].some((v) => isNullType(v.type)),
};
}
const candidate = this.getTypeInfo(program, propResult.type);
candidate.nullableType = propResult.nullable;
return candidate;
}
}
export function getExplicitBodyParameters(program, httpOperation) {
if (httpOperation.parameters.body && httpOperation.parameters.body.bodyKind === "multipart") {
return [
{
name: "reader",
callName: "reader",
nullable: false,
optional: false,
typeName: "MultipartReader",
isExplicitBody: false,
httpParameterKind: "body",
operationKind: "BusinessLogic",
},
];
}
return undefined;
}
export function findNumericType(type) {
const stringValue = type.valueAsString;
if (stringValue.includes(".") || stringValue.includes("e"))
return ["double", stringValue];
return ["int", stringValue];
}
export function coalesceUnionTypes(program, union) {
const [result, _] = coalesceTsTypes(program, [...union.variants.values()].flatMap((v) => v.type));
return result;
}
export function getNonNullableTsType(union) {
const types = [...union.variants.values()];
const nulls = types.flatMap((v) => v.type).filter((t) => isNullType(t));
const nonNulls = types.flatMap((v) => v.type).filter((t) => !isNullType(t));
if (nonNulls.length === 1)
return { type: nonNulls[0], nullable: nulls.length > 0 };
return undefined;
}
export function coalesceTsTypes(program, types) {
const defaultValue = [
new CSharpType({
name: "object",
namespace: "System",
isBuiltIn: true,
isValueType: false,
}),
true,
];
let current = undefined;
let nullable = false;
for (const type of types) {
let candidate = undefined;
switch (type.kind) {
case "Boolean":
candidate = new CSharpType({ name: "bool", namespace: "System", isValueType: true });
break;
case "StringTemplate":
case "String":
candidate = new CSharpType({ name: "string", namespace: "System", isValueType: false });
break;
case "Number":
const stringValue = type.valueAsString;
if (stringValue.includes(".") || stringValue.includes("e")) {
candidate = new CSharpType({
name: "double",
namespace: "System",
isValueType: true,
});
}
else {
candidate = new CSharpType({ name: "int", namespace: "System", isValueType: true });
}
break;
case "Union":
candidate = coalesceUnionTypes(program, type);
break;
case "Scalar":
candidate = getCSharpTypeForScalar(program, type);
break;
case "Intrinsic":
if (isNullType(type)) {
nullable = true;
candidate = current;
}
else {
return defaultValue;
}
break;
default:
return defaultValue;
}
current = current ?? candidate;
if (current === undefined || (candidate !== undefined && !candidate.equals(current)))
return defaultValue;
}
if (current !== undefined && nullable === true)
current.isNullable = true;
return current === undefined ? defaultValue : [current, false];
}
export function isRecord(type) {
return type.kind === "Model" && type.name === "Record" && type.indexer !== undefined;
}
export async function getFreePort(minPort, maxPort, tries = 100) {
const min = Math.floor(minPort);
const max = Math.floor(maxPort);
if (tries === 0)
return min;
const diff = Math.abs(max - min);
const port = min + Math.floor(Math.random() * diff);
const server = createServer();
const free = await checkPort(port);
if (free) {
return port;
}
return await getFreePort(min, max, tries--);
async function checkPort(port, timeout = 100) {
return new Promise((resolve, _) => {
server.on("error", (_) => {
server.close();
resolve(false);
});
server.listen(port, async () => {
try {
setTimeout(() => resolve(true), timeout);
}
catch (e) {
resolve(false);
}
finally {
server.close();
}
});
});
}
}
export async function getOpenApiConfig(program) {
const root = program.projectRoot;
const [options, _] = await resolveCompilerOptions(program.host, {
cwd: root,
entrypoint: resolvePath(root, "main.tsp"),
});
const oaiOptions = options.options !== undefined && Object.keys(options.options).includes("@typespec/openapi3")
? options.options["@typespec/openapi3"]
: undefined;
return {
emitted: options.emit !== undefined && options.emit.includes("@typespec/openapi3"),
outputDir: oaiOptions?.["emitter-output-dir"],
fileName: oaiOptions?.["output-file"],
options: oaiOptions,
};
}
export function getStatusCode(program, model) {
const statusCodeProperty = new ModelInfo().filterAllProperties(program, model, (p) => isStatusCode(program, p));
if (!statusCodeProperty)
return undefined;
const { type } = statusCodeProperty;
switch (type.kind) {
case "Union":
return {
name: statusCodeProperty.name,
value: statusCodeProperty.name,
requiresConstructorArgument: true,
};
case "Number":
return {
value: type.value,
};
default