* Introduce SdkType context

* Add changes

* remove unrelated changes

* Cleanup and test

* Fix conflicts

* Remove unused reify

* address comments

* fix smoke test

---------

Co-authored-by: qiaozha <qiaozha@microsoft.com>
This commit is contained in:
Jose Manuel Heredia Hidalgo 2024-07-17 03:20:16 -07:00 коммит произвёл GitHub
Родитель 9047f97ea8
Коммит 1de92fdbe7
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
27 изменённых файлов: 396 добавлений и 3673 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -26,4 +26,5 @@ dist-esm
dist
/artifacts
.tshy-build
vitest.config.ts.timestamp*
.tshy

Просмотреть файл

@ -19,7 +19,7 @@
"test-next:coverage": "vitest run ./test-next --coverage",
"clean": "rimraf ./dist ./typespec-output",
"build": "tsc -p .",
"test": "npm run unit-test && npm run integration-test-ci",
"test": "npm run test-next && npm run unit-test && npm run integration-test-ci",
"lint": "eslint src --ext .ts --max-warnings=0",
"lint:fix": "eslint src --fix --ext .ts",
"format": "npm run -s prettier -- --write",
@ -139,4 +139,4 @@
"url": "https://github.com/Azure/autorest.typescript/issues"
},
"homepage": "https://github.com/Azure/autorest.typescript/tree/main/packages/typespec-ts/"
}
}

Просмотреть файл

@ -2,6 +2,7 @@ import { Project, SourceFile } from "ts-morph";
import { ModularMetaTree, RlcMetaTree } from "./metaTree.js";
import { EmitContext } from "@typespec/compiler";
import { SdkContext } from "@azure-tools/typespec-client-generator-core";
import { SdkTypeContext } from "./framework/hooks/sdkTypes.js";
/**
* Contexts Object Guidelines
@ -21,6 +22,7 @@ type Contexts = {
modularMetaTree: ModularMetaTree; // Context for modular types metadata.
outputProject: Project; // The TS-Morph root project context for code generation.
symbolMap: Map<string, SourceFile>; // Mapping of symbols to their corresponding source files.
sdkTypes: SdkTypeContext;
emitContext: {
compilerContext: EmitContext;
tcgcContext: SdkContext;

Просмотреть файл

@ -0,0 +1,101 @@
import {
SdkClientType,
SdkHttpOperation,
SdkPackage,
SdkServiceMethod,
SdkType,
getClientType
} from "@azure-tools/typespec-client-generator-core";
import { Operation, Type, getNamespaceFullName } from "@typespec/compiler";
import { provideContext, useContext } from "../../contextManager.js";
export interface SdkTypeContext {
operations: Map<Type, SdkServiceMethod<SdkHttpOperation>>;
types: Map<Type, SdkType>;
}
export function useSdkTypes() {
const sdkTypesContext = useContext("sdkTypes");
const { tcgcContext } = useContext("emitContext");
function getSdkType(type: Operation): SdkServiceMethod<SdkHttpOperation>;
function getSdkType(type: Type): SdkType;
function getSdkType(
type: Type | Operation
): SdkType | SdkServiceMethod<SdkHttpOperation> {
let sdkType: SdkType | SdkServiceMethod<SdkHttpOperation> | undefined;
if (type.kind === "Operation") {
sdkType = sdkTypesContext.operations.get(type);
} else {
sdkType =
sdkTypesContext.types.get(type) ?? getClientType(tcgcContext, type);
}
if (!sdkType) {
throw new Error(
`SdkType not found for type: ${type.kind} ${
"name" in type && typeof type.name == "string" ? type.name : ""
} ${
"namespace" in type && type.namespace
? ` in ${getNamespaceFullName(type.namespace)}`
: ""
}`
);
}
return sdkType;
}
return getSdkType;
}
export function provideSdkTypes(sdkPackage: SdkPackage<SdkHttpOperation>) {
const sdkTypesContext = {
operations: new Map<Type, SdkServiceMethod<SdkHttpOperation>>(),
types: new Map<Type, SdkType>()
};
for (const sdkEnum of sdkPackage.enums) {
if (!sdkEnum.__raw) {
continue;
}
sdkTypesContext.types.set(sdkEnum.__raw, sdkEnum);
}
for (const sdkModel of sdkPackage.models) {
if (!sdkModel.__raw) {
continue;
}
sdkTypesContext.types.set(sdkModel.__raw, sdkModel);
}
for (const client of sdkPackage.clients) {
for (const method of getAllOperationsFromClient(client)) {
if (!method.__raw) {
continue;
}
sdkTypesContext.operations.set(method.__raw, method);
}
}
provideContext("sdkTypes", sdkTypesContext);
}
function getAllOperationsFromClient(client: SdkClientType<SdkHttpOperation>) {
const methodQueue = [...client.methods];
const operations: SdkServiceMethod<SdkHttpOperation>[] = [];
while (methodQueue.length > 0) {
const method = methodQueue.pop()!;
if (method.kind === "clientaccessor") {
method.response.methods.forEach((m) => methodQueue.push(m));
} else {
operations.push(method);
}
}
return operations;
}

Просмотреть файл

@ -60,7 +60,6 @@ import { buildSerializeUtils } from "./modular/buildSerializeUtils.js";
import { buildSubpathIndexFile } from "./modular/buildSubpathIndex.js";
import { buildModels, buildModelsOptions } from "./modular/emitModels.js";
import { ModularCodeModel } from "./modular/modularCodeModel.js";
import { buildSerializers } from "./modular/serialization/index.js";
import { transformRLCModel } from "./transform/transform.js";
import { transformRLCOptions } from "./transform/transfromRLCOptions.js";
import { getRLCClients } from "./utils/clientUtils.js";
@ -68,6 +67,7 @@ import { emitContentByBuilder, emitModels } from "./utils/emitUtil.js";
import { GenerationDirDetail, SdkContext } from "./utils/interfaces.js";
import { provideContext, useContext } from "./contextManager.js";
import { emitSerializerHelpersFile } from "./modular/buildHelperSerializers.js";
import { provideSdkTypes } from "./framework/hooks/sdkTypes.js";
export * from "./lib.js";
@ -89,6 +89,7 @@ export async function $onEmit(context: EmitContext) {
compilerContext: context,
tcgcContext: dpgContext
});
provideSdkTypes(dpgContext.experimental_sdkPackage);
const rlcCodeModels: RLCModel[] = [];
let modularCodeModel: ModularCodeModel;
@ -220,15 +221,11 @@ export async function $onEmit(context: EmitContext) {
isMultiClients
);
// build operation files
const serializerMap = env["EXPERIMENTAL_TYPESPEC_TS_SERIALIZATION"]
? buildSerializers(dpgContext, modularCodeModel, subClient)
: undefined;
buildOperationFiles(
subClient,
dpgContext,
modularCodeModel,
hasClientUnexpectedHelper,
serializerMap
hasClientUnexpectedHelper
);
buildClientContext(subClient, dpgContext, modularCodeModel);
buildSubpathIndexFile(subClient, modularCodeModel, "models");

Просмотреть файл

@ -5,7 +5,6 @@ import {
NameType,
normalizeName
} from "@azure-tools/rlc-common";
import { env } from "process";
import { Project, SourceFile } from "ts-morph";
import { isRLCMultiEndpoint } from "../utils/clientUtils.js";
import { SdkContext } from "../utils/interfaces.js";
@ -22,11 +21,6 @@ import {
import { buildType } from "./helpers/typeHelpers.js";
import { OperationPathAndDeserDetails } from "./interfaces.js";
import { Client, ModularCodeModel, Operation } from "./modularCodeModel.js";
import {
getDeserializePrivateFunction as experimentalGetDeserializePrivateFunction,
getSendPrivateFunction as experimentalGetSendPrivateFunction
} from "./serialization/operationHelpers.js";
import { SerializerMap } from "./serialization/util.js";
import { addImportBySymbol } from "../utils/importHelper.js";
/**
* This function creates a file under /api for each operation group.
@ -37,8 +31,7 @@ export function buildOperationFiles(
client: Client,
dpgContext: SdkContext,
codeModel: ModularCodeModel,
needUnexpectedHelper: boolean = true,
serializerMap?: SerializerMap
needUnexpectedHelper: boolean = true
) {
const operationFiles = [];
const isMultiEndpoint = isRLCMultiEndpoint(dpgContext);
@ -143,39 +136,18 @@ export function buildOperationFiles(
}
operationGroup.operations.forEach((o) => {
const operationDeclaration = getOperationFunction(o, clientType);
const sendOperationDeclaration = env[
"EXPERIMENTAL_TYPESPEC_TS_SERIALIZATION"
]
? experimentalGetSendPrivateFunction(
dpgContext,
o,
clientType,
serializerMap,
codeModel.runtimeImports
)
: getSendPrivateFunction(
dpgContext,
o,
clientType,
codeModel.runtimeImports
);
const deserializeOperationDeclaration = env[
"EXPERIMENTAL_TYPESPEC_TS_SERIALIZATION"
]
? experimentalGetDeserializePrivateFunction(
dpgContext,
o,
isMultiEndpoint,
needUnexpectedHelper,
codeModel.runtimeImports,
serializerMap
)
: getDeserializePrivateFunction(
o,
isMultiEndpoint,
needUnexpectedHelper,
codeModel.runtimeImports
);
const sendOperationDeclaration = getSendPrivateFunction(
dpgContext,
o,
clientType,
codeModel.runtimeImports
);
const deserializeOperationDeclaration = getDeserializePrivateFunction(
o,
isMultiEndpoint,
needUnexpectedHelper,
codeModel.runtimeImports
);
operationGroupFile.addFunctions([
sendOperationDeclaration,
deserializeOperationDeclaration,

Просмотреть файл

@ -13,7 +13,6 @@ import {
Operation,
OperationGroup
} from "../modularCodeModel.js";
import { isDefined } from "../serialization/util.js";
export function getClientName(client: Client) {
return client.name.replace(/Client$/, "");
@ -77,6 +76,10 @@ export function getClassicalLayerPrefix(
return prefix.join(separator);
}
export function isDefined<T>(thing: T | undefined | null): thing is T {
return typeof thing !== "undefined" && thing !== null;
}
export function getRLCIndexFilePath(
dpgContext: SdkContext,
client: Client | SdkClient,

Просмотреть файл

@ -1,20 +0,0 @@
import { CollectionFormat } from "@azure-tools/typespec-client-generator-core";
export function getCollectionSeparator(
collectionFormat?: CollectionFormat
): string {
switch (collectionFormat) {
case "csv":
return ",";
case "ssv":
return " ";
case "tsv":
return "\t";
case "pipes":
return "|";
case "multi":
return "&";
default:
return ",";
}
}

Просмотреть файл

@ -1,267 +0,0 @@
import {
addImportsToFiles,
addImportToSpecifier,
Imports as RuntimeImports
} from "@azure-tools/rlc-common";
import {
getAllModels,
SdkType,
SdkUnionType,
UsageFlags as TCGCUsageFlags
} from "@azure-tools/typespec-client-generator-core";
import { UsageFlags } from "@typespec/compiler";
import * as path from "path";
import { FunctionDeclarationStructure, OptionalKind } from "ts-morph";
import { toCamelCase, toPascalCase } from "../../utils/casingUtils.js";
import { SdkContext } from "../../utils/interfaces.js";
import {
getModularModelFilePath,
getRLCIndexFilePath
} from "../helpers/namingHelpers.js";
import { Client, ModularCodeModel } from "../modularCodeModel.js";
import {
serializeEnumFunctionBody,
serializeModelPropertiesInline,
serializeType,
serializeUnionInline
} from "./serializers.js";
import {
getUsage,
isDefined,
SerializeFunctionType,
SerializerMap
} from "./util.js";
export function buildSerializers(
dpgContext: SdkContext,
codeModel: ModularCodeModel,
client: Client
): SerializerMap {
// Root for changes associated with EXPERIMENTAL_TYPESPEC_TS_SERIALIZATION flag
const unions = codeModel.types
.filter((type) => type.type === "combined" && isDefined(type.tcgcType))
.map((type) => type.tcgcType as SdkUnionType);
const serializers = createSerializerMetadata(dpgContext, unions);
const entries = Object.entries(serializers).map(
([modularTypeName, meta]) => ({
...meta,
modularTypeName
})
);
const functions = entries
.flatMap(({ deserializerFunctionName, serializerFunctionName, type }) => [
serializerFunctionName
? createSerializerFunctionStructure(
dpgContext,
UsageFlags.Input,
serializers,
type,
codeModel.runtimeImports
)
: undefined,
deserializerFunctionName
? createSerializerFunctionStructure(
dpgContext,
UsageFlags.Output,
serializers,
type,
codeModel.runtimeImports
)
: undefined
])
.filter(isDefined);
if (!functions.length) {
return {};
}
const utilFilePath = path.join(
...[
codeModel.modularOptions.sourceRoot,
client.subfolder,
"utils",
"serializeUtil.ts"
].filter(isDefined)
);
const sourceFile = codeModel.project.createSourceFile(utilFilePath);
sourceFile.addFunctions(functions);
entries.forEach(({ modularTypeName, rlcTypeAlias, rlcTypeName }) => {
addImportToSpecifier(
"modularModel",
codeModel.runtimeImports,
modularTypeName
);
if (rlcTypeName) {
addImportToSpecifier(
"rlcIndex",
codeModel.runtimeImports,
rlcTypeAlias ? `${rlcTypeName} as ${rlcTypeAlias}` : rlcTypeName
);
}
});
addImportsToFiles(codeModel.runtimeImports, sourceFile, {
modularModel: path.relative(
path.dirname(utilFilePath),
getModularModelFilePath(codeModel, client, "js")
),
rlcIndex: path.relative(
path.dirname(utilFilePath),
getRLCIndexFilePath(dpgContext, client, "js")
)
});
return serializers;
}
function createSerializerMetadata(
dpgContext: SdkContext,
unions: SdkUnionType[]
): SerializerMap {
const allModels = getAllModels(dpgContext);
const typesWithSerializers = [...allModels, ...unions].filter(
hasSerializeFunction
);
const serializers = Object.fromEntries(
typesWithSerializers.map((type) => {
const serializerType = type as { usage?: TCGCUsageFlags };
const rlcTypeName =
serializerType.usage && serializerType.usage & TCGCUsageFlags.Output
? toPascalCase(`${type.name} Output`)
: serializerType.usage && serializerType.usage & TCGCUsageFlags.Input
? type.name
: undefined;
const rlcTypeAlias = rlcTypeName
? toPascalCase(`${type.name} Rest`)
: undefined;
const usage = getUsage(dpgContext, type);
const [serializerFunctionName, deserializerFunctionName] = (
[UsageFlags.Input, UsageFlags.Output] as const
).map((functionType) =>
functionType & usage
? toCamelCase(
`${
functionType === UsageFlags.Input ? "serialize" : "deserialize"
} ${type.name}`
)
: undefined
);
return [
type.name,
{
rlcTypeName,
rlcTypeAlias,
type,
serializerFunctionName,
deserializerFunctionName
}
] as const;
})
);
return serializers;
}
function createSerializerFunctionStructure(
dpgContext: SdkContext,
functionType: UsageFlags,
serializerMap: SerializerMap,
type: SerializeFunctionType,
runtimeImports: RuntimeImports
): OptionalKind<FunctionDeclarationStructure> {
const modularTypeName = type.name;
const serializerMetadata = serializerMap[modularTypeName];
const functionName =
functionType === UsageFlags.Input
? serializerMetadata?.serializerFunctionName ?? "FIXMYNAME"
: serializerMetadata?.deserializerFunctionName ?? "FIXMYNAME";
const paramName = "o";
const types = type.isGeneratedName
? ["any", "any"]
: [modularTypeName, serializerMetadata?.rlcTypeAlias ?? "FIXMYNAME"];
const [paramType, returnType] =
functionType === UsageFlags.Input ? types : types.reverse();
const parameters = [{ name: paramName, type: paramType }];
const statements = [];
switch (true) {
case !hasSerializeFunction(type): {
statements.push(
`return ${serializeType({
dpgContext,
functionType,
serializerMap,
type,
valueExpr: paramName,
importCallback: (importType, importedName) =>
addImportToSpecifier(importType, runtimeImports, importedName)
})};`
);
break;
}
case type.kind === "model": {
statements.push(
`return ${serializeModelPropertiesInline({
dpgContext,
functionType,
serializerMap,
type,
valueExpr: paramName,
importCallback: (importType, importedName) =>
addImportToSpecifier(importType, runtimeImports, importedName)
})}`
);
break;
}
case type.kind === "union": {
statements.push(
`return ${serializeUnionInline({
dpgContext,
functionType,
serializerMap,
type,
valueExpr: paramName,
importCallback: (importType, importedName) =>
addImportToSpecifier(importType, runtimeImports, importedName)
})}`
);
break;
}
case type.kind === "enum": {
statements.push(
serializeEnumFunctionBody({
dpgContext,
functionType,
serializerMap,
type,
valueExpr: paramName,
importCallback: (importType, importedName) =>
addImportToSpecifier(importType, runtimeImports, importedName)
})
);
break;
}
}
const functionDecl: OptionalKind<FunctionDeclarationStructure> = {
isExported: true,
name: functionName,
parameters,
returnType,
statements
};
return functionDecl;
}
function hasSerializeFunction(type: SdkType): type is SerializeFunctionType {
const isValidTypeKind =
type.kind === "enum" || type.kind === "model" || type.kind === "union";
return Boolean(isValidTypeKind && type.name);
}

Просмотреть файл

@ -1,652 +0,0 @@
import {
addImportToSpecifier,
getResponseBaseName,
getResponseTypeName,
Imports as RuntimeImports,
NameType,
OperationResponse
} from "@azure-tools/rlc-common";
import { SdkContext } from "@azure-tools/typespec-client-generator-core";
import {
FunctionDeclarationStructure,
OptionalKind,
ParameterDeclarationStructure
} from "ts-morph";
import { toPascalCase } from "../../utils/casingUtils.js";
import {
getDocsFromDescription,
getFixmeForMultilineDocs
} from "../helpers/docsHelpers.js";
import {
getClassicalLayerPrefix,
getOperationName
} from "../helpers/namingHelpers.js";
import { buildType } from "../helpers/typeHelpers.js";
import {
BodyParameter,
Client,
ModularCodeModel,
Operation,
ParameterLocation,
Property,
Type
} from "../modularCodeModel.js";
import { UsageFlags } from "@typespec/compiler";
import _ from "lodash";
import { Parameter } from "../modularCodeModel.js";
import { serializeType } from "../serialization/serializers.js";
import { SerializerMap, isDefined } from "../serialization/util.js";
export function getRLCResponseType(rlcResponse?: OperationResponse) {
if (!rlcResponse?.responses) {
return;
}
return rlcResponse?.responses
.map((resp) => {
const baseResponseName = getResponseBaseName(
rlcResponse.operationGroup,
rlcResponse.operationName,
resp.statusCode
);
// Get the information to build the Response Interface
return resp.predefinedName ?? getResponseTypeName(baseResponseName);
})
.join(" | ");
}
export function getRLCLroLogicalResponse(rlcResponse?: OperationResponse) {
const logicalResponse = (rlcResponse?.responses ?? []).filter(
(r) => r.predefinedName && r.predefinedName.endsWith(`LogicalResponse`)
);
return logicalResponse.length > 0
? logicalResponse[0]!.predefinedName!
: "any";
}
export function getSendPrivateFunction(
dpgContext: SdkContext,
operation: Operation,
clientType: string,
serializerMap: SerializerMap | undefined,
runtimeImports: RuntimeImports
): OptionalKind<FunctionDeclarationStructure> {
const parameters = getOperationSignatureParameters(operation, clientType);
const { name } = getOperationName(operation);
const returnType = `StreamableMethod<${getRLCResponseType(
operation.rlcResponse
)}>`;
const functionStatement: OptionalKind<FunctionDeclarationStructure> = {
isAsync: false,
isExported: true,
name: `_${name}Send`,
parameters,
returnType
};
const operationPath = operation.url;
const operationMethod = operation.method.toLowerCase();
const optionalParamName = parameters.filter(
(p) => p.type?.toString().endsWith("OptionalParams")
)[0]?.name;
const statements: string[] = [];
statements.push(
`return context.path("${operationPath}", ${getPathParameters(
operation
)}).${operationMethod}({...operationOptionsToRequestParameters(${optionalParamName}), ${getRequestParameters(
dpgContext,
operation,
serializerMap!,
runtimeImports
)}}) ${operation.isOverload ? `as ${returnType}` : ``} ;`
);
return {
...functionStatement,
statements
};
}
export function getDeserializePrivateFunction(
dpgContext: SdkContext,
operation: Operation,
needSubClient: boolean,
needUnexpectedHelper: boolean,
runtimeImports: RuntimeImports,
serializerMap: SerializerMap | undefined
): OptionalKind<FunctionDeclarationStructure> {
const { name } = getOperationName(operation);
const parameters: OptionalKind<ParameterDeclarationStructure>[] = [
{
name: "result",
type: getRLCResponseType(operation.rlcResponse)
}
];
// TODO: Support LRO + paging operation
// https://github.com/Azure/autorest.typescript/issues/2313
const isLroOnly = isLroOnlyOperation(operation);
// TODO: Support operation overloads
const response = operation.responses[0]!;
let returnType;
if (isLroOnly && operation.method.toLowerCase() !== "patch") {
returnType = buildLroReturnType(operation);
} else if (response?.type?.type) {
returnType = buildType(
response.type.name,
response.type,
response.type.format
);
} else {
returnType = { name: "", type: "void" };
}
const functionStatement: OptionalKind<FunctionDeclarationStructure> = {
isAsync: true,
isExported: true,
name: `_${name}Deserialize`,
parameters,
returnType: `Promise<${returnType.type}>`
};
const statements: string[] = [];
if (needUnexpectedHelper) {
statements.push(
`if(${needSubClient ? "UnexpectedHelper." : ""}isUnexpected(result)){`,
`throw createRestError(result);`,
"}"
);
addImportToSpecifier("restClient", runtimeImports, "createRestError");
} else {
const validStatus = [
...new Set(
operation.responses
.flatMap((r) => r.statusCodes)
.filter((s) => s !== "default")
)
];
if (validStatus.length > 0) {
statements.push(
`if(${validStatus
.map((s) => `result.status !== "${s}"`)
.join(" || ")}){`,
`throw createRestError(result);`,
"}"
);
addImportToSpecifier("restClient", runtimeImports, "createRestError");
}
}
const deserializedType = isLroOnly
? operation?.lroMetadata?.finalResult
: response?.type ?? "void"; // Is setting void here correct? We are handling the case where no response above but here we are
const hasLroSubPath = operation?.lroMetadata?.finalResultPath !== undefined;
const deserializedRoot = hasLroSubPath
? `result.body.${operation?.lroMetadata?.finalResultPath}`
: "result.body";
if (isLroOnly) {
const lroLogicalResponse = getRLCLroLogicalResponse(operation.rlcResponse);
statements.push(`result = result as ${lroLogicalResponse};`);
if (hasLroSubPath) {
statements.push(
`if(${deserializedRoot.split(".").join("?.")} === undefined) {
throw createRestError(\`Expected a result in the response at position "${deserializedRoot}"\`, result);
}
`
);
}
}
if (isDefined(deserializedType?.tcgcType)) {
statements.push(
`return ${deserializeResponseValue(
dpgContext,
serializerMap!,
deserializedType,
deserializedRoot,
runtimeImports
)}`
);
} else if (returnType.type === "void") {
statements.push("return");
} else {
statements.push(`return result.body`);
}
return {
...functionStatement,
statements
};
}
export function getOperationSignatureParameters(
operation: Operation,
clientType: string
): OptionalKind<ParameterDeclarationStructure>[] {
const optionsType = getOperationOptionsName(operation, true);
const parameters: Map<
string,
OptionalKind<ParameterDeclarationStructure>
> = new Map();
operation.parameters
.filter(
(p) =>
p.implementation === "Method" &&
p.type.type !== "constant" &&
p.clientDefaultValue === undefined &&
!p.optional
)
.map((p) => buildType(p.clientName, p.type, p.format))
.forEach((p) => {
parameters.set(p.name, p);
});
if (operation.bodyParameter) {
parameters.set(operation.bodyParameter?.clientName, {
hasQuestionToken: operation.bodyParameter.optional,
...buildType(
operation.bodyParameter.clientName,
operation.bodyParameter.type,
operation.bodyParameter.type.format
)
});
}
// Add context as the first parameter
const contextParam = { name: "context", type: clientType };
// Add the options parameter
const optionsParam = {
name: parameters.has("options") ? "optionalParams" : "options",
type: optionsType,
initializer: "{ requestOptions: {} }"
};
const finalParameters = [contextParam, ...parameters.values(), optionsParam];
return finalParameters;
}
/**
* This operation builds and returns the function declaration for an operation.
*/
export function getOperationFunction(
operation: Operation,
clientType: string
): OptionalKind<FunctionDeclarationStructure> & { propertyName?: string } {
if (isPagingOnlyOperation(operation)) {
// Case 1: paging-only operation
return getPagingOnlyOperationFunction(operation, clientType);
} else if (isLroOnlyOperation(operation)) {
// Case 2: lro-only operation
return getLroOnlyOperationFunction(operation, clientType);
} else if (isLroAndPagingOperation(operation)) {
// Case 3: both paging + lro operation is not supported yet so handle them as normal operation and customization may be needed
// https://github.com/Azure/autorest.typescript/issues/2313
}
// Extract required parameters
const parameters: OptionalKind<ParameterDeclarationStructure>[] =
getOperationSignatureParameters(operation, clientType);
// TODO: Support operation overloads
const response = operation.responses[0]!;
let returnType = { name: "", type: "void" };
if (response.type?.type) {
const type =
extractPagingType(response.type, operation.itemName) ?? response.type;
returnType = buildType(type.name, type, type.format);
}
const { name, fixme = [] } = getOperationName(operation);
const functionStatement = {
docs: [
...getDocsFromDescription(operation.description),
...getFixmeForMultilineDocs(fixme)
],
isAsync: true,
isExported: true,
name,
propertyName: operation.name,
parameters,
returnType: `Promise<${returnType.type}>`
};
const statements: string[] = [];
statements.push(
`const result = await _${name}Send(${parameters
.map((p) => p.name)
.join(", ")});`
);
statements.push(`return _${name}Deserialize(result);`);
return {
...functionStatement,
statements
};
}
function getLroOnlyOperationFunction(operation: Operation, clientType: string) {
// Extract required parameters
const parameters: OptionalKind<ParameterDeclarationStructure>[] =
getOperationSignatureParameters(operation, clientType);
const returnType = buildLroReturnType(operation);
const { name, fixme = [] } = getOperationName(operation);
const functionStatement = {
docs: [
...getDocsFromDescription(operation.description),
...getFixmeForMultilineDocs(fixme)
],
isAsync: false,
isExported: true,
name,
propertyName: operation.name,
parameters,
returnType: `PollerLike<OperationState<${returnType.type}>, ${returnType.type}>`
};
const statements: string[] = [];
statements.push(`
return getLongRunningPoller(context, _${name}Deserialize, {
updateIntervalInMs: options?.updateIntervalInMs,
abortSignal: options?.abortSignal,
getInitialResponse: () => _${name}Send(${parameters
.map((p) => p.name)
.join(", ")})
}) as PollerLike<OperationState<${returnType.type}>, ${returnType.type}>;
`);
return {
...functionStatement,
statements
};
}
export function buildLroReturnType(operation: Operation) {
const metadata = operation.lroMetadata;
if (metadata !== undefined && metadata.finalResult !== undefined) {
const type = metadata.finalResult;
return buildType(type.name, type, type.format);
}
return { name: "", type: "void" };
}
export function getPagingOnlyOperationFunction(
operation: Operation,
clientType: string
) {
// Extract required parameters
const parameters: OptionalKind<ParameterDeclarationStructure>[] =
getOperationSignatureParameters(operation, clientType);
// TODO: Support operation overloads
const response = operation.responses[0];
let returnType = { name: "", type: "void" };
if (response?.type?.type) {
const type =
extractPagingType(response.type, operation.itemName) ?? response.type;
returnType = buildType(type.name, type, type.format);
}
const { name, fixme = [] } = getOperationName(operation);
const functionStatement = {
docs: [
...getDocsFromDescription(operation.description),
...getFixmeForMultilineDocs(fixme)
],
isAsync: false,
isExported: true,
name,
propertyName: operation.name,
parameters,
returnType: `PagedAsyncIterableIterator<${returnType.type}>`
};
const statements: string[] = [];
const options = [];
if (operation.itemName) {
options.push(`itemName: "${operation.itemName}"`);
}
if (operation.continuationTokenName) {
options.push(`nextLinkName: "${operation.continuationTokenName}"`);
}
statements.push(
`return buildPagedAsyncIterator(
context,
() => _${name}Send(${parameters.map((p) => p.name).join(", ")}),
_${name}Deserialize,
${options.length > 0 ? `{${options.join(", ")}}` : ``}
);`
);
return {
...functionStatement,
statements
};
}
function extractPagingType(type: Type, itemName?: string): Type | undefined {
if (!itemName) {
return undefined;
}
const prop = (type.properties ?? [])
?.filter((prop) => prop.restApiName === itemName)
.map((prop) => prop.type);
if (prop.length === 0) {
return undefined;
}
return prop[0]?.type === "list" && prop[0].elementType
? prop[0].elementType
: undefined;
}
export function getOperationOptionsName(
operation: Operation,
includeGroupName = false
) {
const prefix =
includeGroupName && operation.name.indexOf("_") === -1
? getClassicalLayerPrefix(operation, NameType.Interface)
: "";
const optionName = `${prefix}${toPascalCase(operation.name)}OptionalParams`;
return optionName;
}
/**
* This function build the request parameters that we will provide to the
* RLC internally. This will translate High Level parameters into the RLC ones.
* Figuring out what goes in headers, body, path and qsp.
*/
function getRequestParameters(
dpgContext: SdkContext,
operation: Operation,
serializerMap: SerializerMap,
runtimeImports: RuntimeImports
): string {
if (!operation.parameters) {
return "";
}
const operationParameters = operation.parameters.filter(
(p) => p.implementation !== "Client" && !isContentType(p)
);
const contentTypeParameter = operation.parameters.find(isContentType);
const parametersImplementation: Partial<
Record<ParameterLocation, Parameter[] | BodyParameter[]>
> = _.groupBy(operationParameters, (p) => p.location);
parametersImplementation["body"] = isDefined(operation.bodyParameter)
? [operation.bodyParameter]
: parametersImplementation["body"];
const params = [
...(contentTypeParameter
? [getContentTypeValue(contentTypeParameter)]
: []),
...[
(
[
["headers", "header"],
["queryParameters", "query"],
["body", "body"]
] as const
)
.map(
([id, location]) => [id, parametersImplementation[location]] as const
)
.filter(([_, paramList]) => paramList?.length)
.flatMap(([id, paramList]) =>
paramList!.map((i) => {
const initializer = serializeType({
dpgContext,
functionType: UsageFlags.Input,
serializerMap,
type: i.tcgcType!,
valueExpr:
id === "body" ? i.clientName : `options?.${i.clientName}`,
importCallback: (importType, importedName) =>
addImportToSpecifier(importType, runtimeImports, importedName)
});
return `${id}: ${initializer}`;
})
)
]
];
return params.join(",\n");
}
function isContentType(param: Parameter): boolean {
return (
param.location === "header" &&
param.restApiName.toLowerCase() === "content-type"
);
}
function getContentTypeValue(param: Parameter | Property) {
const defaultValue =
param.clientDefaultValue ?? param.type.clientDefaultValue;
if (defaultValue) {
return `contentType: options.${param.clientName} as any ?? "${defaultValue}"`;
} else {
return `contentType: ${
!param.optional
? "contentType"
: "options." + param.clientName + " as any"
}`;
}
}
/**
* Builds the assignment for when a property or parameter has a default value
*/
function getDefaultValue(param: Parameter | Property) {
return (param.clientDefaultValue ?? param.type.clientDefaultValue) !==
undefined
? `${param.optional ? "??" : ""} "${
param.clientDefaultValue ?? param.type.clientDefaultValue
}"`
: "";
}
/**
* Extracts the path parameters
*/
function getPathParameters(operation: Operation) {
if (!operation.parameters) {
return "";
}
let pathParams = "";
for (const param of operation.parameters) {
if (param.location === "path") {
if (!param.optional) {
pathParams += `${pathParams !== "" ? "," : ""} ${param.clientName}`;
continue;
}
const defaultValue = getDefaultValue(param);
pathParams += `${pathParams !== "" ? "," : ""} options.${
param.clientName
}`;
if (defaultValue) {
pathParams += ` ?? "${defaultValue}"`;
}
}
}
return pathParams;
}
/**
* This function helps converting strings into JS complex types recursively.
* We need to drill down into Array elements to make sure that the element type is
* deserialized correctly
*/
function deserializeResponseValue(
dpgContext: SdkContext,
serializerMap: SerializerMap,
type: Type,
restValue: string,
runtimeImports: RuntimeImports
): string {
return serializeType({
dpgContext,
functionType: UsageFlags.Output,
serializerMap,
type: type.tcgcType!,
valueExpr: restValue,
importCallback: (importType, importedName) =>
addImportToSpecifier(importType, runtimeImports, importedName)
});
}
function isLroAndPagingOperation(op: Operation): boolean {
return op.discriminator === "lropaging";
}
export function isLroOnlyOperation(op: Operation): boolean {
return op.discriminator === "lro";
}
export function hasPagingOnlyOperation(
client: Client,
needRLC?: boolean
): boolean;
export function hasPagingOnlyOperation(
codeModel: ModularCodeModel,
needRLC?: boolean
): boolean;
export function hasPagingOnlyOperation(
clientOrCodeModel: Client | ModularCodeModel,
needRLC: boolean = false
): boolean {
let clients: Client[] = [];
if ((clientOrCodeModel as any)?.operationGroups) {
clients = [clientOrCodeModel as Client];
} else if ((clientOrCodeModel as any)?.clients) {
clients = (clientOrCodeModel as ModularCodeModel).clients;
}
return clients.some(
(c) =>
(needRLC ? c.rlcHelperDetails.hasPaging : false) ||
(c.operationGroups ?? []).some((og) =>
(og.operations ?? []).some(isPagingOnlyOperation)
)
);
}
export function isPagingOnlyOperation(op: Operation): boolean {
return op.discriminator === "paging";
}
export function getAllAncestors(type: Type): Type[] {
const ancestors: Type[] = [];
type?.parents?.forEach((p) => {
ancestors.push(p);
ancestors.push(...getAllAncestors(p));
});
return ancestors;
}

Просмотреть файл

@ -1,51 +0,0 @@
import {
SdkArrayType,
SdkType
} from "@azure-tools/typespec-client-generator-core";
import { UsageFlags } from "@typespec/compiler";
import { SerializeTypeOptions, serializeType } from "./serializers.js";
import { SerializerOutput, getModularTypeId, getRLCTypeId } from "./util.js";
export function serializeArray(
options: SerializeTypeOptions<SdkArrayType>
): SerializerOutput {
const { dpgContext, functionType, serializerMap, type, valueExpr } = options;
const valueType = type.valueType as SdkType & { name?: string };
const mapParameterId = "e";
const elementTypeName =
functionType === UsageFlags.Input
? getModularTypeId(valueType)
: getRLCTypeId(dpgContext, valueType);
const serializedChildExpr = serializeType({
...options,
type: valueType,
valueExpr: mapParameterId
});
if (serializedChildExpr === mapParameterId) {
// mapping over identity function, so map is unnecessary
// arr.map((e) => e) -> arr
return valueExpr;
}
// arr.map((e) => f(e)) -> arr.map(f)
const unaryFunctionInvocation =
/(?<functionName>\w+)\((?<childArgExpr>\w+)\)/;
const { functionName, childArgExpr } =
serializedChildExpr.match(unaryFunctionInvocation)?.groups ?? {};
const mapArg =
elementTypeName && serializerMap?.[elementTypeName]
? mapParameterId
: `${mapParameterId}: ${elementTypeName}`;
const mapFunction =
childArgExpr === mapParameterId
? functionName
: `(${mapArg})=>(${serializedChildExpr})`;
return `${valueExpr}.map(${mapFunction})`;
}

Просмотреть файл

@ -1,22 +0,0 @@
import { SdkDatetimeType } from "@azure-tools/typespec-client-generator-core";
import { UsageFlags } from "@typespec/compiler";
import { SerializeTypeOptions } from "./serializers.js";
export function serializeDatetime(
options: SerializeTypeOptions<SdkDatetimeType>
): string {
const { functionType, type, valueExpr } = options;
if (functionType === UsageFlags.Input) {
switch (type.encode) {
case "rfc7231":
return `(${valueExpr}).toUTCString()`;
case "unixTimestamp":
return `(${valueExpr}).getTime()`;
case "rfc3339":
default:
return `(${valueExpr}).toISOString()`;
}
} else {
return `new Date(${valueExpr})`;
}
}

Просмотреть файл

@ -1,64 +0,0 @@
import {
SdkHeaderParameter,
UsageFlags
} from "@azure-tools/typespec-client-generator-core";
import { SerializeTypeOptions, serializeType } from "./serializers.js";
import { SerializerOutput } from "./util.js";
import { getCollectionSeparator } from "./collectionUtils.js";
import { serializeArray } from "./serializeArray.js";
import { isNumericTypeKind } from "../helpers/typeHelpers.js";
export function serializeHeader(
options: SerializeTypeOptions<
SdkHeaderParameter & {
kind: "header";
}
>
): SerializerOutput {
const { type, functionType, valueExpr } = options;
// We need to handle arrays differently than other types.
// For headers arrays are serialized as a single string with a separator.
if (type.type.kind === "array") {
const separator = JSON.stringify(
getCollectionSeparator(type.collectionFormat)
);
if (functionType === UsageFlags.Input) {
// Serialization
// Here we need to turn an array into a string with a separator.
// We can use the serializeArray function to do this.
const serializedArray = serializeArray({
...options,
type: type.type
});
// Now that we have serialized the array, we can join the elements with the separator.
return `${serializedArray}.join(${separator})`;
} else {
// Deserialization
// Here we have a string that we need to split into an array.
const array = `${valueExpr}.split(${separator})`;
// No need to parse if the elements ar strings
if (type.type.valueType.kind === "string") {
return array;
}
if (
isNumericTypeKind(type.type.valueType.kind) ||
type.type.valueType.kind === "boolean"
) {
return `${array}.map(e => JSON.parse(e))`;
}
return `${array}.map(e => ${serializeType({
...options,
type: type.type.valueType,
valueExpr: "e"
})})`;
}
}
return serializeType({ ...options, type: type.type });
}

Просмотреть файл

@ -1,304 +0,0 @@
import { ImportType } from "@azure-tools/rlc-common";
import {
SdkConstantType,
SdkContext,
SdkEnumType,
SdkModelPropertyType,
SdkModelType,
SdkType,
SdkUnionType
} from "@azure-tools/typespec-client-generator-core";
import { getDiscriminator, UsageFlags } from "@typespec/compiler";
import _ from "lodash";
import * as Reify from "../../reify/index.js";
import {
getEncodingFormat,
getParameterTypePropertyName,
getReturnTypePropertyName,
isDefined,
SerializerMap,
SerializerOutput
} from "./util.js";
import { serializeHeader } from "./serializeHeaders.js";
import { serializeArray } from "./serializeArray.js";
import { serializeDatetime } from "./serializeDateTime.js";
export interface SerializeTypeOptions<
TCGCType extends SdkType | SdkModelPropertyType
> {
dpgContext: SdkContext;
functionType: UsageFlags;
serializerMap?: SerializerMap;
type: TCGCType;
valueExpr: string;
importCallback: (importType: ImportType, importedName: string) => void;
}
const placeholder = <T>(_?: T) => {
return '(()=>{throw Error("Not implemented.")})()';
};
export function serializeType<TCGCType extends SdkType | SdkModelPropertyType>(
options: SerializeTypeOptions<TCGCType>
): SerializerOutput {
const { functionType, serializerMap, type, valueExpr } = options;
const modularTypeName = (type as { name?: string }).name;
const serializerMetadata =
isDefined(modularTypeName) && serializerMap?.[modularTypeName];
if (serializerMetadata) {
const functionName =
functionType === UsageFlags.Input
? serializerMetadata?.serializerFunctionName
: serializerMetadata?.deserializerFunctionName;
return `${
functionName ??
(functionType === UsageFlags.Input
? "MISSING_SERIALIZER"
: "MISSING_DESERIALIZER")
}((${valueExpr}))`;
}
return getSerializeHandler(type.kind)?.(options) ?? valueExpr;
}
function getSerializeHandler<TCGCType extends SdkType | SdkModelPropertyType>(
kind: TCGCType["kind"]
): ((options: SerializeTypeOptions<TCGCType>) => SerializerOutput) | undefined {
type SimpleType =
| "any"
| "armId"
| "azureLocation"
| "boolean"
| "constant"
| "decimal"
| "decimal128"
| "enum"
| "eTag"
| "float"
| "float32"
| "float64"
| "guid"
| "int16"
| "int32"
| "int64"
| "int8"
| "integer"
| "ipAddress"
| "ipV4Address"
| "ipV6Address"
| "numeric"
| "password"
| "safeint"
| "string"
| "uint16"
| "uint32"
| "uint64"
| "uint8"
| "uri"
| "url"
| "uuid"
| "nullable";
type SerializeHandler<
TCGCTypeKind extends (SdkType | SdkModelPropertyType)["kind"]
> = {
[K in TCGCTypeKind]: (
options: SerializeTypeOptions<
(SdkType | SdkModelPropertyType) & { kind: K }
>
) => SerializerOutput;
};
type SerializeHandlerMap = Partial<SerializeHandler<SimpleType>> &
SerializeHandler<
Exclude<(SdkType | SdkModelPropertyType)["kind"], SimpleType>
>;
const handlers: SerializeHandlerMap = {
array: serializeArray,
bytes: serializeByteArray,
enumvalue: (options) =>
serializeType({ ...options, type: options.type.valueType }),
model: serializeModelPropertiesInline,
offsetDateTime: serializeDatetime,
plainDate: ({ valueExpr }) => `(${valueExpr}).toDateString()`,
plainTime: ({ valueExpr }) => `(${valueExpr}).toTimeString()`,
property: serializeModelProperty,
// TODO: This is the function that emits the body of a function meant to serialize a type union.
// It's implemented as a switch statement. This is fine for a function body. However,
// `getSerializeHandler` is designed to always return something that can go in expression
// position. We may or may not decide to emit a function to serialize anonymous unions using the
// TCGC generated name as its identifier. If we don't, a copy of this function will need to be
// adapted to expression position (e.g. nested ternary statements). As it stands, this function
// should never actually be dispatched, and should be removed if we find it to be unnecessary.
union: serializeUnionInline,
utcDateTime: serializeDatetime,
duration: placeholder,
tuple: placeholder,
dict: placeholder,
credential: placeholder,
endpoint: placeholder,
method: placeholder,
query: placeholder,
path: placeholder,
body: placeholder,
header: serializeHeader
};
const handler = handlers[kind];
return handler as any;
}
function serializeByteArray(
options: SerializeTypeOptions<SdkType & { kind: "bytes" }>
): string {
const { functionType, type, valueExpr, importCallback } = options;
if (type.encode === "binary") {
return valueExpr;
}
const format = getEncodingFormat(functionType, type);
const args = [valueExpr, `"${format}"`].join(", ");
if (functionType === UsageFlags.Input) {
importCallback("coreUtil", "uint8ArrayToString");
return `uint8ArrayToString(${args})`;
}
importCallback("coreUtil", "stringToUint8Array");
return `(typeof (${valueExpr}) === 'string')
? (stringToUint8Array(${args}))
: (${valueExpr})`;
}
export function serializeModelPropertiesInline(
options: SerializeTypeOptions<SdkModelType>
): string {
const { dpgContext, functionType, type, valueExpr } = options;
const props = type.properties.filter((p) => p.kind === "property");
const serializedParents = type.baseModel
? `...(${serializeType({ ...options, type: type.baseModel })})`
: undefined;
const serializedProps = props.map((prop) => {
const id = getParameterTypePropertyName(dpgContext, functionType, prop);
return serializeModelProperty({
...options,
type: prop,
valueExpr: `${valueExpr}["${id}"]`
});
});
const body = [`...(${valueExpr})`, serializedParents, ...serializedProps]
.filter(isDefined)
.join(",\n");
return `{${body}}`;
}
export function serializeUnionInline(
options: SerializeTypeOptions<SdkUnionType>
): string {
const { dpgContext, type, valueExpr } = options;
const discriminator = getDiscriminator(dpgContext.program, type.__raw!)
?.propertyName;
if (!discriminator) {
return placeholder();
}
const baseType = type.values.find((v) => v.kind !== "constant")!;
const variants = type.values.filter(
(v) => v !== baseType
) as SdkConstantType[];
// TODO: Used to catch corner cases during testing. Remove this before shipping.
variants
.filter((v) => v.kind !== "constant")
.forEach((v) => {
throw Error(
`TODO: Handle non-constant non-base variant of union ${type.name} ` +
`with type name ${v.name} and kind ${v.kind}`
);
});
const cases = variants
.map((variant) => {
`${variant.value}: ${serializeType({
...options,
type: variant
})}`;
})
.join(",\n");
// TODO: Non-discriminated unions have UB at the moment, as opposed to having good messaging that
// they're not supported
return `({${cases}})[(${valueExpr})["${discriminator}"]] ?? (${serializeType({
...options,
type: baseType
})})`;
}
export function serializeEnumFunctionBody(
options: SerializeTypeOptions<SdkEnumType>
): string {
const { type, valueExpr } = options;
const defaultBody: Reify.Statement[] = [{ kind: "return", value: valueExpr }];
const cases = _(type.values)
.map((value): [string | undefined, Reify.Statement[]] => {
const variantId = `${type.name}.${value.name}`;
const variantBody: Reify.ReturnStatement[] = [
{
kind: "return",
value: serializeType({ ...options, type: value.valueType })
}
];
return [variantId, variantBody];
})
.concat([[undefined, defaultBody]])
.reduce((acc, [id, body]) => {
const ids = acc.get(body) ?? new Set();
ids.add(id);
acc.set(body, ids);
return acc;
}, new Map<Reify.Statement[], Set<string | undefined>>());
const functionBody: Reify.SwitchReturn = {
kind: "switch return",
expr: valueExpr,
cases
};
return Reify.reifySwitchReturn(functionBody);
}
function serializeModelProperty(
options: SerializeTypeOptions<SdkModelPropertyType>
): string {
const { type, valueExpr } = options;
const propDecl = getPropertyDeclaration(options);
return type.optional
? `...((${valueExpr}) === undefined ? {} : {${propDecl}})`
: `${propDecl}`;
}
function getPropertyDeclaration(
options: SerializeTypeOptions<SdkModelPropertyType>
): string {
const { dpgContext, functionType, type } = options;
const id = getReturnTypePropertyName(dpgContext, functionType, type);
const propInitializer = getPropertyInitializer(options);
return `"${id}": (${propInitializer})`;
}
function getPropertyInitializer(
options: SerializeTypeOptions<SdkModelPropertyType>
): string {
const { type } = options;
const propSerializer = serializeType({
...options,
type: { ...type.type, nullable: false, optional: false }
});
return propSerializer;
}

Просмотреть файл

@ -1,171 +0,0 @@
import {
getLibraryName,
getWireName,
SdkEnumType,
SdkModelPropertyType,
SdkModelType,
SdkType,
SdkUnionType,
UsageFlags as TCGCUsageFlags
} from "@azure-tools/typespec-client-generator-core";
import { UsageFlags } from "@typespec/compiler";
import { toPascalCase } from "../../utils/casingUtils.js";
import { SdkContext } from "../../utils/interfaces.js";
type ModularTypeName = string;
export type SerializeFunctionType = SdkModelType | SdkUnionType | SdkEnumType;
export type SerializerMap = Record<ModularTypeName, SerializerMetadata>;
export type SerializerOutput = string;
interface SerializerMetadata {
rlcTypeName?: string;
rlcTypeAlias?: string;
type: SerializeFunctionType;
serializerFunctionName?: string;
deserializerFunctionName?: string;
}
const supportedFormats = ["base64url", "base64", "byte"];
export function getEncodingFormat(
functionType: UsageFlags,
type: SdkType & { kind: "bytes" }
) {
if (functionType === UsageFlags.Output) {
return type.encode;
}
return supportedFormats.find((format) => format === type.encode) ?? "base64";
}
const dispatch: {
[Kind in SdkType["kind"]]: (
type: SdkType & { kind: Kind }
) => string | undefined;
} = {
string: (_) => "string",
boolean: (_) => "boolean",
bytes: (_) => "Uint8Array",
model: (type) => type.name,
union: (type) => type.name,
enum: (type) => type.name,
plainDate: (_) => "Date",
plainTime: (_) => "Date",
any: (_) => "any",
numeric: (_) => "number",
integer: (_) => "number",
safeint: (_) => "number",
int8: (_) => "number",
int16: (_) => "number",
int32: (_) => "number",
int64: (_) => "number",
uint8: (_) => "number",
uint16: (_) => "number",
uint32: (_) => "number",
uint64: (_) => "number",
float: (_) => "number",
float32: (_) => "number",
float64: (_) => "number",
decimal: (_) => "number",
decimal128: (_) => "number",
password: (_) => "string",
guid: (_) => "string",
url: (_) => "string",
uri: (_) => "string",
ipAddress: (_) => "string",
uuid: (_) => "string",
ipV4Address: (_) => "string",
ipV6Address: (_) => "string",
eTag: (_) => "string",
armId: (_) => "string",
azureLocation: (_) => "string",
utcDateTime: (_) => "Date",
offsetDateTime: (_) => "Date",
duration: (_) => "FIXME",
array: (type) => {
const valueTypeId = getModularTypeId(type.valueType);
return valueTypeId ? `Array<${valueTypeId}>` : undefined;
},
tuple: (type) => {
const elementIds = type.values.map(getModularTypeId);
if (!type.values.length || elementIds.some((id) => !id)) {
return;
}
return `[${elementIds.join(", ")}]`;
},
dict: (type) => {
const keyId = getModularTypeId(type.keyType);
const valueId = getModularTypeId(type.valueType);
if (!keyId || !valueId) {
return;
}
const valueType = valueId;
return `Record<${keyId}, ${valueType}>`;
},
enumvalue: (_) => "FIXME",
constant: (_) => "FIXME",
credential: (_) => "FIXME",
endpoint: (_) => "string",
nullable: (_) => "null"
};
export function getModularTypeId<T extends SdkType>(
type: T
): string | undefined {
const callback = dispatch[type.kind] as (type: T) => string | undefined;
return callback(type);
}
export function getParameterTypePropertyName(
dpgContext: SdkContext,
functionType: UsageFlags,
p: SdkModelPropertyType
): string {
return functionType === UsageFlags.Output
? getWireName(dpgContext, p.__raw!)
: getLibraryName(dpgContext, p.__raw!);
}
export function getReturnTypePropertyName(
dpgContext: SdkContext,
functionType: UsageFlags,
p: SdkModelPropertyType
): string {
return functionType === UsageFlags.Output
? getLibraryName(dpgContext, p.__raw!)
: getWireName(dpgContext, p.__raw!);
}
export function getRLCTypeId(
dpgContext: SdkContext,
type: SdkType & { name?: string }
) {
const modularTypeId = getModularTypeId(type);
const usage = getUsage(dpgContext, type);
const useOutputModel =
usage & UsageFlags.Output && !(usage & UsageFlags.Input);
return useOutputModel ? toPascalCase(`${modularTypeId} Rest`) : modularTypeId;
}
export function getUsage(
// @ts-expect-error: To be removed with the below TODO
dpgContext: SdkContext,
// @ts-expect-error: Ditto
type: SdkType
): TCGCUsageFlags {
return toDpgUsageFlags(
//TODO: Currently disregards usage when calculating whether or not to emit the function. This
//doesn't affect client API surface.
TCGCUsageFlags.Input | TCGCUsageFlags.Output
);
}
function toDpgUsageFlags(usage: TCGCUsageFlags): UsageFlags {
return (
(usage & TCGCUsageFlags.Input ? UsageFlags.Input : 0) |
(usage & TCGCUsageFlags.Output ? UsageFlags.Output : 0)
);
}
export function isDefined<T>(thing: T | undefined | null): thing is T {
return typeof thing !== "undefined" && thing !== null;
}

Просмотреть файл

@ -1,41 +0,0 @@
import _ from "lodash";
import {} from "ts-morph";
export type Statement = ReturnStatement;
export interface ReturnStatement {
kind: "return";
value?: string;
}
export interface SwitchReturn {
kind: "switch return";
expr: string;
cases: Map<Statement[], Set<string | undefined>>;
}
export function reifySwitchReturn(switchReturn: SwitchReturn): string {
const [cases, defaultCase] = _([...switchReturn.cases])
.groupBy(([__, ids]) => _(ids).some(_.isUndefined))
.at(["false", "true[0]"])
.value() as [
[ReturnStatement[], Set<string>][],
ReturnStatement[] | undefined
];
const fallthroughCases = cases.map(
([body, ids]): [string, ReturnStatement[]] => [
_(ids)
.map((id) => `case ${id}:`)
.join("\n"),
body
]
);
const fallthroughCasesWithDefault = defaultCase
? fallthroughCases.concat(["default:", defaultCase])
: fallthroughCases;
return [
`switch (${switchReturn.expr}) {`,
...fallthroughCasesWithDefault,
"}"
].join("\n");
}

Просмотреть файл

@ -0,0 +1,163 @@
import {describe, it, beforeAll, assert} from "vitest";
import { createSdkContextFromTypespec } from "../test-hots.js";
import { SdkContext, SdkHttpOperation, SdkPackage, SdkUnionType, namespace as tcgcNamespace } from "@azure-tools/typespec-client-generator-core";
import {provideSdkTypes, useSdkTypes} from "../../../src/framework/hooks/sdkTypes.js"
import {getNamespaceFullName, isGlobalNamespace, isStdNamespace, Namespace, navigateProgram} from "@typespec/compiler"
import {namespace as httpNamespace} from "@typespec/http"
import { provideContext } from "../../../src/contextManager.js";
describe("SdkTypes hook", () => {
let sdkPackage: SdkPackage<SdkHttpOperation>;
let sdkContext: SdkContext;
beforeAll(async () => {
const spec = `
union NoNamespaceUnion {
D: "d"
};
@service({
title: "Widget Service",
})
namespace DemoService{
model Widget {
@visibility("read", "update")
@path
id: string;
weight: int32;
color: "red" | "blue";
position: Foo
}
@error
model Error {
code: int32;
message: string;
bar: Bar;
}
enum Foo {
ONE: "one";
TWO: "two";
};
union Bar {
ONE: "one";
TWO: "two";
}
@route("/widgets")
@tag("Widgets")
interface Widgets {
@get list(): Widget[] | Error;
@get read(@path id: string): Widget | Error;
@post create(...Widget): Widget | Error;
@patch update(...Widget): Widget | Error;
@delete delete(@path id: string): void | Error;
@route("{id}/analyze") @post analyze(@path id: string): string | Error;
}
namespace SubDemoService {
interface SubWidgets {
@get list(): Widget[] | Error;
@post create(...SubWidget): SubWidget | Error;
}
model SubWidget {
weight: int32;
color: "red" | "blue";
number: SubFoo;
bar: SubBar;
}
enum SubFoo {
THREE: "3";
FOUR: "4";
};
union SubBar {
ONE: "one";
TWO: "two";
}
}
}
`
sdkContext = await createSdkContextFromTypespec(spec, {});
sdkPackage = sdkContext.experimental_sdkPackage;
provideSdkTypes(sdkPackage);
provideContext("emitContext", {tcgcContext: sdkContext, compilerContext: sdkContext.emitContext as any})
});
it("should setup the SdkTypeContext", async () => {
const getSdkType = useSdkTypes();
assert.isDefined(getSdkType);
});
it("should provide all operations regardless of namespace", () => {
const getSdkType = useSdkTypes();
navigateProgram(sdkContext.program, {
operation(o) {
const sdkMethod = getSdkType(o);
assert.isDefined(sdkMethod, `Couldn't find sdkOperation for ${o.name}`);
}
})
})
it("should provide all models regardless of namespace", () => {
const getSdkType = useSdkTypes();
navigateProgram(sdkContext.program, {
model(m) {
// Filtering out namespaces declared in other libraries such as @typespec/http
if(isIgnoredNamespace(m.namespace)) {
return;
}
const sdkMethod = getSdkType(m);
assert.isDefined(sdkMethod, `Couldn't find sdk model for ${m.name}`);
}
})
})
it("should provide all enums regardless of namespace", () => {
const getSdkType = useSdkTypes();
navigateProgram(sdkContext.program, {
enum(e) {
// Filtering out namespaces declared in other libraries such as @typespec/http
if(isIgnoredNamespace(e.namespace)) {
return;
}
const sdkMethod = getSdkType(e);
assert.isDefined(sdkMethod, `Couldn't find sdk model for ${e.name}`);
},
union(u) {
if(isIgnoredNamespace(u.namespace)) {
return;
}
const sdkMethod = getSdkType(u);
assert.isDefined(sdkMethod, `Couldn't find sdk union for ${u.name}`);
}
})
})
})
function isIgnoredNamespace(namespace: Namespace | undefined) {
const externalNamespaces = [httpNamespace, tcgcNamespace]
if(!namespace) {
return true;
}
if(namespace) {
const nsName = getNamespaceFullName(namespace);
if(isStdNamespace(namespace) || externalNamespaces.includes(nsName)) {
return true;
}
}
}

Просмотреть файл

@ -1,460 +0,0 @@
import { beforeEach, describe, it, expect } from "vitest";
import { createMyTestRunner, createTcgcContext } from "./test-hots.js";
import { serializeArray } from "../../../src/modular/serialization/serializeArray.js";
import {
SdkArrayType,
SdkContext
} from "@azure-tools/typespec-client-generator-core";
import { EmitContext, UsageFlags } from "@typespec/compiler";
import { BasicTestRunner } from "@typespec/compiler/testing";
import { SerializeTypeOptions } from "../../../src/modular/serialization/serializers.js";
import { format } from "prettier";
import { SerializerMap } from "../../../src/modular/serialization/util.js";
describe("serializeArray", () => {
let runner: BasicTestRunner;
beforeEach(async () => {
runner = await createMyTestRunner();
});
describe("serialization", () => {
let testArrays: TestArrays;
beforeEach(async () => {
testArrays = await getSdkArrays(runner, "serialize");
});
it("should handle a string array", async () => {
const { stringArray } = testArrays;
const result = serializeArray(stringArray);
expect(result).to.equal("myArray");
});
it("should handle a numeric array", async () => {
const { numericArray } = testArrays;
const result = serializeArray(numericArray);
expect(result).to.equal("myArray");
});
it("should handle a boolean array", async () => {
const { booleanArray } = testArrays;
const result = serializeArray(booleanArray);
expect(result).to.equal("myArray");
});
it("should handle a date array", async () => {
const { dateArray } = testArrays;
const result = serializeArray(dateArray);
expect(result).to.equal("myArray.map((e: Date)=>((e).toISOString()))");
});
it("should handle a model array inline", async () => {
const { modelArray } = testArrays;
const result = serializeArray({
...modelArray,
serializerMap: undefined
});
const expected = await format(
`myArray.map((e: Foo)=>({...(e),
"bar": ({...(e["bar"]),
"baz": ({...(e["bar"]["myBaz"]),
"qux": (e["bar"]["myBaz"]["qux"])})})}))`,
{ parser: "typescript" }
);
const actual = await format(result, { parser: "typescript" });
expect(actual).to.equal(expected);
});
it("should handle a model array ", async () => {
const { modelArray } = testArrays;
const result = serializeArray(modelArray);
const expected = await format(`myArray.map((e) => serializeFoo(e));;`, {
parser: "typescript"
});
const actual = await format(result, { parser: "typescript" });
expect(actual).to.equal(expected);
});
it("should handle a cyclic array", async () => {
const { cyclicArray } = testArrays;
const result = serializeArray(cyclicArray);
expect(result).to.equal("myArray.map((e)=>(serializeTest((e))))");
});
it("should handle an optional properties array", async () => {
const { optionalPropertiesArray } = testArrays;
const result = serializeArray(optionalPropertiesArray);
const expected = await format(
"myArray.map((e) => serializeOptionalFoo(e));",
{ parser: "typescript" }
);
const actual = await format(result, { parser: "typescript" });
expect(actual).to.equal(expected);
});
it("should handle an optional properties array inline", async () => {
const { optionalPropertiesArray } = testArrays;
const result = serializeArray({
...optionalPropertiesArray,
serializerMap: undefined
});
const expected = await format(
`myArray.map((e: OptionalFoo) => ({
...e,
...(e["bar"] === undefined
? {}
: {
bar: {
...e["bar"],
baz: { ...e["bar"]["myBaz"], qux: e["bar"]["myBaz"]["qux"] },
},
}),
}));`,
{ parser: "typescript" }
);
const actual = await format(result, { parser: "typescript" });
expect(actual).to.equal(expected);
});
});
describe("deserialization", () => {
let testArrays: TestArrays;
beforeEach(async () => {
testArrays = await getSdkArrays(runner, "deserialize");
});
it("should handle a string array", async () => {
const { stringArray } = testArrays;
const result = serializeArray(stringArray);
expect(result).to.equal("myArray");
});
it("should handle a numeric array", async () => {
const { numericArray } = testArrays;
const result = serializeArray(numericArray);
expect(result).to.equal("myArray");
});
it("should handle a boolean array", async () => {
const { booleanArray } = testArrays;
const result = serializeArray(booleanArray);
expect(result).to.equal("myArray");
});
it("should handle a date array", async () => {
const { dateArray } = testArrays;
const result = serializeArray(dateArray);
expect(result).to.equal("myArray.map(Date)");
});
it("should handle a model array inline", async () => {
const { modelArray } = testArrays;
const result = serializeArray({
...modelArray,
serializerMap: undefined
});
const expected = await format(
`myArray.map((e: Foo)=>({...(e),
"bar": ({...(e["bar"]),
"myBaz": ({...(e["bar"]["baz"]),
"qux": (e["bar"]["baz"]["qux"])})})}))`,
{ parser: "typescript" }
);
const actual = await format(result, { parser: "typescript" });
expect(actual).to.equal(expected);
});
it("should handle a model array", async () => {
const { modelArray } = testArrays;
const result = serializeArray(modelArray);
const expected = await format(`myArray.map((e) => deserializeFoo(e));`, {
parser: "typescript"
});
const actual = await format(result, { parser: "typescript" });
expect(actual).to.equal(expected);
});
// Handle cyclic models
it("should handle a cyclic array", async () => {
const { cyclicArray } = testArrays;
const result = serializeArray(cyclicArray);
expect(result).to.equal("myArray.map((e)=>(deserializeTest((e))))");
});
it("should handle an optional properties array", async () => {
const { optionalPropertiesArray } = testArrays;
const result = serializeArray(optionalPropertiesArray);
const expected = await format(
"myArray.map((e) => deserializeOptionalFoo(e));",
{ parser: "typescript" }
);
const actual = await format(result, { parser: "typescript" });
expect(actual).to.equal(expected);
});
it("should handle an optional properties array inline", async () => {
const { optionalPropertiesArray } = testArrays;
const result = serializeArray({
...optionalPropertiesArray,
serializerMap: undefined
});
const expected = await format(
`myArray.map((e: OptionalFoo) => ({
...e,
...(e["bar"] === undefined
? {}
: {
bar: {
...e["bar"],
myBaz: { ...e["bar"]["baz"],
qux: e["bar"]["baz"]["qux"] },
},
}),
}));`,
{ parser: "typescript" }
);
const actual = await format(result, { parser: "typescript" });
expect(actual).to.equal(expected);
});
});
});
type ArraySerializeOptions = SerializeTypeOptions<
SdkArrayType & {
kind: "array";
}
>;
interface TestArrays {
stringArray: ArraySerializeOptions;
numericArray: ArraySerializeOptions;
booleanArray: ArraySerializeOptions;
dateArray: ArraySerializeOptions;
modelArray: ArraySerializeOptions;
cyclicArray: ArraySerializeOptions;
optionalPropertiesArray: ArraySerializeOptions;
}
async function getSdkArrays(
runner: BasicTestRunner,
mode: "serialize" | "deserialize"
): Promise<TestArrays> {
const template = ` @service({
title: "Widget Service",
})
namespace DemoService;
@test model Test {
stringArray: string[];
numericArray: int32[];
booleanArray: boolean[];
dateArray: utcDateTime[];
modelArray: Foo[];
cyclicArray: Test[];
optionalModelArray?: Foo[];
optionalPropertiesArray?: OptionalFoo[];
};
model OptionalFoo {
bar?: Bar
}
model Foo {
bar: Bar
};
model Bar {
@clientName("myBaz")
baz: Baz
};
model Baz {
qux: string;
};
op foo(): Test;
`;
await runner.compile(template);
const context: EmitContext = {
program: runner.program
} as any;
const sdkContext = createTcgcContext(context);
const stringArray =
sdkContext.experimental_sdkPackage.models[0].properties.find(
(p) => p.name === "stringArray"
)?.type as unknown as SdkArrayType;
const numericArray =
sdkContext.experimental_sdkPackage.models[0].properties.find(
(p) => p.name === "numericArray"
)?.type as unknown as SdkArrayType;
const booleanArray =
sdkContext.experimental_sdkPackage.models[0].properties.find(
(p) => p.name === "booleanArray"
)?.type as unknown as SdkArrayType;
const dateArray =
sdkContext.experimental_sdkPackage.models[0].properties.find(
(p) => p.name === "dateArray"
)?.type as unknown as SdkArrayType;
const modelArray =
sdkContext.experimental_sdkPackage.models[0].properties.find(
(p) => p.name === "modelArray"
)?.type as unknown as SdkArrayType;
const cyclicArray =
sdkContext.experimental_sdkPackage.models[0].properties.find(
(p) => p.name === "cyclicArray"
)?.type as unknown as SdkArrayType;
const optionalModelArray =
sdkContext.experimental_sdkPackage.models[0].properties.find(
(p) => p.name === "optionalModelArray"
)?.type as unknown as SdkArrayType;
const optionalPropertiesArray =
sdkContext.experimental_sdkPackage.models[0].properties.find(
(p) => p.name === "optionalPropertiesArray"
)?.type as unknown as SdkArrayType;
const serializerMap: SerializerMap = {
Test: {
type: sdkContext.experimental_sdkPackage.models.find(
(m) => m.name === "Test"
) as any,
rlcTypeAlias: "TestRest",
rlcTypeName: "Test",
deserializerFunctionName: "deserializeTest",
serializerFunctionName: "serializeTest"
},
Foo: {
type: sdkContext.experimental_sdkPackage.models.find(
(m) => m.name === "Foo"
) as any,
rlcTypeAlias: "FooRest",
rlcTypeName: "Foo",
deserializerFunctionName: "deserializeFoo",
serializerFunctionName: "serializeFoo"
},
Bar: {
type: sdkContext.experimental_sdkPackage.models.find(
(m) => m.name === "Bar"
) as any,
rlcTypeAlias: "BarRest",
rlcTypeName: "Bar",
deserializerFunctionName: "deserializeBar",
serializerFunctionName: "serializeBar"
},
Baz: {
type: sdkContext.experimental_sdkPackage.models.find(
(m) => m.name === "Baz"
) as any,
rlcTypeAlias: "BazRest",
rlcTypeName: "Baz",
deserializerFunctionName: "deserializeBaz",
serializerFunctionName: "serializeBaz"
},
OptionalFoo: {
type: sdkContext.experimental_sdkPackage.models.find(
(m) => m.name === "OptionalFoo"
) as any,
rlcTypeAlias: "OptionalFooRest",
rlcTypeName: "OptionalFoo",
deserializerFunctionName: "deserializeOptionalFoo",
serializerFunctionName: "serializeOptionalFoo"
}
};
return {
booleanArray: getSerializeOptions(
booleanArray,
sdkContext,
serializerMap,
mode
),
cyclicArray: getSerializeOptions(
cyclicArray,
sdkContext,
serializerMap,
mode
),
dateArray: getSerializeOptions(dateArray, sdkContext, serializerMap, mode),
modelArray: getSerializeOptions(
modelArray,
sdkContext,
serializerMap,
mode
),
numericArray: getSerializeOptions(
numericArray,
sdkContext,
serializerMap,
mode
),
stringArray: getSerializeOptions(
stringArray,
sdkContext,
serializerMap,
mode
),
optionalPropertiesArray: getSerializeOptions(
optionalPropertiesArray,
sdkContext,
serializerMap,
mode
)
};
}
function getSerializeOptions<T>(
type: T,
context: SdkContext,
serializerMap: SerializerMap,
mode: "serialize" | "deserialize"
) {
const functionType =
mode === "serialize" ? UsageFlags.Input : UsageFlags.Output;
return {
dpgContext: context,
type,
valueExpr: "myArray",
functionType,
serializerMap
} as any;
}

Просмотреть файл

@ -1,193 +0,0 @@
import { beforeEach, describe, it, expect } from "vitest";
import { createMyTestRunner, createTcgcContext } from "./test-hots.js";
import {
SdkArrayType,
SdkContext,
SdkDatetimeType
} from "@azure-tools/typespec-client-generator-core";
import { EmitContext, UsageFlags } from "@typespec/compiler";
import { BasicTestRunner } from "@typespec/compiler/testing";
import { SerializeTypeOptions } from "../../../src/modular/serialization/serializers.js";
import { serializeDatetime } from "../../../src/modular/serialization/serializeDateTime.js";
describe("serializeDateTime", () => {
let runner: BasicTestRunner;
beforeEach(async () => {
runner = await createMyTestRunner();
});
describe("serialization", () => {
let testArrays: TestArrays;
beforeEach(async () => {
testArrays = await getSdkDateTimes(runner, "serialize");
});
it("should serialize utcDateTime without encoding", () => {
const { utcDateTime } = testArrays;
const result = serializeDatetime(utcDateTime);
expect(result).toEqual("(myDate).toISOString()");
});
it("should serialize rfc3339 with encoding", () => {
const { rfc3339 } = testArrays;
const result = serializeDatetime(rfc3339);
expect(result).toEqual("(myDate).toISOString()");
});
it("should serialize rfc7231 with encoding", () => {
const { rfc7231 } = testArrays;
const result = serializeDatetime(rfc7231);
expect(result).toEqual("(myDate).toUTCString()");
});
it("should serialize unixTimestamp with encoding", () => {
const { unixTimestamp } = testArrays;
const result = serializeDatetime(unixTimestamp);
expect(result).toEqual("(myDate).getTime()");
});
it("should serialize offsetDateTime without encoding", () => {
const { offsetDateTime } = testArrays;
const result = serializeDatetime(offsetDateTime);
expect(result).toEqual("(myDate).toISOString()");
});
});
describe("deserialization", () => {
let testArrays: TestArrays;
beforeEach(async () => {
testArrays = await getSdkDateTimes(runner, "deserialize");
});
it("should deserialize utcDateTime without encoding", () => {
const { utcDateTime } = testArrays;
const result = serializeDatetime(utcDateTime);
expect(result).toEqual("new Date(myDate)");
});
it("should deserialize rfc3339 with encoding", () => {
const { rfc3339 } = testArrays;
const result = serializeDatetime(rfc3339);
expect(result).toEqual("new Date(myDate)");
});
it("should deserialize rfc7231 with encoding", () => {
const { rfc7231 } = testArrays;
const result = serializeDatetime(rfc7231);
expect(result).toEqual("new Date(myDate)");
});
it("should deserialize unixTimestamp with encoding", () => {
const { unixTimestamp } = testArrays;
const result = serializeDatetime(unixTimestamp);
expect(result).toEqual("new Date(myDate)");
});
it("should deserialize offsetDateTime without encoding", () => {
const { offsetDateTime } = testArrays;
const result = serializeDatetime(offsetDateTime);
expect(result).toEqual("new Date(myDate)");
});
});
});
type DateTimeSerializeOptions = SerializeTypeOptions<
SdkDatetimeType & {
kind: "";
}
>;
interface TestArrays {
utcDateTime: DateTimeSerializeOptions;
rfc3339: DateTimeSerializeOptions;
rfc7231: DateTimeSerializeOptions;
unixTimestamp: DateTimeSerializeOptions;
offsetDateTime: DateTimeSerializeOptions;
}
async function getSdkDateTimes(
runner: BasicTestRunner,
mode: "serialize" | "deserialize"
): Promise<TestArrays> {
const template = ` @service({
title: "Widget Service",
})
namespace DemoService;
@test model Test {
utcDateTime: utcDateTime;
@encode(DateTimeKnownEncoding.rfc3339)
rfc3339: utcDateTime;
@encode(DateTimeKnownEncoding.rfc7231)
rfc7231: utcDateTime;
@encode(DateTimeKnownEncoding.unixTimestamp, int32)
unixTimestamp: utcDateTime;
offsetDateTime: offsetDateTime;
};
op getTest(): Test;
`;
await runner.compile(template);
const context: EmitContext = {
program: runner.program
} as any;
const sdkContext = createTcgcContext(context);
const utcDateTime =
sdkContext.experimental_sdkPackage.models[0].properties.find(
(p) => p.name === "utcDateTime"
)?.type as unknown as SdkDatetimeType;
const offsetDateTime =
sdkContext.experimental_sdkPackage.models[0].properties.find(
(p) => p.name === "offsetDateTime"
)?.type as unknown as SdkDatetimeType;
const rfc3339 = sdkContext.experimental_sdkPackage.models[0].properties.find(
(p) => p.name === "rfc3339"
)?.type as unknown as SdkDatetimeType;
const rfc7231 = sdkContext.experimental_sdkPackage.models[0].properties.find(
(p) => p.name === "rfc7231"
)?.type as unknown as SdkDatetimeType;
const unixTimestamp =
sdkContext.experimental_sdkPackage.models[0].properties.find(
(p) => p.name === "unixTimestamp"
)?.type as unknown as SdkDatetimeType;
return {
utcDateTime: getSerializeOptions(utcDateTime, sdkContext, mode),
offsetDateTime: getSerializeOptions(offsetDateTime, sdkContext, mode),
rfc3339: getSerializeOptions(rfc3339, sdkContext, mode),
rfc7231: getSerializeOptions(rfc7231, sdkContext, mode),
unixTimestamp: getSerializeOptions(unixTimestamp, sdkContext, mode)
};
}
function getSerializeOptions<T>(
type: T,
context: SdkContext,
mode: "serialize" | "deserialize"
) {
const functionType =
mode === "serialize" ? UsageFlags.Input : UsageFlags.Output;
return {
dpgContext: context,
type,
valueExpr: "myDate",
functionType
} as any;
}

Просмотреть файл

@ -1,185 +0,0 @@
import { describe, it, assert, beforeEach } from "vitest";
import { createMyTestRunner, createTcgcContext } from "./test-hots.js";
import { SdkHeaderParameter } from "@azure-tools/typespec-client-generator-core";
import { serializeHeader } from "../../../src/modular/serialization/serializeHeaders.js";
import { EmitContext, UsageFlags } from "@typespec/compiler";
import { BasicTestRunner } from "@typespec/compiler/testing";
import { SerializeTypeOptions } from "../../../src/modular/serialization/serializers.js";
describe("headerSerializer", () => {
let runner: BasicTestRunner;
beforeEach(async () => {
runner = await createMyTestRunner();
});
describe("serialization", () => {
it("should handle a simple header", async () => {
const headerParam = await getSdkHeader(
`@header param: string`,
runner,
"serialize"
);
const result = serializeHeader(headerParam);
assert.equal(result, "param");
});
it("should handle a simple array header", async () => {
const headerParam = await getSdkHeader(
`@header({format: "csv"}) param: string[]`,
runner,
"serialize"
);
const result = serializeHeader(headerParam);
assert.equal(result, 'param.join(",")');
});
it("should handle a numeric array header", async () => {
const headerParam = await getSdkHeader(
`@header({format: "csv"}) param: int32[]`,
runner,
"serialize"
);
const result = serializeHeader(headerParam);
assert.equal(result, `param.join(",")`);
});
it("should handle a boolean array header", async () => {
const headerParam = await getSdkHeader(
`@header({format: "csv"}) param: boolean[]`,
runner,
"serialize"
);
const result = serializeHeader(headerParam);
assert.equal(result, `param.join(",")`);
});
it("should handle a datetime array header", async () => {
const headerParam = await getSdkHeader(
`@header({format: "csv"}) param: utcDateTime[]`,
runner,
"serialize"
);
const result = serializeHeader(headerParam);
assert.equal(
result,
`param.map((e: Date)=>((e).toISOString())).join(",")`
);
});
it("should handle a string array header with tsv format", async () => {
const headerParam = await getSdkHeader(
`@header({format: "tsv"}) param: string[]`,
runner,
"serialize"
);
const result = serializeHeader(headerParam);
assert.equal(result, `param.join("\\t")`);
});
});
describe("deserialization", () => {
it("should handle a simple header", async () => {
const headerParam = await getSdkHeader(
`@header param: string`,
runner,
"deserialize"
);
const result = serializeHeader(headerParam);
assert.equal(result, "param");
});
it("should handle a simple array header", async () => {
const headerParam = await getSdkHeader(
`@header({format: "csv"}) param: string[]`,
runner,
"deserialize"
);
const result = serializeHeader(headerParam);
assert.equal(result, 'param.split(",")');
});
it("should handle a numeric array header", async () => {
const headerParam = await getSdkHeader(
`@header({format: "csv"}) param: int32[]`,
runner,
"deserialize"
);
const result = serializeHeader(headerParam);
assert.equal(result, `param.split(",").map(e => JSON.parse(e))`);
});
it("should handle a boolean array header", async () => {
const headerParam = await getSdkHeader(
`@header({format: "csv"}) param: boolean[]`,
runner,
"deserialize"
);
const result = serializeHeader(headerParam);
assert.equal(result, `param.split(",").map(e => JSON.parse(e))`);
});
it("should handle a datetime array header", async () => {
const headerParam = await getSdkHeader(
`@header({format: "csv"}) param: utcDateTime[]`,
runner,
"deserialize"
);
const result = serializeHeader(headerParam);
assert.equal(result, `param.split(",").map(e => new Date(e))`);
});
});
});
type HeaderType = SerializeTypeOptions<
SdkHeaderParameter & {
kind: "header";
}
>;
async function getSdkHeader(
code: string,
runner: BasicTestRunner,
mode: "serialize" | "deserialize"
): Promise<HeaderType> {
const template = ` @service({
title: "Widget Service",
})
namespace DemoService;
@route("/widgets")
interface Widgets {
@test op foo(${code}): string;
}`;
await runner.compile(template);
const context: EmitContext = {
program: runner.program
} as any;
const sdkContext = createTcgcContext(context);
const headerParam = sdkContext.experimental_sdkPackage.clients[0].methods[0]
.parameters[0] as unknown as SdkHeaderParameter;
const functionType =
mode === "serialize" ? UsageFlags.Input : UsageFlags.Output;
return {
dpgContext: sdkContext,
type: headerParam,
valueExpr: headerParam.name,
functionType
} as any;
}

Просмотреть файл

@ -1,47 +0,0 @@
import {
SdkContext,
createSdkContext
} from "@azure-tools/typespec-client-generator-core";
import { SdkTestLibrary } from "@azure-tools/typespec-client-generator-core/testing";
import { EmitContext } from "@typespec/compiler";
import { createTestHost, createTestWrapper } from "@typespec/compiler/testing";
import { HttpTestLibrary } from "@typespec/http/testing";
export async function createMyTestHost() {
return createTestHost({
libraries: [HttpTestLibrary, SdkTestLibrary]
});
}
export async function createMyTestRunner() {
const host = await createMyTestHost();
return createTestWrapper(host, {
autoUsings: ["TypeSpec.Http", "Azure.ClientGenerator.Core"]
});
}
export function createTcgcContext(
context: EmitContext<Record<string, any>>
): SdkContext {
const tcgcSettings = {
"generate-protocol-methods": true,
"generate-convenience-methods": true,
"flatten-union-as-enum": false,
emitters: [
{
main: "@azure-tools/typespec-ts",
metadata: { name: "@azure-tools/typespec-ts" }
}
]
};
const contextForTcgc = {
...context,
options: {
...context.options,
...tcgcSettings
}
};
return createSdkContext(contextForTcgc);
}

Просмотреть файл

@ -0,0 +1,86 @@
import {
SdkContext,
SdkEmitterOptions,
SdkHttpOperation,
SdkServiceOperation,
createSdkContext,
} from "@azure-tools/typespec-client-generator-core";
import { SdkTestLibrary } from "@azure-tools/typespec-client-generator-core/testing";
import { EmitContext, Program } from "@typespec/compiler";
import { createTestHost, createTestWrapper, TypeSpecTestLibrary } from "@typespec/compiler/testing";
import { HttpTestLibrary } from "@typespec/http/testing";
export async function createMyTestHost() {
return createTestHost({
libraries: [HttpTestLibrary, SdkTestLibrary]
});
}
export interface CreateSdkTestRunnerOptions extends SdkEmitterOptions {
emitterName?: string;
librariesToAdd?: TypeSpecTestLibrary[];
autoUsings?: string[];
packageName?: string;
}
export async function createSdkContextFromTypespec(code: string, options: CreateSdkTestRunnerOptions = {}): Promise<SdkContext<CreateSdkTestRunnerOptions, SdkHttpOperation>>{
const runner = await createMyTestRunner();
await runner.compile(code);
return createSdkContextTestHelper(runner.program, options);
}
export function createSdkContextTestHelper<
TOptions extends Record<string, any> = CreateSdkTestRunnerOptions,
TServiceOperation extends SdkServiceOperation = SdkHttpOperation,
>(
program: Program,
options: TOptions,
sdkContextOption?: any
): SdkContext<TOptions, TServiceOperation> {
const emitContext: EmitContext<TOptions> = {
program: program,
emitterOutputDir: "dummy",
options: options,
getAssetEmitter: null as any,
};
return createSdkContext(
emitContext,
options.emitterName ?? "@azure-tools/typespec-csharp",
sdkContextOption
);
}
export async function createMyTestRunner() {
const host = await createMyTestHost();
return createTestWrapper(host, {
autoUsings: ["TypeSpec.Http", "Azure.ClientGenerator.Core"]
});
}
export function createTcgcContext(
context: EmitContext<Record<string, any>>
): SdkContext {
const tcgcSettings = {
"generate-protocol-methods": true,
"generate-convenience-methods": true,
"flatten-union-as-enum": false,
emitters: [
{
main: "@azure-tools/typespec-ts",
metadata: { name: "@azure-tools/typespec-ts" }
}
]
};
const contextForTcgc = {
...context,
options: {
...context.options,
...tcgcSettings
}
};
return createSdkContext(contextForTcgc);
}

Просмотреть файл

@ -1,34 +0,0 @@
import { describe, it, expect } from "vitest";
import { getCollectionSeparator } from "../../../src/modular/serialization/collectionUtils.js";
describe("getCollectionSeparator", () => {
it("should return ',' for csv", () => {
const result = getCollectionSeparator("csv");
expect(result).toBe(",");
});
it("should return ' ' for ssv", () => {
const result = getCollectionSeparator("ssv");
expect(result).toBe(" ");
});
it("should return '\\t' for tsv", () => {
const result = getCollectionSeparator("tsv");
expect(result).toBe("\t");
});
it("should return '|' for pipes", () => {
const result = getCollectionSeparator("pipes");
expect(result).toBe("|");
});
it("should return '&' for multi", () => {
const result = getCollectionSeparator("multi");
expect(result).toBe("&");
});
it("should return ',' by default", () => {
const result = getCollectionSeparator(undefined);
expect(result).toBe(",");
});
});

Просмотреть файл

@ -1,941 +0,0 @@
import { describe, it, assert } from "vitest";
import { format } from "prettier";
import {
buildLroReturnType,
getDeserializePrivateFunction,
getOperationFunction,
getOperationSignatureParameters,
getPagingOnlyOperationFunction,
getRLCLroLogicalResponse,
getRLCResponseType,
getSendPrivateFunction
} from "../../../src/modular/serialization/operationHelpers.js";
import {
Imports as RuntimeImports,
OperationResponse
} from "@azure-tools/rlc-common";
import { SerializerMap } from "../../../src/modular/serialization/util.js";
import {
Operation,
Parameter,
Response
} from "../../../src/modular/modularCodeModel.js";
import {
SdkModelType,
UsageFlags
} from "@azure-tools/typespec-client-generator-core";
describe("Operation Helpers", () => {
describe("getRLCResponseType", () => {
it("should return the response type for a given operation", () => {
const operation: OperationResponse = {
operationGroup: "MyOperationGroup",
operationName: "MyOperation",
path: "/my/operation",
responses: [
{
statusCode: "200",
predefinedName: "MyResponseType"
}
]
};
const result = getRLCResponseType(operation as any);
assert.equal(result, "MyResponseType");
});
it("should return the response type for a given operation that has no predefinedName", () => {
const operation: OperationResponse = {
operationGroup: "MyOperationGroup",
operationName: "MyOperation",
path: "/my/operation",
responses: [
{
statusCode: "200"
}
]
};
const result = getRLCResponseType(operation as any);
assert.equal(result, "MyOperationGroupMyOperation200Response");
});
it("should return the response type when multiple responses", () => {
const operation: OperationResponse = {
operationGroup: "MyOperationGroup",
operationName: "MyOperation",
path: "/my/operation",
responses: [
{
statusCode: "200"
},
{
statusCode: "201"
}
]
};
const result = getRLCResponseType(operation as any);
assert.equal(
result,
"MyOperationGroupMyOperation200Response | MyOperationGroupMyOperation201Response"
);
});
});
describe("getRLCLroLogicalResponse", () => {
it("should return the LRO logical response for a given operation", () => {
const operation: OperationResponse = {
operationGroup: "MyOperationGroup",
operationName: "MyOperation",
path: "/my/operation",
responses: [
{
statusCode: "201",
predefinedName: "MyLogicalResponse"
}
]
};
const result = getRLCLroLogicalResponse(operation);
assert.equal(result, "MyLogicalResponse");
});
it("should fallback to any if no LogicalResponse found", () => {
const operation: OperationResponse = {
operationGroup: "MyOperationGroup",
operationName: "MyOperation",
path: "/my/operation",
responses: [
{
statusCode: "201"
}
]
};
const result = getRLCLroLogicalResponse(operation);
assert.equal(result, "any");
});
});
describe("getSendPrivateFunction", () => {
it("should generate an operation with no parameters", () => {
const dpgContext = {} as any;
const operation: Operation = { ...mockOperation, parameters: [] };
const clientType = "";
let serializerMap: SerializerMap | undefined;
const runtimeImports: RuntimeImports = {} as any;
const result = getSendPrivateFunction(
dpgContext,
operation,
clientType,
serializerMap,
runtimeImports
);
assert.equal(result.name, "_myOperationSend");
assert.equal(result.parameters?.length, 2);
assert.equal(result.parameters?.[0].name, "context");
assert.equal(result.parameters?.[1].name, "options");
assert.equal(result.parameters?.[1].type, "MyOperationOptionalParams");
});
it("should generate an operation with parameters", () => {
const operation: Operation = {
...mockOperation
};
const result = getSendPrivateFunction(
{} as any,
operation,
"",
undefined,
{} as any
);
assert.equal(result.name, "_myOperationSend");
assert.equal(result.parameters?.[0].name, "context");
assert.equal(result.parameters?.[1].name, "niceParam");
assert.equal(result.parameters?.[2].name, "options");
assert.equal(result.parameters?.[2].type, "MyOperationOptionalParams");
assert.isTrue(
(result.statements as string[])
.join("\n")
.includes(`return context.path("/my/operation"`)
);
});
});
describe("getDeserializePrivateFunction", () => {
it("should generate a deserialize function", () => {
const dpgContext = {} as any;
const operation: Operation = {
...mockOperation,
parameters: []
};
const runtimeImports: RuntimeImports = {} as any;
const result = getDeserializePrivateFunction(
dpgContext,
operation,
false,
false,
runtimeImports,
{} as any
);
assert.equal(result.name, "_myOperationDeserialize");
assert.equal(result.parameters?.length, 1);
assert.equal(result.parameters?.[0].name, "result");
});
it("should generate a deserialize function when only error response is provided", () => {
const dpgContext = {} as any;
const operation: Operation = {
...mockOperation,
parameters: [],
responses: [
{
statusCodes: ["default"],
type: { name: "MyErrorType", type: "model" },
discriminator: "error",
headers: []
}
]
};
const runtimeImports: RuntimeImports = {} as any;
const result = getDeserializePrivateFunction(
dpgContext,
operation,
false,
false,
runtimeImports,
{} as any
);
assert.deepEqual(result.statements, ["return result.body"]);
});
it("should generate a deserialize function when both success and error response is provided", () => {
const dpgContext = {} as any;
const operation: Operation = {
...mockOperation,
parameters: [],
responses: [
{
statusCodes: [201],
type: { name: "MyGoodType", type: "model" },
discriminator: "",
headers: []
},
{
statusCodes: ["default"],
type: { name: "MyErrorType", type: "model" },
discriminator: "error",
headers: []
}
]
};
const runtimeImports: RuntimeImports = {} as any;
const result = getDeserializePrivateFunction(
dpgContext,
operation,
false,
false,
runtimeImports,
{} as any
);
assert.deepEqual(result.statements, [
'if(result.status !== "201"){',
"throw createRestError(result);",
"}",
"return result.body"
]);
});
it("should generate a deserialize function when only success response is provided", () => {
const dpgContext = {} as any;
const operation: Operation = {
...mockOperation,
parameters: [],
responses: [
{
statusCodes: [200],
type: { name: "MyGoodType", type: "model" },
discriminator: "",
headers: []
}
]
};
const runtimeImports: RuntimeImports = {} as any;
const result = getDeserializePrivateFunction(
dpgContext,
operation,
false,
false,
runtimeImports,
{} as any
);
assert.deepEqual(result.statements, [
'if(result.status !== "200"){',
"throw createRestError(result);",
"}",
"return result.body"
]);
});
it("should generate a deserialize function when there is no response", () => {
const dpgContext = {} as any;
const operation: Operation = {
...mockOperation,
parameters: [],
responses: []
};
const runtimeImports: RuntimeImports = {} as any;
const result = getDeserializePrivateFunction(
dpgContext,
operation,
false,
false,
runtimeImports,
{} as any
);
assert.equal(result.name, "_myOperationDeserialize");
assert.equal(result.parameters?.length, 1);
assert.equal(result.parameters?.[0].name, "result");
assert.equal(result.returnType, "Promise<void>");
// There is nothing to deserialize, so return?
assert.deepEqual(result.statements, ["return"]);
});
// This is for the scenario where there is more than one client and need to use the
// right isUnexpected helper
it("should generate a deserialize function that needs subclient", () => {
const dpgContext = {} as any;
const operation: Operation = {
...mockOperation,
parameters: []
};
const runtimeImports: RuntimeImports = {} as any;
const result = getDeserializePrivateFunction(
dpgContext,
operation,
true,
true,
runtimeImports,
{} as any
);
assert.equal(result.name, "_myOperationDeserialize");
assert.equal(result.parameters?.length, 1);
assert.equal(result.parameters?.[0].name, "result");
assert.isTrue(
(result.statements as string[]).some((s) =>
s.includes("if(UnexpectedHelper.isUnexpected(result))")
)
);
});
it("should generate a deserialize function that needs isUnexpected", () => {
const dpgContext = {} as any;
const operation: Operation = {
...mockOperation,
parameters: []
};
const runtimeImports: RuntimeImports = {} as any;
const result = getDeserializePrivateFunction(
dpgContext,
operation,
false,
true,
runtimeImports,
{} as any
);
assert.equal(result.name, "_myOperationDeserialize");
assert.equal(result.parameters?.length, 1);
assert.equal(result.parameters?.[0].name, "result");
assert.deepEqual(result.statements, [
"if(isUnexpected(result)){",
"throw createRestError(result);",
"}",
"return {...(result.body)}"
]);
});
it("should generate a deserialize when operation is lroOnly and non-patch and no finalResultPath", () => {
const dpgContext = {} as any;
const operation: Operation = {
...mockOperation,
parameters: [mockParameter],
discriminator: "lro",
method: "get",
lroMetadata: {
finalResultPath: undefined
}
};
const runtimeImports: RuntimeImports = {} as any;
const result = getDeserializePrivateFunction(
dpgContext,
operation,
false,
false,
runtimeImports,
{} as any
);
// Not enough information to figure out the type so falling back to any
assert.deepEqual(result.statements, [
'if(result.status !== "200"){',
"throw createRestError(result);",
"}",
"result = result as any;",
"return"
]);
assert.equal(result.name, "_myOperationDeserialize");
assert.equal(result.returnType, "Promise<void>");
});
it("should generate a deserialize when operation is lroOnly and non-patch and has finalResultPath", () => {
const dpgContext = {} as any;
const operation: Operation = {
...mockOperation,
parameters: [mockParameter],
discriminator: "lro",
method: "get",
lroMetadata: {
finalResultPath: "result"
}
};
const runtimeImports: RuntimeImports = {} as any;
const result = getDeserializePrivateFunction(
dpgContext,
operation,
false,
false,
runtimeImports,
{} as any
);
assert.equal(result.name, "_myOperationDeserialize");
assert.equal(result.returnType, "Promise<void>");
assert.deepEqual(result.statements, [
'if(result.status !== "200"){',
"throw createRestError(result);",
"}",
"result = result as any;",
`if(result?.body?.result === undefined) {
throw createRestError(\`Expected a result in the response at position \"result.body.result\"\`, result);
}
`,
"return"
]);
});
it("should generate a deserialize when operation is lroOnly and non-patch and no finalResultPath", () => {
const dpgContext = {} as any;
const operation: Operation = {
...mockOperation,
parameters: [mockParameter],
discriminator: "lro",
method: "get",
lroMetadata: {
finalResultPath: undefined
}
};
const runtimeImports: RuntimeImports = {} as any;
const result = getDeserializePrivateFunction(
dpgContext,
operation,
false,
false,
runtimeImports,
{} as any
);
assert.equal(result.name, "_myOperationDeserialize");
assert.equal(result.returnType, "Promise<void>");
assert.deepEqual(result.statements, [
'if(result.status !== "200"){',
"throw createRestError(result);",
"}",
"result = result as any;",
"return"
]);
});
it("should generate a deserialize when operation is lroOnly and patch", () => {
const dpgContext = {} as any;
const operation: Operation = {
...mockOperation,
parameters: [mockParameter],
discriminator: "lro",
method: "patch"
};
const runtimeImports: RuntimeImports = {} as any;
const result = getDeserializePrivateFunction(
dpgContext,
operation,
false,
false,
runtimeImports,
{} as any
);
assert.equal(result.name, "_myOperationDeserialize");
assert.equal(result.returnType, "Promise<MyResponseType>");
});
});
describe("getOperationSignatureParameters", () => {
it("should return default params context and options", () => {
const operation = { ...mockOperation, parameters: [] };
const result = getOperationSignatureParameters(
operation,
"MyClientContext"
);
assert.equal(result.length, 2);
assert.equal(result[0].name, "context");
assert.equal(Boolean(result[0].hasQuestionToken), false);
assert.equal(result[0].type, "MyClientContext");
assert.equal(result[1].name, "options");
assert.equal(result[1].type, "MyOperationOptionalParams");
assert.equal(result[1].initializer, `{ requestOptions: {} }`);
});
it("should return the the default params plus the operation param", () => {
const operation: Operation = {
...mockOperation,
parameters: [
{
...mockParameter,
clientName: "operationParam",
restApiName: "uglyName"
}
]
};
const result = getOperationSignatureParameters(
operation,
"MyClientContext"
);
assert.equal(result.length, 3);
assert.equal(result[0].name, "context");
assert.equal(Boolean(result[0].hasQuestionToken), false);
assert.equal(result[0].type, "MyClientContext");
assert.equal(result[1].name, "operationParam");
assert.equal(result[2].name, "options");
assert.equal(result[2].type, "MyOperationOptionalParams");
assert.equal(result[2].initializer, `{ requestOptions: {} }`);
});
it("should not include non-method level params", () => {
const operation: Operation = {
...mockOperation,
parameters: [
{
...mockParameter,
clientName: "operationParam",
restApiName: "uglyName",
implementation: "Client"
}
]
};
const result = getOperationSignatureParameters(
operation,
"MyClientContext"
);
assert.equal(result.length, 2);
assert.equal(result[0].name, "context");
assert.equal(Boolean(result[0].hasQuestionToken), false);
assert.equal(result[0].type, "MyClientContext");
assert.equal(result[1].name, "options");
assert.equal(result[1].type, "MyOperationOptionalParams");
assert.equal(result[1].initializer, `{ requestOptions: {} }`);
});
it("should not include constant params", () => {
const operation: Operation = {
...mockOperation,
parameters: [
{
...mockParameter,
clientName: "operationParam",
restApiName: "uglyName",
type: { type: "constant", value: "constantValue" }
}
]
};
const result = getOperationSignatureParameters(
operation,
"MyClientContext"
);
assert.equal(result.length, 2);
assert.equal(result[0].name, "context");
assert.equal(Boolean(result[0].hasQuestionToken), false);
assert.isFalse(result.some((p) => p.name === "operationParam"));
assert.equal(result[0].type, "MyClientContext");
assert.equal(result[1].name, "options");
assert.equal(result[1].type, "MyOperationOptionalParams");
assert.equal(result[1].initializer, `{ requestOptions: {} }`);
});
it("should include the body parameter", () => {
const operation: Operation = {
...mockOperation,
parameters: [],
bodyParameter: {
clientName: "niceBodyName",
restApiName: "uglyBodyName",
contentTypes: ["application/json"],
description: "",
inOverload: false,
isBinaryPayload: false,
location: "body",
optional: false,
tcgcType: {
apiVersions: [],
contentTypes: ["application/json"],
kind: "body",
name: "MyBodyType",
nullable: false,
optional: false,
defaultContentType: "application/json",
correspondingMethodParams: [],
type: {} as any,
nameInClient: "niceBodyName",
isApiVersionParam: false,
isGeneratedName: false,
onClient: false
},
type: { type: "model", name: "MyBodyType" }
}
};
const result = getOperationSignatureParameters(
operation,
"MyClientContext"
);
assert.equal(result.length, 3);
assert.equal(result[0].name, "context");
assert.equal(Boolean(result[0].hasQuestionToken), false);
assert.equal(result[0].type, "MyClientContext");
assert.equal(result[1].name, "niceBodyName");
assert.equal(result[1].type, "MyBodyType");
assert.equal(result[2].name, "options");
assert.equal(result[2].type, "MyOperationOptionalParams");
assert.equal(result[2].initializer, `{ requestOptions: {} }`);
});
});
describe("getOperationFunction", () => {
it("should generate a simple operation function", () => {
const operation: Operation = {
...mockOperation,
parameters: []
};
const result = getOperationFunction(operation, "MyClient");
assert.equal(result.name, "myOperation");
// Only context and options
assert.equal(result.parameters?.length, 2);
assert.equal(result.parameters?.[0].name, "context");
assert.equal(result.parameters?.[1].name, "options");
assert.equal(result.parameters?.[1].type, "MyOperationOptionalParams");
assert.deepEqual(result.statements, [
"const result = await _myOperationSend(context, options);",
"return _myOperationDeserialize(result);"
]);
});
it("should generate a function for a pageable operation", async () => {
const operation: Operation = {
...mockOperation,
discriminator: "paging",
parameters: []
};
const result = getOperationFunction(operation, "MyClient");
assert.equal(result.name, "myOperation");
// Only context and options
assert.equal(result.parameters?.length, 2);
assert.equal(result.parameters?.[0].name, "context");
assert.equal(result.parameters?.[1].name, "options");
assert.equal(result.parameters?.[1].type, "MyOperationOptionalParams");
const formattedExpected =
await reformatString(`return buildPagedAsyncIterator(
context,
() => _myOperationSend(context, options),
_myOperationDeserialize,);`);
const formattedActual = await Promise.all(
(result.statements as string[]).map(
async (s) => await reformatString(s)
)
);
assert.deepEqual(formattedActual, [formattedExpected]);
assert.equal(
result.returnType,
"PagedAsyncIterableIterator<MyResponseType>"
);
});
it("should generate a function for an lro operation", async () => {
const operation: Operation = {
...mockOperation,
discriminator: "lro",
parameters: []
};
const result = getOperationFunction(operation, "MyClient");
assert.equal(result.name, "myOperation");
// Only context and options
assert.equal(result.parameters?.length, 2);
assert.equal(result.parameters?.[0].name, "context");
assert.equal(result.parameters?.[1].name, "options");
assert.equal(result.parameters?.[1].type, "MyOperationOptionalParams");
const formattedExpected = await reformatString(`
return getLongRunningPoller(
context,
_myOperationDeserialize, {
updateIntervalInMs: options?.updateIntervalInMs,
abortSignal: options?.abortSignal,
getInitialResponse: () => _myOperationSend(context, options)}
) as PollerLike<OperationState<void>, void>;`);
const formattedActual = await Promise.all(
(result.statements as string[]).map(
async (s) => await reformatString(s)
)
);
assert.deepEqual(formattedActual, [formattedExpected]);
assert.equal(result.returnType, "PollerLike<OperationState<void>, void>");
});
});
describe("buildLroReturnType", () => {
it("should return the correct return type for an lro operation without extra metadata", () => {
const operation: Operation = { ...mockOperation, discriminator: "lro" };
const result = buildLroReturnType(operation);
assert.equal(result.type, "void");
});
it("should return the correct return type for an lro operation with extra metadata", () => {
const operation: Operation = {
...mockOperation,
discriminator: "lro",
lroMetadata: {
finalResultPath: "result",
finalResult: { type: "string", name: "resourceName" }
}
};
const result = buildLroReturnType(operation);
assert.equal(result.type, "string");
assert.equal(result.name, "resourceName");
});
});
describe("getPagingOnlyOperationFunction", () => {
it("should generate a function for a pageable operation when there are no responses", async () => {
const operation: Operation = {
...mockOperation,
parameters: [],
responses: [],
discriminator: "paging"
};
const result = getPagingOnlyOperationFunction(operation, "MyClient");
assert.equal(result.returnType, "PagedAsyncIterableIterator<void>");
const formattedActualStatements = await reformatString(
result.statements.join("\n")
);
const formattedExpectedStatements = await reformatString(`
return buildPagedAsyncIterator(
context,
() => _myOperationSend(context, options),
_myOperationDeserialize
)`);
assert.equal(formattedActualStatements, formattedExpectedStatements);
});
it("should generate a function for a pageable operation", async () => {
const operation: Operation = {
...mockOperation,
parameters: [],
discriminator: "paging"
};
const result = getPagingOnlyOperationFunction(operation, "MyClient");
assert.equal(
result.returnType,
"PagedAsyncIterableIterator<MyResponseType>"
);
const formattedActualStatements = await reformatString(
result.statements.join("\n")
);
const formattedExpectedStatements = await reformatString(`
return buildPagedAsyncIterator(
context,
() => _myOperationSend(context, options),
_myOperationDeserialize
)`);
assert.equal(formattedActualStatements, formattedExpectedStatements);
});
it("should generate a function for a pageable operation when itemName is present", async () => {
const operation: Operation = {
...mockOperation,
parameters: [],
discriminator: "paging",
itemName: "myValues"
};
const result = getPagingOnlyOperationFunction(operation, "MyClient");
assert.equal(
result.returnType,
"PagedAsyncIterableIterator<MyResponseType>"
);
const formattedActualStatements = await reformatString(
result.statements.join("\n")
);
const formattedExpectedStatements = await reformatString(`
return buildPagedAsyncIterator(
context,
() => _myOperationSend(context, options),
_myOperationDeserialize,
{ itemName: "myValues" },
)`);
assert.equal(formattedActualStatements, formattedExpectedStatements);
});
it("should generate a function for a pageable operation when continuationTokenName is present", async () => {
const operation: Operation = {
...mockOperation,
parameters: [],
discriminator: "paging",
continuationTokenName: "foo"
};
const result = getPagingOnlyOperationFunction(operation, "MyClient");
assert.equal(
result.returnType,
"PagedAsyncIterableIterator<MyResponseType>"
);
const formattedActualStatements = await reformatString(
result.statements.join("\n")
);
const formattedExpectedStatements = await reformatString(`
return buildPagedAsyncIterator(
context,
() => _myOperationSend(context, options),
_myOperationDeserialize,
{ nextLinkName: "foo" },
)`);
assert.equal(formattedActualStatements, formattedExpectedStatements);
});
});
});
const mockResponse: Response = {
discriminator: "",
headers: [],
statusCodes: [200],
type: {
name: "MyResponseType",
type: "model",
tcgcType: {
apiVersions: [],
crossLanguageDefinitionId: "",
isError: false,
isGeneratedName: false,
isFormDataType: false,
kind: "model",
name: "MyResponseType",
nullable: false,
properties: [],
usage: UsageFlags.Output
} as SdkModelType
}
};
const mockParameter: Parameter = {
clientName: "niceParam",
description: "",
implementation: "Method",
inOverload: false,
location: "query",
optional: false,
restApiName: "uglyName",
type: { name: "uglyName", type: "model" } as any,
tcgcType: {
name: "niceParam",
kind: "model",
properties: [],
apiVersions: [],
crossLanguageDefinitionId: "",
isGeneratedName: false,
usage: UsageFlags.Input,
isFormDataType: false,
nullable: false,
isError: false
} as SdkModelType
};
const mockOperation: Operation = {
apiVersions: [],
bodyParameter: undefined,
description: "",
discriminator: "",
exceptions: [],
groupName: "",
isOverload: false,
name: "MyOperation",
method: "get",
namespaceHierarchies: [],
overloads: [],
parameters: [mockParameter],
responses: [mockResponse],
url: "/my/operation",
summary: "",
rlcResponse: {
operationGroup: "MyOperationGroup",
operationName: "MyOperation",
path: "/my/operation",
responses: [
{
statusCode: "200"
}
]
}
};
async function reformatString(str: string) {
return format(str, { parser: "typescript" });
}

Просмотреть файл

@ -1,151 +0,0 @@
import { assert } from "chai";
import {
getEncodingFormat,
getModularTypeId,
getParameterTypePropertyName,
getRLCTypeId,
getReturnTypePropertyName
} from "../../../src/modular/serialization/util.js";
import { UsageFlags } from "@typespec/compiler";
import { describe, it, afterEach, vi } from "vitest";
import { SdkModelType } from "@azure-tools/typespec-client-generator-core";
// Mock the module
vi.mock("@azure-tools/typespec-client-generator-core", async () => {
const actual = await vi.importActual(
"@azure-tools/typespec-client-generator-core"
);
return {
...actual,
getWireName: vi.fn(),
getLibraryName: vi.fn()
};
});
type SdkType = any;
describe("serialization utils", () => {
describe("getEncodingFormat", () => {
it("should return the encoding format for output", () => {
const type = { encode: "base64" } as SdkType & { kind: "bytes" };
const result = getEncodingFormat(UsageFlags.Output, type);
assert.equal(result, "base64");
});
it("should return the encoding format for input when it is supported", () => {
const type = { encode: "base64url" } as SdkType & { kind: "bytes" };
const result = getEncodingFormat(UsageFlags.Input, type);
assert.equal(result, "base64url");
});
it("should fallback to base64 when the provided encode is not supported", () => {
const type = { encode: "somerandom" } as SdkType & { kind: "bytes" };
const result = getEncodingFormat(UsageFlags.Input, type);
assert.equal(result, "base64");
});
});
describe("getModularTypeId", () => {
it("should return the name of the type", () => {
const type: SdkModelType = {
name: "myType",
kind: "model"
} as SdkModelType;
const result = getModularTypeId(type);
assert.equal(result, "myType");
});
});
// this needs the compiled types
describe("getParameterTypePropertyName", () => {
afterEach(() => {
// Restore all mocks after each test to ensure clean state
vi.restoreAllMocks();
});
it("should return the wire name for output", async () => {
const { getWireName, getLibraryName } = await import(
"@azure-tools/typespec-client-generator-core"
);
(getWireName as any).mockImplementation(() => "wireName");
(getLibraryName as any).mockImplementation(() => "libraryName");
const dpgContext = {} as any;
const functionType = UsageFlags.Output;
const p = { __raw: {} } as any;
const result = getParameterTypePropertyName(dpgContext, functionType, p);
assert.equal(result, "wireName");
});
it("should return the library name for input", async () => {
const { getWireName, getLibraryName } = await import(
"@azure-tools/typespec-client-generator-core"
);
(getWireName as any).mockImplementation(() => "wireName");
(getLibraryName as any).mockImplementation(() => "libraryName");
const dpgContext = {} as any;
const functionType = UsageFlags.Input;
const p = { __raw: {} } as any;
const result = getParameterTypePropertyName(dpgContext, functionType, p);
assert.equal(result, "libraryName");
});
});
// this needs the compiled types
describe("getReturnTypePropertyName", () => {
afterEach(() => {
// Restore all mocks after each test to ensure clean state
vi.restoreAllMocks();
});
it("should return the library name for output", async () => {
const { getWireName, getLibraryName } = await import(
"@azure-tools/typespec-client-generator-core"
);
(getWireName as any).mockImplementation(() => "wireName");
(getLibraryName as any).mockImplementation(() => "libraryName");
const result = getReturnTypePropertyName(
{} as any,
UsageFlags.Output,
{} as any
);
assert.equal(result, "libraryName");
});
it("should return the wire name for input", async () => {
const { getWireName, getLibraryName } = await import(
"@azure-tools/typespec-client-generator-core"
);
(getWireName as any).mockImplementation(() => "wireName");
(getLibraryName as any).mockImplementation(() => "libraryName");
const result = getReturnTypePropertyName(
{} as any,
UsageFlags.Input,
{} as any
);
assert.equal(result, "wireName");
});
});
describe("getRLCTypeId", () => {
it("should return the type name for input", async () => {
const dpgContext = {} as any;
const type = { name: "myType", kind: "model" } as SdkType;
const result = getRLCTypeId(dpgContext, type);
assert.equal(result, "myType");
});
// TODO: Enable when getUsage gets updated. Currently it always returns TCGCUsageFlags.Input | TCGCUsageFlags.Output
it.skip("should return the type name for output", async () => {
const dpgContext = {} as any;
const type = { name: "myType" } as SdkType;
const result = getRLCTypeId(dpgContext, type);
assert.equal(result, "myType Rest");
});
});
});

Просмотреть файл

@ -33,8 +33,6 @@ import { buildSerializeUtils } from "../../src/modular/buildSerializeUtils.js";
import { buildClientContext } from "../../src/modular/buildClientContext.js";
import { buildClassicalClient } from "../../src/modular/buildClassicalClient.js";
import { Project } from "ts-morph";
import { buildSerializers } from "../../src/modular/serialization/index.js";
import { env } from "process";
export async function emitPageHelperFromTypeSpec(
tspContent: string,
@ -446,13 +444,6 @@ export async function emitModularOperationsFromTypeSpec(
dpgContext,
modularCodeModel,
false,
env["EXPERIMENTAL_TYPESPEC_TS_SERIALIZATION"]
? buildSerializers(
dpgContext,
modularCodeModel,
modularCodeModel.clients[0]
)
: undefined
);
if (mustEmptyDiagnostic && dpgContext.program.diagnostics.length > 0) {
throw dpgContext.program.diagnostics;

Просмотреть файл

@ -16,6 +16,7 @@ import { prettierTypeScriptOptions } from "../../src/lib.js";
import { createContextWithDefaultOptions } from "../../src/index.js";
import { provideContext } from "../../src/contextManager.js";
import { Project } from "ts-morph";
import { provideSdkTypes } from "../../src/framework/hooks/sdkTypes.js";
export async function createRLCEmitterTestHost() {
return createTestHost({
@ -91,20 +92,16 @@ export function createDpgContextTestHelper(
program: Program,
enableModelNamespace = false
): SdkContext {
const context = createContextWithDefaultOptions({
program
} as EmitContext);
provideContext("emitContext", {
compilerContext: context as any,
tcgcContext: context
});
provideContext("rlcMetaTree", new Map());
provideContext("modularMetaTree", new Map());
provideContext("symbolMap", new Map());
provideContext("outputProject", new Project());
return {
const context = createContextWithDefaultOptions({
program
} as EmitContext);
const sdkContext = {
...context,
program,
rlcOptions: { flavor: "azure", enableModelNamespace },
@ -112,6 +109,15 @@ export function createDpgContextTestHelper(
emitterName: "@azure-tools/typespec-ts",
originalProgram: program
} as SdkContext;
provideContext("emitContext", {
compilerContext: context as any,
tcgcContext: sdkContext
});
provideSdkTypes(context.experimental_sdkPackage);
return sdkContext;
}
export async function assertEqualContent(
@ -121,11 +127,15 @@ export async function assertEqualContent(
) {
assert.strictEqual(
await format(
ignoreWeirdLine ? actual.replace(/$\n^/g, "").replace(/\s+/g, " ") : actual,
ignoreWeirdLine
? actual.replace(/$\n^/g, "").replace(/\s+/g, " ")
: actual,
prettierTypeScriptOptions
),
await format(
ignoreWeirdLine ? expected.replace(/$\n^/g, "").replace(/\s+/g, " ") : expected,
ignoreWeirdLine
? expected.replace(/$\n^/g, "").replace(/\s+/g, " ")
: expected,
prettierTypeScriptOptions
)
);