Skip to content

Commit

Permalink
refinement to the type cache in dotnet emitter (#4204)
Browse files Browse the repository at this point in the history
This PR introduces a global cache for types when we are doing the type
conversion from TCGC's type to our input types in the emitter.
  • Loading branch information
ArcturusZhang authored Aug 21, 2024
1 parent 8d80847 commit e8c493e
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 171 deletions.
24 changes: 14 additions & 10 deletions packages/http-client-csharp/emitter/src/lib/client-model-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
SdkEndpointType,
SdkHttpOperation,
SdkServiceMethod,
SdkType,
UsageFlags,
getAllModels,
} from "@azure-tools/typespec-client-generator-core";
Expand All @@ -19,6 +20,7 @@ import { InputOperationParameterKind } from "../type/input-operation-parameter-k
import { InputParameter } from "../type/input-parameter.js";
import { InputEnumType, InputModelType, InputType } from "../type/input-type.js";
import { RequestLocation } from "../type/request-location.js";
import { SdkTypeMap } from "../type/sdk-type-map.js";
import { fromSdkType } from "./converter.js";
import { Logger } from "./logger.js";
import { navigateModels } from "./model.js";
Expand All @@ -31,10 +33,13 @@ export function createModel(sdkContext: SdkContext<NetEmitterOptions>): CodeMode

const sdkPackage = sdkContext.sdkPackage;

const modelMap = new Map<string, InputModelType>();
const enumMap = new Map<string, InputEnumType>();
const sdkTypeMap: SdkTypeMap = {
types: new Map<SdkType, InputType>(),
models: new Map<string, InputModelType>(),
enums: new Map<string, InputEnumType>(),
};

navigateModels(sdkContext, modelMap, enumMap);
navigateModels(sdkContext, sdkTypeMap);

const sdkApiVersionEnums = sdkPackage.enums.filter((e) => e.usage === UsageFlags.ApiVersionEnum);

Expand All @@ -50,14 +55,14 @@ export function createModel(sdkContext: SdkContext<NetEmitterOptions>): CodeMode
[]
);

const clientModel = {
const clientModel: CodeModel = {
Name: sdkPackage.rootNamespace,
ApiVersions: rootApiVersions,
Enums: Array.from(enumMap.values()),
Models: Array.from(modelMap.values()),
Enums: Array.from(sdkTypeMap.enums.values()),
Models: Array.from(sdkTypeMap.models.values()),
Clients: inputClients,
Auth: processServiceAuthentication(sdkPackage),
} as CodeModel;
};
return clientModel;

function fromSdkClients(
Expand Down Expand Up @@ -95,8 +100,7 @@ export function createModel(sdkContext: SdkContext<NetEmitterOptions>): CodeMode
clientParameters,
rootApiVersions,
sdkContext,
modelMap,
enumMap
sdkTypeMap
)
),
Protocol: {},
Expand Down Expand Up @@ -155,7 +159,7 @@ export function createModel(sdkContext: SdkContext<NetEmitterOptions>): CodeMode
Name: "url",
CrossLanguageDefinitionId: "TypeSpec.url",
}
: fromSdkType(parameter.type, sdkContext, modelMap, enumMap); // TODO: consolidate with converter.fromSdkEndpointType
: fromSdkType(parameter.type, sdkContext, sdkTypeMap); // TODO: consolidate with converter.fromSdkEndpointType
parameters.push({
Name: parameter.name,
NameInRequest: parameter.serializedName,
Expand Down
150 changes: 87 additions & 63 deletions packages/http-client-csharp/emitter/src/lib/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,56 +31,86 @@ import {
InputEnumType,
InputLiteralType,
InputModelType,
InputNullableType,
InputPrimitiveType,
InputType,
InputUnionType,
} from "../type/input-type.js";
import { LiteralTypeContext } from "../type/literal-type-context.js";
import { SdkTypeMap } from "../type/sdk-type-map.js";

export function fromSdkType(
sdkType: SdkType,
context: SdkContext,
models: Map<string, InputModelType>,
enums: Map<string, InputEnumType>,
typeMap: SdkTypeMap,
literalTypeContext?: LiteralTypeContext
): InputType {
if (sdkType.kind === "nullable") {
const inputType = fromSdkType(sdkType.type, context, models, enums);
return {
Kind: "nullable",
Type: inputType,
} as InputNullableType;
if (typeMap.types.has(sdkType)) {
return typeMap.types.get(sdkType)!;
}
if (sdkType.kind === "model") return fromSdkModelType(sdkType, context, models, enums);
if (sdkType.kind === "endpoint") return fromSdkEndpointType();
if (sdkType.kind === "enum") return fromSdkEnumType(sdkType, context, enums);
if (sdkType.kind === "enumvalue")
return fromSdkEnumValueTypeToConstantType(sdkType, context, enums, literalTypeContext);
if (sdkType.kind === "dict") return fromSdkDictionaryType(sdkType, context, models, enums);
if (sdkType.kind === "array") return fromSdkArrayType(sdkType, context, models, enums);
if (sdkType.kind === "constant")
return fromSdkConstantType(sdkType, context, models, enums, literalTypeContext);
if (sdkType.kind === "union") return fromUnionType(sdkType, context, models, enums);
if (sdkType.kind === "utcDateTime" || sdkType.kind === "offsetDateTime")
return fromSdkDateTimeType(sdkType);
if (sdkType.kind === "duration") return fromSdkDurationType(sdkType);
if (sdkType.kind === "tuple") return fromTupleType(sdkType);
// TODO -- endpoint and credential are handled separately in emitter, since we have specific locations for them in input model.
// We can handle unify the way we handle them in the future, probably by chaning the input model schema and do the conversion in generator.
if (sdkType.kind === "credential") throw new Error("Credential type is not supported yet.");

return fromSdkBuiltInType(sdkType);

let retVar: InputType;
switch (sdkType.kind) {
case "nullable":
const inputType = fromSdkType(sdkType.type, context, typeMap);
retVar = {
Kind: "nullable",
Type: inputType,
};
break;
case "model":
retVar = fromSdkModelType(sdkType, context, typeMap);
break;
case "enum":
retVar = fromSdkEnumType(sdkType, context, typeMap);
break;
case "enumvalue":
retVar = fromSdkEnumValueTypeToConstantType(sdkType, context, typeMap, literalTypeContext);
break;
case "dict":
retVar = fromSdkDictionaryType(sdkType, context, typeMap);
break;
case "array":
retVar = fromSdkArrayType(sdkType, context, typeMap);
break;
case "constant":
retVar = fromSdkConstantType(sdkType, typeMap, literalTypeContext);
break;
case "union":
retVar = fromUnionType(sdkType, context, typeMap);
break;
case "utcDateTime":
case "offsetDateTime":
retVar = fromSdkDateTimeType(sdkType);
break;
case "duration":
retVar = fromSdkDurationType(sdkType);
break;
case "tuple":
retVar = fromTupleType(sdkType);
break;
// TODO -- endpoint and credential are handled separately in emitter, since we have specific locations for them in input model.
// We can handle unify the way we handle them in the future, probably by chaning the input model schema and do the conversion in generator.
case "endpoint":
retVar = fromSdkEndpointType();
break;
case "credential":
throw new Error("Credential type is not supported yet.");
default:
retVar = fromSdkBuiltInType(sdkType);
break;
}

typeMap.types.set(sdkType, retVar);
return retVar;
}

export function fromSdkModelType(
modelType: SdkModelType,
context: SdkContext,
models: Map<string, InputModelType>,
enums: Map<string, InputEnumType>
typeMap: SdkTypeMap
): InputModelType {
const modelTypeName = modelType.name;
let inputModelType = models.get(modelTypeName);
let inputModelType = typeMap.models.get(modelTypeName);
if (!inputModelType) {
inputModelType = {
Kind: "model",
Expand All @@ -97,10 +127,10 @@ export function fromSdkModelType(
Decorators: modelType.decorators,
} as InputModelType;

models.set(modelTypeName, inputModelType);
typeMap.models.set(modelTypeName, inputModelType);

inputModelType.AdditionalProperties = modelType.additionalProperties
? fromSdkType(modelType.additionalProperties, context, models, enums)
? fromSdkType(modelType.additionalProperties, context, typeMap)
: undefined;

const propertiesDict = new Map<SdkModelPropertyType, InputModelProperty[]>();
Expand All @@ -124,7 +154,7 @@ export function fromSdkModelType(
: undefined;

inputModelType.BaseModel = modelType.baseModel
? fromSdkModelType(modelType.baseModel, context, models, enums)
? fromSdkModelType(modelType.baseModel, context, typeMap)
: undefined;

inputModelType.Properties = Array.from(propertiesDict.values()).flat();
Expand All @@ -133,7 +163,7 @@ export function fromSdkModelType(
const discriminatedSubtypes: Record<string, InputModelType> = {};
for (const key in modelType.discriminatedSubtypes) {
const subtype = modelType.discriminatedSubtypes[key];
discriminatedSubtypes[key] = fromSdkModelType(subtype, context, models, enums);
discriminatedSubtypes[key] = fromSdkModelType(subtype, context, typeMap);
}
inputModelType.DiscriminatedSubtypes = discriminatedSubtypes;
}
Expand All @@ -160,8 +190,7 @@ export function fromSdkModelType(
Type: fromSdkType(
property.type,
context,
models,
enums,
typeMap,
isDiscriminator ? undefined : literalTypeContext // this is a workaround because the type of discriminator property in derived models is always literal and we wrap literal into enums, which leads to a lot of extra enum types, adding this check to avoid them
),
IsRequired: isRequired,
Expand Down Expand Up @@ -194,13 +223,13 @@ export function fromSdkModelType(
export function fromSdkEnumType(
enumType: SdkEnumType,
context: SdkContext,
enums: Map<string, InputEnumType>,
typeMap: SdkTypeMap,
addToCollection: boolean = true
): InputEnumType {
const enumName = enumType.name;
let inputEnumType = enums.get(enumName);
if (inputEnumType === undefined) {
const newInputEnumType: InputEnumType = {
let inputEnumType = typeMap.enums.get(enumName);
if (!inputEnumType) {
inputEnumType = {
Kind: "enum",
Name: enumName,
CrossLanguageDefinitionId: enumType.crossLanguageDefinitionId,
Expand All @@ -216,9 +245,9 @@ export function fromSdkEnumType(
Usage: enumType.usage,
Decorators: enumType.decorators,
};
if (addToCollection) enums.set(enumName, newInputEnumType);
inputEnumType = newInputEnumType;
if (addToCollection) typeMap.enums.set(enumName, inputEnumType);
}

return inputEnumType;
}

Expand Down Expand Up @@ -247,7 +276,7 @@ function fromSdkDurationType(durationType: SdkDurationType): InputDurationType {
}

// TODO: tuple is not officially supported
function fromTupleType(tupleType: SdkTupleType): InputPrimitiveType {
function fromTupleType(tupleType: SdkTupleType): InputType {
return {
Kind: "any",
Name: "tuple",
Expand All @@ -270,12 +299,11 @@ function fromSdkBuiltInType(builtInType: SdkBuiltInType): InputPrimitiveType {
function fromUnionType(
union: SdkUnionType,
context: SdkContext,
models: Map<string, InputModelType>,
enums: Map<string, InputEnumType>
typeMap: SdkTypeMap
): InputUnionType {
const variantTypes: InputType[] = [];
for (const value of union.values) {
const variantType = fromSdkType(value, context, models, enums);
const variantType = fromSdkType(value, context, typeMap);
variantTypes.push(variantType);
}

Expand All @@ -289,9 +317,7 @@ function fromUnionType(

function fromSdkConstantType(
constantType: SdkConstantType,
context: SdkContext,
models: Map<string, InputModelType>,
enums: Map<string, InputEnumType>,
typeMap: SdkTypeMap,
literalTypeContext?: LiteralTypeContext
): InputLiteralType {
return {
Expand All @@ -301,14 +327,13 @@ function fromSdkConstantType(
? fromSdkBuiltInType(constantType.valueType)
: // TODO: this might change in the near future
// we might keep constant as-is, instead of creating an enum for it.
convertConstantToEnum(constantType, enums, literalTypeContext),
convertConstantToEnum(constantType, literalTypeContext),
Value: constantType.value,
Decorators: constantType.decorators,
};

function convertConstantToEnum(
constantType: SdkConstantType,
enums: Map<string, InputEnumType>,
literalTypeContext: LiteralTypeContext
) {
// otherwise we need to wrap this into an extensible enum
Expand All @@ -335,23 +360,24 @@ function fromSdkConstantType(
Usage: literalTypeContext.Usage,
Decorators: constantType.decorators,
};
enums.set(enumName, enumType);

typeMap.enums.set(enumName, enumType);
return enumType;
}
}

function fromSdkEnumValueTypeToConstantType(
enumValueType: SdkEnumValueType,
context: SdkContext,
enums: Map<string, InputEnumType>,
typeMap: SdkTypeMap,
literalTypeContext?: LiteralTypeContext
): InputLiteralType {
return {
Kind: "constant",
ValueType:
enumValueType.valueType.kind === "boolean" || literalTypeContext === undefined
? fromSdkBuiltInType(enumValueType.valueType as SdkBuiltInType) // TODO: TCGC fix
: fromSdkEnumType(enumValueType.enumType, context, enums),
? fromSdkBuiltInType(enumValueType.valueType)
: fromSdkEnumType(enumValueType.enumType, context, typeMap),
Value: enumValueType.value,
Decorators: enumValueType.decorators,
};
Expand All @@ -369,27 +395,25 @@ function fromSdkEnumValueType(enumValueType: SdkEnumValueType): InputEnumTypeVal
function fromSdkDictionaryType(
dictionaryType: SdkDictionaryType,
context: SdkContext,
models: Map<string, InputModelType>,
enums: Map<string, InputEnumType>
typeMap: SdkTypeMap
): InputDictionaryType {
return {
Kind: "dict",
KeyType: fromSdkType(dictionaryType.keyType, context, models, enums),
ValueType: fromSdkType(dictionaryType.valueType, context, models, enums),
KeyType: fromSdkType(dictionaryType.keyType, context, typeMap),
ValueType: fromSdkType(dictionaryType.valueType, context, typeMap),
Decorators: dictionaryType.decorators,
};
}

function fromSdkArrayType(
arrayType: SdkArrayType,
context: SdkContext,
models: Map<string, InputModelType>,
enums: Map<string, InputEnumType>
typeMap: SdkTypeMap
): InputArrayType {
return {
Kind: "array",
Name: arrayType.name,
ValueType: fromSdkType(arrayType.valueType, context, models, enums),
ValueType: fromSdkType(arrayType.valueType, context, typeMap),
CrossLanguageDefinitionId: arrayType.crossLanguageDefinitionId,
Decorators: arrayType.decorators,
};
Expand Down
Loading

0 comments on commit e8c493e

Please sign in to comment.