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

Align the client type shape from TCGC in our emitter #6179

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/http-client-csharp/emitter/src/emitter.ts
Original file line number Diff line number Diff line change
@@ -73,6 +73,7 @@ export async function $onEmit(context: EmitContext<CSharpEmitterOptions>) {
logger: logger,
__typeCache: {
crossLanguageDefinitionIds: new Map(),
clients: new Map(),
types: new Map(),
models: new Map(),
enums: new Map(),
163 changes: 163 additions & 0 deletions packages/http-client-csharp/emitter/src/lib/client-converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import {
SdkClientType as SdkClientTypeOfT,
SdkEndpointParameter,
SdkEndpointType,
SdkHttpOperation,
} from "@azure-tools/typespec-client-generator-core";
import { NoTarget } from "@typespec/compiler";
import { CSharpEmitterContext } from "../sdk-context.js";
import { InputOperationParameterKind } from "../type/input-operation-parameter-kind.js";
import { InputParameter } from "../type/input-parameter.js";
import { InputClientType, InputType } from "../type/input-type.js";
import { RequestLocation } from "../type/request-location.js";
import { fromSdkServiceMethod, getParameterDefaultValue } from "./operation-converter.js";
import { fromSdkType } from "./type-converter.js";

type SdkClientType = SdkClientTypeOfT<SdkHttpOperation>;

export function fromSdkClients(
sdkContext: CSharpEmitterContext,
clients: SdkClientType[],
rootApiVersions: string[],
): InputClientType[] {
const inputClients: InputClientType[] = [];
for (const client of clients) {
const inputClient = fromSdkClient(sdkContext, client, rootApiVersions);
inputClients.push(inputClient);
}

return inputClients;
}

function fromSdkClient(
sdkContext: CSharpEmitterContext,
client: SdkClientType,
rootApiVersions: string[],
): InputClientType {
let inputClient: InputClientType | undefined = sdkContext.__typeCache.clients.get(client);
if (inputClient) {
return inputClient;
}
const endpointParameter = client.clientInitialization.parameters.find(
(p) => p.kind === "endpoint",
) as SdkEndpointParameter;
const uri = getMethodUri(endpointParameter);
const clientParameters = fromSdkEndpointParameter(endpointParameter);

inputClient = {
kind: "client",
name: client.name,
namespace: client.namespace,
doc: client.doc,
summary: client.summary,
parameters: clientParameters,
operations: client.methods
.filter((m) => m.kind !== "clientaccessor")
.map((m) => fromSdkServiceMethod(sdkContext, m, uri, rootApiVersions)),
apiVersions: client.apiVersions,
crossLanguageDefinitionId: client.crossLanguageDefinitionId,
parent: undefined,
children: undefined,
};

updateSdkClientTypeReferences(sdkContext, client, inputClient);

// fill parent
if (client.parent) {
inputClient.parent = fromSdkClient(sdkContext, client.parent, rootApiVersions);
}
// fill children
if (client.children) {
inputClient.children = client.children.map((c) =>
fromSdkClient(sdkContext, c, rootApiVersions),
);
}

return inputClient;

function fromSdkEndpointParameter(p: SdkEndpointParameter): InputParameter[] {
if (p.type.kind === "union") {
return fromSdkEndpointType(p.type.variantTypes[0]);
} else {
return fromSdkEndpointType(p.type);
}
}

function fromSdkEndpointType(type: SdkEndpointType): InputParameter[] {
// TODO: support free-style endpoint url with multiple parameters
const endpointExpr = type.serverUrl
.replace("https://", "")
.replace("http://", "")
.split("/")[0];
if (!/^\{\w+\}$/.test(endpointExpr)) {
sdkContext.logger.reportDiagnostic({
code: "unsupported-endpoint-url",
format: { endpoint: type.serverUrl },
target: NoTarget,
});
return [];
}
const endpointVariableName = endpointExpr.substring(1, endpointExpr.length - 1);

const parameters: InputParameter[] = [];
for (const parameter of type.templateArguments) {
const isEndpoint = parameter.name === endpointVariableName;
const parameterType: InputType = isEndpoint
? {
kind: "url",
name: "url",
crossLanguageDefinitionId: "TypeSpec.url",
}
: fromSdkType(sdkContext, parameter.type); // TODO: consolidate with converter.fromSdkEndpointType
parameters.push({
Name: parameter.name,
NameInRequest: parameter.serializedName,
Summary: parameter.summary,
Doc: parameter.doc,
// TODO: we should do the magic in generator
Type: parameterType,
Location: RequestLocation.Uri,
IsApiVersion: parameter.isApiVersionParam,
IsResourceParameter: false,
IsContentType: false,
IsRequired: !parameter.optional,
IsEndpoint: isEndpoint,
SkipUrlEncoding: false,
Explode: false,
Kind: InputOperationParameterKind.Client,
DefaultValue: getParameterDefaultValue(
sdkContext,
parameter.clientDefaultValue,
parameterType,
),
});
}
return parameters;
}
}

function updateSdkClientTypeReferences(
sdkContext: CSharpEmitterContext,
sdkClient: SdkClientType,
inputClient: InputClientType,
) {
sdkContext.__typeCache.clients.set(sdkClient, inputClient);
sdkContext.__typeCache.crossLanguageDefinitionIds.set(
sdkClient.crossLanguageDefinitionId,
sdkClient.__raw.type,
);
}

function getMethodUri(p: SdkEndpointParameter | undefined): string {
if (!p) return "";

if (p.type.kind === "endpoint" && p.type.templateArguments.length > 0) return p.type.serverUrl;

if (p.type.kind === "union" && p.type.variantTypes.length > 0)
return (p.type.variantTypes[0] as SdkEndpointType).serverUrl;

return `{${p.name}}`;
}
161 changes: 4 additions & 157 deletions packages/http-client-csharp/emitter/src/lib/client-model-builder.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import {
SdkClientType,
SdkEndpointParameter,
SdkEndpointType,
SdkHttpOperation,
SdkServiceMethod,
UsageFlags,
} from "@azure-tools/typespec-client-generator-core";
import { UsageFlags } from "@azure-tools/typespec-client-generator-core";
import { NoTarget } from "@typespec/compiler";
import { CSharpEmitterContext } from "../sdk-context.js";
import { CodeModel } from "../type/code-model.js";
import { InputClient } from "../type/input-client.js";
import { InputOperationParameterKind } from "../type/input-operation-parameter-kind.js";
import { InputParameter } from "../type/input-parameter.js";
import { InputType } from "../type/input-type.js";
import { RequestLocation } from "../type/request-location.js";
import { fromSdkClients } from "./client-converter.js";
import { navigateModels } from "./model.js";
import { fromSdkServiceMethod, getParameterDefaultValue } from "./operation-converter.js";
import { processServiceAuthentication } from "./service-authentication.js";
import { fromSdkType } from "./type-converter.js";

/**
* Creates the code model from the SDK context.
@@ -35,7 +22,7 @@ export function createModel(sdkContext: CSharpEmitterContext): CodeModel {

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

const rootClients = sdkPackage.clients.filter((c) => c.initialization.access === "public");
const rootClients = sdkPackage.clients;
if (rootClients.length === 0) {
sdkContext.logger.reportDiagnostic({
code: "no-root-client",
@@ -50,8 +37,7 @@ export function createModel(sdkContext: CSharpEmitterContext): CodeModel {
? sdkApiVersionEnums[0].values.map((v) => v.value as string).flat()
: rootClients[0].apiVersions;

const inputClients: InputClient[] = [];
fromSdkClients(rootClients, inputClients, []);
const inputClients = fromSdkClients(sdkContext, rootClients, rootApiVersions);

const clientModel: CodeModel = {
// rootNamespace is really coalescing the `package-name` option and the first namespace found.
@@ -64,143 +50,4 @@ export function createModel(sdkContext: CSharpEmitterContext): CodeModel {
};

return clientModel;

function fromSdkClients(
clients: SdkClientType<SdkHttpOperation>[],
inputClients: InputClient[],
parentClientNames: string[],
) {
for (const client of clients) {
const inputClient = fromSdkClient(client, parentClientNames);
inputClients.push(inputClient);
const subClients = client.methods
.filter((m) => m.kind === "clientaccessor")
.map((m) => m.response as SdkClientType<SdkHttpOperation>);
parentClientNames.push(inputClient.Name);
fromSdkClients(subClients, inputClients, parentClientNames);
parentClientNames.pop();
}
}

function fromSdkClient(
client: SdkClientType<SdkHttpOperation>,
parentNames: string[],
): InputClient {
const endpointParameter = client.initialization.properties.find(
(p) => p.kind === "endpoint",
) as SdkEndpointParameter;
const uri = getMethodUri(endpointParameter);
const clientParameters = fromSdkEndpointParameter(endpointParameter);
const clientName = getClientName(client, parentNames);

sdkContext.__typeCache.crossLanguageDefinitionIds.set(
client.crossLanguageDefinitionId,
client.__raw.type,
);
return {
Name: clientName,
Namespace: client.namespace,
Summary: client.summary,
Doc: client.doc,
Operations: client.methods
.filter((m) => m.kind !== "clientaccessor")
.map((m) =>
fromSdkServiceMethod(
sdkContext,
m as SdkServiceMethod<SdkHttpOperation>,
uri,
rootApiVersions,
),
),
Protocol: {},
Parent: parentNames.length > 0 ? parentNames[parentNames.length - 1] : undefined,
Parameters: clientParameters,
Decorators: client.decorators,
CrossLanguageDefinitionId: client.crossLanguageDefinitionId,
};
}

function getClientName(
client: SdkClientType<SdkHttpOperation>,
parentClientNames: string[],
): string {
const clientName = client.name;

if (parentClientNames.length === 0) return clientName;
if (parentClientNames.length >= 2)
return `${parentClientNames.slice(parentClientNames.length - 1).join("")}${clientName}`;

return clientName;
}

function fromSdkEndpointParameter(p: SdkEndpointParameter): InputParameter[] {
if (p.type.kind === "union") {
return fromSdkEndpointType(p.type.variantTypes[0]);
} else {
return fromSdkEndpointType(p.type);
}
}

function fromSdkEndpointType(type: SdkEndpointType): InputParameter[] {
// TODO: support free-style endpoint url with multiple parameters
const endpointExpr = type.serverUrl
.replace("https://", "")
.replace("http://", "")
.split("/")[0];
if (!/^\{\w+\}$/.test(endpointExpr)) {
sdkContext.logger.reportDiagnostic({
code: "unsupported-endpoint-url",
format: { endpoint: type.serverUrl },
target: NoTarget,
});
return [];
}
const endpointVariableName = endpointExpr.substring(1, endpointExpr.length - 1);

const parameters: InputParameter[] = [];
for (const parameter of type.templateArguments) {
const isEndpoint = parameter.name === endpointVariableName;
const parameterType: InputType = isEndpoint
? {
kind: "url",
name: "url",
crossLanguageDefinitionId: "TypeSpec.url",
}
: fromSdkType(sdkContext, parameter.type); // TODO: consolidate with converter.fromSdkEndpointType
parameters.push({
Name: parameter.name,
NameInRequest: parameter.serializedName,
Summary: parameter.summary,
Doc: parameter.doc,
// TODO: we should do the magic in generator
Type: parameterType,
Location: RequestLocation.Uri,
IsApiVersion: parameter.isApiVersionParam,
IsResourceParameter: false,
IsContentType: false,
IsRequired: !parameter.optional,
IsEndpoint: isEndpoint,
SkipUrlEncoding: false,
Explode: false,
Kind: InputOperationParameterKind.Client,
DefaultValue: getParameterDefaultValue(
sdkContext,
parameter.clientDefaultValue,
parameterType,
),
});
}
return parameters;
}
}

function getMethodUri(p: SdkEndpointParameter | undefined): string {
if (!p) return "";

if (p.type.kind === "endpoint" && p.type.templateArguments.length > 0) return p.type.serverUrl;

if (p.type.kind === "union" && p.type.variantTypes.length > 0)
return (p.type.variantTypes[0] as SdkEndpointType).serverUrl;

return `{${p.name}}`;
}
10 changes: 8 additions & 2 deletions packages/http-client-csharp/emitter/src/sdk-context.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import { SdkContext, SdkType } from "@azure-tools/typespec-client-generator-core";
import {
SdkClientType,
SdkContext,
SdkHttpOperation,
SdkType,
} from "@azure-tools/typespec-client-generator-core";
import { Type } from "@typespec/compiler";
import { Logger } from "./lib/logger.js";
import { CSharpEmitterOptions } from "./options.js";
import { InputEnumType, InputModelType, InputType } from "./type/input-type.js";
import { InputClientType, InputEnumType, InputModelType, InputType } from "./type/input-type.js";

/**
* The emitter context for the CSharp emitter.
@@ -18,6 +23,7 @@ export interface CSharpEmitterContext extends SdkContext<CSharpEmitterOptions> {

export interface SdkTypeMap {
crossLanguageDefinitionIds: Map<string, Type | undefined>;
clients: Map<SdkClientType<SdkHttpOperation>, InputClientType>;
types: Map<SdkType, InputType>;
models: Map<string, InputModelType>;
enums: Map<string, InputEnumType>;
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.