Skip to content

Commit

Permalink
Implement tsp namespace for http-client-csharp (#5443)
Browse files Browse the repository at this point in the history
Fixes #5442
Fixes #5471
Fixes #5563
Fixes Azure/azure-sdk-for-net#47670

This PR contains the following:
1. now we honor the namespace defined in typespec for both model and
clients (short for "namespace is namespace" feature)
2. <del>removes the `namespace` configuration from the emitter because
we no longer need it. Azure generator still needs this, we will add it
back in azure emitter (temporary until azure generator decides how to
deal with it)</del>
3. removes the `use-model-namespace` configuration from the emitter
because we no longer need it. Azure generator still needs this, we will
add it back in azure emitter (temporary until azure generator decides
how to deal with it)
4. reports diagnostic when there are conflicts between namespace
segments and subclient names.

Because this change changes the namespace of literally everything, there
are quite a few file changed. Majority of them are the generated files
changed for the namespace.

We also have a piece of logic that automatically renames the client
`Models` to `ModelsOps` in our emitter. We should not have it in the
emitter any way. This part is removed. Azure part needs it, therefore we
will add the same logic back in azure generator.

Update:
I have to add the configuration `namespace` back because
`autorest.csharp` is still strongly coupled with this configuration,
removing it causes massive changes on that side. Therefore I decided
that we keep it here for now, we still write its value to
configuration.json for backward compatibility, but MGC will not read it
nor use it.
  • Loading branch information
ArcturusZhang authored Feb 7, 2025
1 parent 1940924 commit 6210262
Show file tree
Hide file tree
Showing 1,336 changed files with 9,273 additions and 2,753 deletions.
4 changes: 1 addition & 3 deletions packages/http-client-csharp/emitter/src/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ export async function $onEmit(context: EmitContext<NetEmitterOptions>) {
logDiagnostics(context.program.diagnostics, context.program.host.logSink);
process.exit(1);
}
const tspNamespace = root.Name; // this is the top-level namespace defined in the typespec file, which is actually always different from the namespace of the SDK

if (root) {
const generatedFolder = resolvePath(outputFolder, "src", "Generated");
Expand All @@ -93,13 +92,12 @@ export async function $onEmit(context: EmitContext<NetEmitterOptions>) {
);

//emit configuration.json
const namespace = options.namespace ?? tspNamespace;
const namespace = options.namespace ?? root.Name;
const configurations: Configuration = {
"output-folder": ".",
namespace: namespace,
"library-name": options["library-name"] ?? namespace,
"unreferenced-types-handling": options["unreferenced-types-handling"],
"model-namespace": options["model-namespace"],
"disable-xml-docs":
options["disable-xml-docs"] === false ? undefined : options["disable-xml-docs"],
};
Expand Down
44 changes: 29 additions & 15 deletions packages/http-client-csharp/emitter/src/lib/client-model-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
UsageFlags,
} from "@azure-tools/typespec-client-generator-core";
import { NoTarget } from "@typespec/compiler";
import { NetEmitterOptions, resolveOptions } from "../options.js";
import { NetEmitterOptions } from "../options.js";
import { CodeModel } from "../type/code-model.js";
import { InputClient } from "../type/input-client.js";
import { InputOperationParameterKind } from "../type/input-operation-parameter-kind.js";
Expand All @@ -21,7 +21,6 @@ import { InputEnumType, InputModelType, InputType } from "../type/input-type.js"
import { RequestLocation } from "../type/request-location.js";
import { SdkTypeMap } from "../type/sdk-type-map.js";
import { reportDiagnostic } from "./lib.js";
import { Logger } from "./logger.js";
import { navigateModels } from "./model.js";
import { fromSdkServiceMethod, getParameterDefaultValue } from "./operation-converter.js";
import { processServiceAuthentication } from "./service-authentication.js";
Expand Down Expand Up @@ -51,8 +50,9 @@ export function createModel(sdkContext: SdkContext<NetEmitterOptions>): CodeMode
? sdkApiVersionEnums[0].values.map((v) => v.value as string).flat()
: rootClients[0].apiVersions;

// this is a set tracking the bad namespace segments
const inputClients: InputClient[] = [];
fromSdkClients(rootClients, inputClients, []);
fromSdkClients(sdkContext, rootClients, inputClients, []);

const clientModel: CodeModel = {
Name: sdkPackage.rootNamespace,
Expand All @@ -62,33 +62,55 @@ export function createModel(sdkContext: SdkContext<NetEmitterOptions>): CodeMode
Clients: inputClients,
Auth: processServiceAuthentication(sdkContext, sdkPackage),
};

return clientModel;

function fromSdkClients(
sdkContext: SdkContext<NetEmitterOptions>,
clients: SdkClientType<SdkHttpOperation>[],
inputClients: InputClient[],
parentClientNames: string[],
) {
for (const client of clients) {
const inputClient = emitClient(client, parentClientNames);
const inputClient = fromSdkClient(sdkContext, 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);
fromSdkClients(sdkContext, subClients, inputClients, parentClientNames);
parentClientNames.pop();
}
}

function emitClient(client: SdkClientType<SdkHttpOperation>, parentNames: string[]): InputClient {
function fromSdkClient(
sdkContext: SdkContext<NetEmitterOptions>,
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);
// see if this namespace is a sub-namespace of an existing bad namespace
const segments = client.clientNamespace.split(".");
const lastSegment = segments[segments.length - 1];
if (lastSegment === clientName) {
// we report diagnostics when the last segment of the namespace is the same as the client name
// because in our design, a sub namespace will be generated as a sub client with exact the same name as the namespace
// in csharp, this will cause a conflict between the namespace and the class name
reportDiagnostic(sdkContext.program, {
code: "client-namespace-conflict",
format: { clientNamespace: client.clientNamespace, clientName },
target: client.__raw.type ?? NoTarget,
});
}

return {
Name: getClientName(client, parentNames),
Name: clientName,
ClientNamespace: client.clientNamespace,
Summary: client.summary,
Doc: client.doc,
Operations: client.methods
Expand Down Expand Up @@ -119,14 +141,6 @@ export function createModel(sdkContext: SdkContext<NetEmitterOptions>): CodeMode
if (parentClientNames.length >= 2)
return `${parentClientNames.slice(parentClientNames.length - 1).join("")}${clientName}`;

if (
clientName === "Models" &&
resolveOptions(sdkContext.emitContext)["model-namespace"] !== false
) {
Logger.getInstance().warn(`Invalid client name "${clientName}"`);
return "ModelsOps";
}

return clientName;
}

Expand Down
12 changes: 6 additions & 6 deletions packages/http-client-csharp/emitter/src/lib/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@ const $lib = createTypeSpecLibrary({
default: paramMessage`No Route for service for service ${"service"}`,
},
},
"invalid-name": {
severity: "warning",
messages: {
default: paramMessage`Invalid interface or operation group name ${"name"} when configuration "model-namespace" is on`,
},
},
"general-warning": {
severity: "warning",
messages: {
Expand Down Expand Up @@ -58,6 +52,12 @@ const $lib = createTypeSpecLibrary({
default: paramMessage`${"message"}`,
},
},
"client-namespace-conflict": {
severity: "warning",
messages: {
default: paramMessage`namespace ${"clientNamespace"} conflicts with client ${"clientName"}, please use @clientName to specify a different name for the client.`,
},
},
},
emitter: {
options: NetEmitterOptionsSchema,
Expand Down
6 changes: 6 additions & 0 deletions packages/http-client-csharp/emitter/src/lib/type-converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function fromSdkType(
retVar = {
kind: "nullable",
type: inputType,
clientNamespace: sdkType.clientNamespace,
};
break;
case "model":
Expand Down Expand Up @@ -115,6 +116,7 @@ export function fromSdkModelType(
inputModelType = {
kind: "model",
name: modelTypeName,
clientNamespace: modelType.clientNamespace,
crossLanguageDefinitionId: modelType.crossLanguageDefinitionId,
access: getAccessOverride(
context,
Expand Down Expand Up @@ -142,6 +144,7 @@ export function fromSdkModelType(
const ourProperty = fromSdkModelProperty(property, {
ModelName: modelTypeName,
Usage: modelType.usage,
ClientNamespace: modelType.clientNamespace,
} as LiteralTypeContext);
propertiesDict.set(property, ourProperty);
}
Expand Down Expand Up @@ -227,6 +230,7 @@ export function fromSdkEnumType(
context,
enumType.__raw as any,
) /* when tcgc provide a way to identify if the access is override or not, we can get the accessibility from the enumType.access,*/,
clientNamespace: enumType.clientNamespace,
deprecation: enumType.deprecation,
summary: enumType.summary,
doc: enumType.doc,
Expand Down Expand Up @@ -304,6 +308,7 @@ function fromUnionType(
kind: "union",
name: union.name,
variantTypes: variantTypes,
clientNamespace: union.clientNamespace,
decorators: union.decorators,
};
}
Expand Down Expand Up @@ -341,6 +346,7 @@ function fromSdkConstantType(
values: values,
crossLanguageDefinitionId: "",
access: undefined,
clientNamespace: literalTypeContext.ClientNamespace,
doc: `The ${enumName}`, // TODO -- what should we put here?
isFixed: false,
isFlags: false,
Expand Down
2 changes: 0 additions & 2 deletions packages/http-client-csharp/emitter/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export interface NetEmitterOptions extends SdkEmitterOptions {
"new-project"?: boolean;
"clear-output-folder"?: boolean;
"save-inputs"?: boolean;
"model-namespace"?: boolean;
debug?: boolean;
logLevel?: LoggerLevel;
"disable-xml-docs"?: boolean;
Expand Down Expand Up @@ -53,7 +52,6 @@ export const NetEmitterOptionsSchema: JSONSchemaType<NetEmitterOptions> = {
"new-project": { type: "boolean", nullable: true },
"clear-output-folder": { type: "boolean", nullable: true },
"save-inputs": { type: "boolean", nullable: true },
"model-namespace": { type: "boolean", nullable: true },
"generate-protocol-methods": { type: "boolean", nullable: true },
"generate-convenience-methods": { type: "boolean", nullable: true },
"flatten-union-as-enum": { type: "boolean", nullable: true },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@ export interface Configuration {
namespace: string;
"library-name": string | null;
"unreferenced-types-handling"?: "removeOrInternalize" | "internalize" | "keepAll";
"model-namespace"?: boolean;
"disable-xml-docs"?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Protocols } from "./protocols.js";

export interface InputClient {
Name: string;
ClientNamespace: string;
Summary?: string;
Doc?: string;
Operations: InputOperation[];
Expand Down
4 changes: 4 additions & 0 deletions packages/http-client-csharp/emitter/src/type/input-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export interface InputUnionType extends InputTypeBase {
kind: "union";
name: string;
variantTypes: InputType[];
clientNamespace: string;
}

export function isInputUnionType(type: InputType): type is InputUnionType {
Expand All @@ -92,6 +93,7 @@ export interface InputModelType extends InputTypeBase {
crossLanguageDefinitionId: string;
access?: AccessFlags;
usage: UsageFlags;
clientNamespace: string;
additionalProperties?: InputType;
discriminatorValue?: string;
discriminatedSubtypes?: Record<string, InputModelType>;
Expand Down Expand Up @@ -127,6 +129,7 @@ export interface InputEnumType extends InputTypeBase {
isFlags: boolean;
usage: UsageFlags;
access?: AccessFlags;
clientNamespace: string;
}

export interface InputEnumTypeValue extends InputTypeBase {
Expand All @@ -140,6 +143,7 @@ export interface InputEnumTypeValue extends InputTypeBase {
export interface InputNullableType extends InputTypeBase {
kind: "nullable";
type: InputType;
clientNamespace: string;
}

export function isInputEnumType(type: InputType): type is InputEnumType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface LiteralTypeContext {
ModelName: string;
PropertyName: string;
Usage: UsageFlags;
ClientNamespace: string;
}
3 changes: 2 additions & 1 deletion packages/http-client-csharp/eng/scripts/Generate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ $failingSpecs = @(
Join-Path 'http' 'payload' 'xml'
Join-Path 'http' 'type' 'model' 'flatten'
Join-Path 'http' 'type' 'model' 'templated'
Join-Path 'http' 'client' 'naming' # pending until https://github.com/microsoft/typespec/issues/5653 is resolved
)

$azureAllowSpecs = @(
Join-Path 'http' 'client' 'naming'
Join-Path 'http' 'client' 'structure' 'client-operation-group'
Join-Path 'http' 'client' 'structure' 'default'
Join-Path 'http' 'client' 'structure' 'multi-client'
Expand Down Expand Up @@ -110,6 +110,7 @@ foreach ($directory in $directories) {
}

if ($folders.Contains("versioning")) {
Write-Host "Generating versioning for $subPath" -ForegroundColor Cyan
Generate-Versioning $directory.FullName $generationDir -generateStub $stubbed
$cadlRanchLaunchProjects.Add($($folders -join "-") + "-v1", $("TestProjects/CadlRanch/$($subPath.Replace([System.IO.Path]::DirectorySeparatorChar, '/'))") + "/v1")
$cadlRanchLaunchProjects.Add($($folders -join "-") + "-v2", $("TestProjects/CadlRanch/$($subPath.Replace([System.IO.Path]::DirectorySeparatorChar, '/'))") + "/v2")
Expand Down
28 changes: 14 additions & 14 deletions packages/http-client-csharp/eng/scripts/Generation.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function Get-TspCommand {
[string]$specFile,
[string]$generationDir,
[bool]$generateStub = $false,
[string]$namespaceOverride = $null,
[string]$libraryNameOverride = $null,
[string]$apiVersion = $null
)
$command = "npx tsp compile $specFile"
Expand All @@ -41,8 +41,8 @@ function Get-TspCommand {
$command += " --option @typespec/http-client-csharp.plugin-name=StubLibraryPlugin"
}

if ($namespaceOverride) {
$command += " --option @typespec/http-client-csharp.namespace=$namespaceOverride"
if ($libraryNameOverride) {
$command += " --option @typespec/http-client-csharp.library-name=$libraryNameOverride"
}

if ($apiVersion) {
Expand Down Expand Up @@ -104,14 +104,14 @@ function Generate-Srv-Driven {
## get the last two directories of the output directory and add V1/V2 to disambiguate the namespaces
$namespaceRoot = $(($outputDir.Split([System.IO.Path]::DirectorySeparatorChar)[-2..-1] | `
ForEach-Object { $_.Substring(0,1).ToUpper() + $_.Substring(1) }) -replace '-(\p{L})', { $_.Groups[1].Value.ToUpper() } -replace '\W', '' -join ".")
$v1NamespaceOverride = $namespaceRoot + ".V1"
$v2NamespaceOverride = $namespaceRoot + ".V2"
$v1LibraryNameOverride = $namespaceRoot + ".V1"
$v2LibraryNameOverride = $namespaceRoot + ".V2"

$v1SpecFilePath = $(Join-Path $specFilePath "old.tsp")
$v2SpecFilePath = $(Join-Path $specFilePath "main.tsp")

Invoke (Get-TspCommand $v1SpecFilePath $v1Dir -generateStub $generateStub -namespaceOverride $v1NamespaceOverride)
Invoke (Get-TspCommand $v2SpecFilePath $v2Dir -generateStub $generateStub -namespaceOverride $v2NamespaceOverride)
Invoke (Get-TspCommand $v1SpecFilePath $v1Dir -generateStub $generateStub -libraryNameOverride $v1LibraryNameOverride)
Invoke (Get-TspCommand $v2SpecFilePath $v2Dir -generateStub $generateStub -libraryNameOverride $v2LibraryNameOverride)

# exit if the generation failed
if ($LASTEXITCODE -ne 0) {
Expand Down Expand Up @@ -140,19 +140,19 @@ function Generate-Versioning {
## get the last two directories of the output directory and add V1/V2 to disambiguate the namespaces
$namespaceRoot = $(($outputFolders[-2..-1] | `
ForEach-Object { $_.Substring(0,1).ToUpper() + $_.Substring(1) }) -join ".")
$v1NamespaceOverride = $namespaceRoot + ".V1"
$v2NamespaceOverride = $namespaceRoot + ".V2"
Invoke (Get-TspCommand $specFilePath $v1Dir -generateStub $generateStub -apiVersion "v1" -namespaceOverride $v1NamespaceOverride)
Invoke (Get-TspCommand $specFilePath $v2Dir -generateStub $generateStub -apiVersion "v2" -namespaceOverride $v2NamespaceOverride)
$v1LibraryNameOverride = $namespaceRoot + ".V1"
$v2LibraryNameOverride = $namespaceRoot + ".V2"

Invoke (Get-TspCommand $specFilePath $v1Dir -generateStub $generateStub -apiVersion "v1" -libraryNameOverride $v1LibraryNameOverride)
Invoke (Get-TspCommand $specFilePath $v2Dir -generateStub $generateStub -apiVersion "v2" -libraryNameOverride $v2LibraryNameOverride)

if ($outputFolders.Contains("removed")) {
$v2PreviewDir = $(Join-Path $outputDir "v2Preview")
if ($createOutputDirIfNotExist -and -not (Test-Path $v2PreviewDir)) {
New-Item -ItemType Directory -Path $v2PreviewDir | Out-Null
}
$v2PreviewNamespaceOverride = $namespaceRoot + ".V2Preview"
Invoke (Get-TspCommand $specFilePath $v2PreviewDir -generateStub $generateStub -apiVersion "v2preview" -namespaceOverride $v2PreviewNamespaceOverride)
$v2PreviewLibraryNameOverride = $namespaceRoot + ".V2Preview"
Invoke (Get-TspCommand $specFilePath $v2PreviewDir -generateStub $generateStub -apiVersion "v2preview" -libraryNameOverride $v2PreviewLibraryNameOverride)
}

# exit if the generation failed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ public ClientProvider(InputClient inputClient)
_subClients = new(GetSubClients);
}

protected override string BuildNamespace() => string.IsNullOrEmpty(_inputClient.Namespace) ?
base.BuildNamespace() :
ClientModelPlugin.Instance.TypeFactory.GetCleanNameSpace(_inputClient.Namespace);

private IReadOnlyList<ParameterProvider> GetSubClientInternalConstructorParameters()
{
var subClientParameters = new List<ParameterProvider>
Expand Down Expand Up @@ -311,7 +315,7 @@ protected override ConstructorProvider[] BuildConstructors()
AppendConstructors(_apiKeyAuthFields, primaryConstructors, secondaryConstructors);
}
// if there is oauth2 auth
if (_oauth2Fields!= null)
if (_oauth2Fields != null)
{
AppendConstructors(_oauth2Fields, primaryConstructors, secondaryConstructors);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public RestClientProvider(InputClient inputClient, ClientProvider clientProvider

protected override string BuildName() => _inputClient.Name.ToCleanName();

protected override string BuildNamespace() => ClientProvider.Namespace;

protected override PropertyProvider[] BuildProperties()
{
return [.. _pipelineMessage20xClassifiers.Values.OrderBy(v => v.Name)];
Expand Down
Loading

0 comments on commit 6210262

Please sign in to comment.