Introduce SdkType context (#2662)
* 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:
Родитель
9047f97ea8
Коммит
1de92fdbe7
|
@ -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
|
||||
)
|
||||
);
|
||||
|
|
Загрузка…
Ссылка в новой задаче