Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tcgc] add client initialization usage #1664

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: breaking
packages:
- "@azure-tools/typespec-client-generator-core"
---

Make `SdkInitializationType` have an `access` property to denote if publicly instantiable, and a `model` property to show the client options model
20 changes: 2 additions & 18 deletions packages/typespec-client-generator-core/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ import {
SdkContext,
SdkEmitterOptions,
SdkHttpOperation,
SdkInitializationType,
SdkMethodParameter,
SdkModelPropertyType,
SdkOperationGroup,
SdkServiceOperation,
TCGCContext,
Expand Down Expand Up @@ -1023,21 +1020,8 @@ export const $clientInitialization: ClientInitializationDecorator = (
export function getClientInitialization(
context: TCGCContext,
entity: Namespace | Interface,
): SdkInitializationType | undefined {
const model = getScopedDecoratorData(context, clientInitializationKey, entity);
if (!model) return model;
const sdkModel = getSdkModel(context, model);
const initializationProps = sdkModel.properties.map(
(property: SdkModelPropertyType): SdkMethodParameter => {
property.onClient = true;
property.kind = "method";
return property as SdkMethodParameter;
},
);
return {
...sdkModel,
properties: initializationProps,
};
): Model | undefined {
return getScopedDecoratorData(context, clientInitializationKey, entity);
}

const paramAliasKey = createStateSymbol("paramAlias");
Expand Down
14 changes: 9 additions & 5 deletions packages/typespec-client-generator-core/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,24 +350,26 @@ export interface SdkUnionType<TValueType extends SdkTypeBase = SdkType> extends

export type AccessFlags = "internal" | "public";

export interface SdkModelType extends SdkTypeBase {
export interface SdkModelType<TPropertyType extends SdkModelPropertyType = SdkModelPropertyType>
extends SdkTypeBase {
kind: "model";
properties: SdkModelPropertyType[];
properties: TPropertyType[];
name: string;
isGeneratedName: boolean;
access: AccessFlags;
usage: UsageFlags;
additionalProperties?: SdkType;
discriminatorValue?: string;
discriminatedSubtypes?: Record<string, SdkModelType>;
discriminatorProperty?: SdkModelPropertyType;
discriminatorProperty?: TPropertyType;
baseModel?: SdkModelType;
crossLanguageDefinitionId: string;
apiVersions: string[];
}

export interface SdkInitializationType extends SdkModelType {
properties: SdkParameter[];
export interface SdkInitializationType {
access: AccessFlags;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i've realized we can't modify the .access on our existing SdkInitializationType to denote whether a client is publicly or internally initialized. There is both the context that the client options model is used, and the client options model itself. In this case, the context that the client options model is used has access that depends on whether it's a client or og. However, that doesn't change the actual access of the client options model itself.

This is a breaking change, so want to call this out and will call it out more in our sync

model: SdkModelType<SdkParameter>;
}

export interface SdkCredentialType extends SdkTypeBase {
Expand Down Expand Up @@ -713,6 +715,8 @@ export enum UsageFlags {
Json = 1 << 8,
// Set when model is used in conjunction with an application/xml content type.
Xml = 1 << 9,
// Set when model is used as client initialization model
ClientInitialization = 1 << 10,
}

interface SdkExampleBase {
Expand Down
81 changes: 30 additions & 51 deletions packages/typespec-client-generator-core/src/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
createDiagnosticCollector,
Diagnostic,
getDoc,
getNamespaceFullName,
getService,
getSummary,
ignoreDiagnostics,
Expand Down Expand Up @@ -33,7 +32,6 @@ import {
SdkEndpointType,
SdkEnumType,
SdkHttpOperation,
SdkInitializationType,
SdkLroPagingServiceMethod,
SdkLroServiceMetadata,
SdkLroServiceMethod,
Expand All @@ -53,7 +51,6 @@ import {
SdkType,
SdkUnionType,
TCGCContext,
UsageFlags,
} from "./interfaces.js";
import {
createGeneratedName,
Expand All @@ -80,9 +77,11 @@ import {
} from "./public-utils.js";
import {
addEncodeInfo,
createSdkInitializationTypeIfNonExist,
getAllModelsWithDiagnostics,
getClientTypeWithDiagnostics,
getSdkCredentialParameter,
getSdkInitializationType,
getSdkModelPropertyType,
getTypeSpecBuiltInType,
} from "./types.js";
Expand Down Expand Up @@ -375,44 +374,6 @@ function getClientDefaultApiVersion(
return defaultVersion;
}

function getSdkInitializationType(
context: TCGCContext,
client: SdkClient | SdkOperationGroup,
): [SdkInitializationType, readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();
let initializationModel = getClientInitialization(context, client.type);
let clientParams = context.__clientToParameters.get(client.type);
if (!clientParams) {
clientParams = [];
context.__clientToParameters.set(client.type, clientParams);
}
const access = client.kind === "SdkClient" ? "public" : "internal";
if (initializationModel) {
for (const prop of initializationModel.properties) {
clientParams.push(prop);
}
initializationModel.access = access;
} else {
const namePrefix = client.kind === "SdkClient" ? client.name : client.groupPath;
const name = `${namePrefix.split(".").at(-1)}Options`;
initializationModel = {
__raw: client.service,
doc: "Initialization class for the client",
kind: "model",
properties: [],
name,
isGeneratedName: true,
access,
usage: UsageFlags.Input,
crossLanguageDefinitionId: `${getNamespaceFullName(client.service.namespace!)}.${name}`,
apiVersions: context.__tspTypeToApiVersions.get(client.type)!,
decorators: [],
};
}

return diagnostics.wrap(initializationModel);
}

function getSdkMethodParameter(
context: TCGCContext,
type: Type,
Expand Down Expand Up @@ -463,11 +424,18 @@ function getSdkMethods<TServiceOperation extends SdkServiceOperation>(
const operationGroupClient = diagnostics.pipe(
createSdkClientType<TServiceOperation>(context, operationGroup, sdkClientType),
);
const clientInitialization = getClientInitialization(context, operationGroup.type);
const tspClientInitialization = getClientInitialization(context, operationGroup.type);
const parameters: SdkParameter[] = [];
if (clientInitialization) {
for (const property of clientInitialization.properties) {
parameters.push(property);
if (tspClientInitialization) {
const clientInitialization = getSdkInitializationType(
context,
operationGroup,
tspClientInitialization,
);
for (const property of clientInitialization.model.properties) {
if (property.kind === "method") {
iscai-msft marked this conversation as resolved.
Show resolved Hide resolved
parameters.push(property);
}
}
} else {
}
iscai-msft marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -633,6 +601,10 @@ function createSdkClientType<TServiceOperation extends SdkServiceOperation>(
} else {
name = getClientNameOverride(context, client.type) ?? client.type.name;
}
const tspInitializationModel = getClientInitialization(context, client.type);
const initialization = tspInitializationModel
? getSdkInitializationType(context, client, tspInitializationModel)
: createSdkInitializationTypeIfNonExist(context, client);
const sdkClientType: SdkClientType<TServiceOperation> = {
__raw: client,
kind: "client",
Expand All @@ -642,7 +614,7 @@ function createSdkClientType<TServiceOperation extends SdkServiceOperation>(
methods: [],
apiVersions: context.__tspTypeToApiVersions.get(client.type)!,
nameSpace: getClientNamespaceStringHelper(context, client.service)!,
initialization: diagnostics.pipe(getSdkInitializationType(context, client)),
initialization,
decorators: diagnostics.pipe(getTypeDecorators(context, client.type)),
parent,
// if it is client, the crossLanguageDefinitionId is the ${namespace}, if it is operation group, the crosslanguageDefinitionId is the %{namespace}.%{operationGroupName}
Expand All @@ -661,10 +633,17 @@ function addDefaultClientParameters<
>(context: TCGCContext, client: SdkClientType<TServiceOperation>): void {
const diagnostics = createDiagnosticCollector();
// there will always be an endpoint property
client.initialization.properties.push(diagnostics.pipe(getSdkEndpointParameter(context, client)));
if (!client.initialization.model.properties.find((x) => x.kind === "endpoint")) {
client.initialization.model.properties.push(
diagnostics.pipe(getSdkEndpointParameter(context, client)),
);
}
const credentialParam = getSdkCredentialParameter(context, client.__raw);
if (credentialParam) {
client.initialization.properties.push(credentialParam);
if (
credentialParam &&
!client.initialization.model.properties.find((x) => x.kind === "credential")
) {
client.initialization.model.properties.push(credentialParam);
}
let apiVersionParam = context.__clientToParameters
.get(client.__raw.type)
Expand All @@ -680,7 +659,7 @@ function addDefaultClientParameters<
}
}
if (apiVersionParam) {
client.initialization.properties.push(apiVersionParam);
client.initialization.model.properties.push(apiVersionParam);
}
let subId = context.__clientToParameters
.get(client.__raw.type)
Expand All @@ -695,7 +674,7 @@ function addDefaultClientParameters<
}
}
if (subId) {
client.initialization.properties.push(subId);
client.initialization.model.properties.push(subId);
}
}

Expand Down
89 changes: 77 additions & 12 deletions packages/typespec-client-generator-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
getDoc,
getEncode,
getKnownValues,
getNamespaceFullName,
getSummary,
getVisibility,
ignoreDiagnostics,
Expand All @@ -43,6 +44,7 @@ import {
} from "@typespec/http";
import {
getAccessOverride,
getClientInitialization,
getOverriddenClientMethod,
getUsageOverride,
listClients,
Expand All @@ -67,10 +69,12 @@ import {
SdkEnumType,
SdkEnumValueType,
SdkInitializationType,
SdkMethodParameter,
SdkModelPropertyType,
SdkModelPropertyTypeBase,
SdkModelType,
SdkOperationGroup,
SdkParameter,
SdkTupleType,
SdkType,
SdkUnionType,
Expand Down Expand Up @@ -685,18 +689,6 @@ export function getSdkModel(
return ignoreDiagnostics(getSdkModelWithDiagnostics(context, type, operation));
}

export function getInitializationType(
context: TCGCContext,
type: Model,
operation?: Operation,
): SdkInitializationType {
const model = ignoreDiagnostics(getSdkModelWithDiagnostics(context, type, operation));
for (const property of model.properties) {
property.kind = "method";
}
return model as SdkInitializationType;
}

export function getSdkModelWithDiagnostics(
context: TCGCContext,
type: Model,
Expand Down Expand Up @@ -781,6 +773,75 @@ function getSdkEnumValueType(
return diagnostics.wrap(getTypeSpecBuiltInType(context, kind!));
}

export function createSdkInitializationTypeIfNonExist(
context: TCGCContext,
client: SdkClient | SdkOperationGroup,
): SdkInitializationType {
const namePrefix = client.kind === "SdkClient" ? client.name : client.groupPath;
const name = `${namePrefix.split(".").at(-1)}Options`;
return {
access: client.kind === "SdkClient" ? "public" : "internal",
model: {
__raw: client.service,
doc: "Initialization class for the client",
kind: "model",
properties: [],
name,
isGeneratedName: true,
access: "public",
usage: UsageFlags.Input | UsageFlags.ClientInitialization,
crossLanguageDefinitionId: `${getNamespaceFullName(client.service.namespace!)}.${name}`,
apiVersions: context.__tspTypeToApiVersions.get(client.type)!,
decorators: [],
},
};
}

export function getSdkInitializationType(
context: TCGCContext,
client: SdkClient | SdkOperationGroup,
model: Model,
): SdkInitializationType {
// convert to SDK type
const existingModel = context.modelsMap?.get(model) as SdkModelType<SdkParameter> | undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we allow to reuse model in operation as a client initialization model? if so, can we have a test? if not, we could return directly and shall have some lint.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@m-nash since c# appears to be the main sdk using client initialization model generation, is there anything precluding us from having a model be used in both scenarios?

let sdkModel: SdkModelType<SdkParameter>;
if (existingModel) {
sdkModel = existingModel;
} else {
sdkModel = getSdkModel(context, model) as SdkModelType<SdkParameter>;

sdkModel.properties = sdkModel.properties.map(
(property: SdkModelPropertyType): SdkMethodParameter => {
property.onClient = true;
property.kind = "method";
return property as SdkMethodParameter;
},
);
}
const initializationModel: SdkInitializationType = {
access: client.kind === "SdkClient" ? "public" : "internal",
model: sdkModel,
};
let clientParams = context.__clientToParameters.get(client.type);
if (!clientParams) {
clientParams = [];
context.__clientToParameters.set(client.type, clientParams);
}

for (const prop of initializationModel.model.properties) {
if (!clientParams.filter((p) => p.name === prop.name).length) {
clientParams.push(prop);
prop.onClient = true;
}
}
updateUsageOrAccessOfModel(context, UsageFlags.Input, sdkModel, { propagation: false });
updateUsageOrAccessOfModel(context, UsageFlags.ClientInitialization, sdkModel, {
propagation: false,
});
updateModelsMap(context, model, sdkModel);
return initializationModel;
}

function getUnionAsEnumValueType(
context: TCGCContext,
union: Union,
Expand Down Expand Up @@ -1778,6 +1839,10 @@ export function getAllModelsWithDiagnostics(
diagnostics.pipe(updateTypesFromOperation(context, operation));
}
const ogs = listOperationGroups(context, client);
const clientInitModel = getClientInitialization(context, client.type);
if (clientInitModel) {
getSdkInitializationType(context, client, clientInitModel);
}
iscai-msft marked this conversation as resolved.
Show resolved Hide resolved
while (ogs.length) {
const operationGroup = ogs.pop();
for (const operation of listOperationsInOperationGroup(context, operationGroup!)) {
Expand Down
Loading
Loading