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 namespace flag #2161

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
7 changes: 7 additions & 0 deletions .chronus/changes/tcgc-namespaceFlag-2025-1-3-14-58-27.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-client-generator-core"
---

Add support for `namespace` flag in tspconfig.yaml
4 changes: 4 additions & 0 deletions packages/typespec-client-generator-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ options:

**Type:** `string`

### `namespace`

**Type:** `string`

## Decorators

### Azure.ClientGenerator.Core
Expand Down
1 change: 1 addition & 0 deletions packages/typespec-client-generator-core/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export async function createSdkContext<
decoratorsAllowList: [...defaultDecoratorsAllowList, ...(options?.additionalDecorators ?? [])],
previewStringRegex: options?.versioning?.previewStringRegex || tcgcContext.previewStringRegex,
disableUsageAccessPropagationToBase: options?.disableUsageAccessPropagationToBase ?? false,
namespace: context.options["namespace"],
Copy link
Member

Choose a reason for hiding this comment

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

I might be missing it but where are we blocking / ignoring this outside of azure flavor? @srnagar if I recall correctly that is what we were doing is azure only since thats the only use case that actually needs this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My understanding is we weren't going to block unbranded from using it, but we are really mainly adding it for Azure compatibility.

The general use case I have for this is:

  1. there's a main.tsp that defines the service under one main namespace (say namespace DocumentIntelligence)
  2. there's a client.tsp with client-customizations under one main namespace (say namespace DocumentIntelligenceCustomizations)
  3. we use namespace flag in tspconfig.yaml to give the azure name, so namespace: Azure.DocumentIntelligence

Copy link
Member

Choose a reason for hiding this comment

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

The namespace in tspconfig.yaml is mainly used for Azure and we will not use it for unbranded libraries. We just don't publicly document namespace option for unbranded libraries.

If we want to be very explicit, we can add a check in the TCGC that can be removed later if we want to extend this support for unbranded libraries.

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 personally feel like it's cleaner to keep it as is, and when we split up tcgc, we can keep namespace in the tcgc library. I think I'd almost prefer if we only want azure to use it, to call it something like azureNamespace. I don't feel very strongly about this though

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I agree with just keeping it as is.

};
sdkContext.sdkPackage = diagnostics.pipe(getSdkPackage(sdkContext));
for (const client of sdkContext.sdkPackage.clients) {
Expand Down
30 changes: 25 additions & 5 deletions packages/typespec-client-generator-core/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
DecoratorContext,
DecoratorExpressionNode,
DecoratorFunction,
Diagnostic,
Enum,
EnumMember,
Interface,
Expand All @@ -17,6 +18,7 @@ import {
SyntaxKind,
Type,
Union,
createDiagnosticCollector,
getDiscriminator,
getNamespaceFullName,
getProjectedName,
Expand Down Expand Up @@ -60,6 +62,7 @@ import {
AllScopes,
clientNameKey,
clientNamespaceKey,
getRootUserDefinedNamespaceName,
getValidApiVersion,
isAzureCoreTspModel,
negationScopesKey,
Expand Down Expand Up @@ -1209,19 +1212,30 @@ export const $clientNamespace: ClientNamespaceDecorator = (
export function getClientNamespace(
context: TCGCContext,
entity: Namespace | Interface | Model | Enum | Union,
): string {
): [string, readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();
const override = getScopedDecoratorData(context, clientNamespaceKey, entity);
if (override) return override;
if (override) {
const rootNamespaceName = diagnostics.pipe(getRootUserDefinedNamespaceName(context));
// if you've defined a namespace flag, we replace the root name with the flag
return diagnostics.wrap(
override.replace(rootNamespaceName, context.namespace || rootNamespaceName),
Copy link
Member

Choose a reason for hiding this comment

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

better to judge undefined of context.namespace to prevent always find rootNamespaceName part.

);
}
if (!entity.namespace) {
return "";
return diagnostics.wrap(context.namespace || "");
}
if (entity.kind === "Namespace") {
return getNamespaceFullNameWithOverride(context, entity);
}
return getNamespaceFullNameWithOverride(context, entity.namespace);
}

function getNamespaceFullNameWithOverride(context: TCGCContext, namespace: Namespace): string {
function getNamespaceFullNameWithOverride(
context: TCGCContext,
namespace: Namespace,
): [string, readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();
const segments = [];
let current: Namespace | undefined = namespace;
while (current && current.name !== "") {
Expand All @@ -1233,7 +1247,13 @@ function getNamespaceFullNameWithOverride(context: TCGCContext, namespace: Names
segments.unshift(current.name);
current = current.namespace;
}
return segments.join(".");
if (context.namespace) {
const rootNamespaceName = diagnostics.pipe(getRootUserDefinedNamespaceName(context));

return diagnostics.wrap(segments.join(".").replace(rootNamespaceName, context.namespace));
}

return diagnostics.wrap(segments.join("."));
}

export const $scope: ScopeDecorator = (
Expand Down
5 changes: 5 additions & 0 deletions packages/typespec-client-generator-core/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export interface TCGCContext {
previewStringRegex: RegExp;
disableUsageAccessPropagationToBase: boolean;
__pagedResultSet: Set<SdkType>;
namespace?: string;
}

export interface SdkContext<
Expand All @@ -79,6 +80,7 @@ export interface SdkEmitterOptions {
"examples-directory"?: string;
"examples-dir"?: string;
"emitter-name"?: string;
namespace?: string;
}

// Types for TCGC customization decorators
Expand Down Expand Up @@ -848,6 +850,9 @@ export type SdkMethod<TServiceOperation extends SdkServiceOperation> =

export interface SdkPackage<TServiceOperation extends SdkServiceOperation> {
name: string;
/**
* @deprecated Look at `.namespaces` instead
*/
rootNamespace: string;
clients: SdkClientType<TServiceOperation>[];
models: SdkModelType[];
Expand Down
46 changes: 45 additions & 1 deletion packages/typespec-client-generator-core/src/internal-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
HttpOperationResponseContent,
} from "@typespec/http";
import { getAddedOnVersions, getRemovedOnVersions, getVersions } from "@typespec/versioning";
import { getParamAlias } from "./decorators.js";
import { getParamAlias, listClients } from "./decorators.js";
import {
DecoratorInfo,
SdkBuiltInType,
Expand Down Expand Up @@ -83,6 +83,8 @@ export function parseEmitterName(
}

/**
*
* @deprecated Use `.namespaces` from sdkContext.sdkPackage instead
*
* @param context
* @param namespace If we know explicitly the namespace of the client, pass this in
Expand Down Expand Up @@ -593,3 +595,45 @@ export function getValueTypeValue(
return undefined;
}
}

/**
* Get all of the root user-defined namespaces in a TypeSpec definition if they're tied to a client
* @param context
* @returns
*/
export function getRootUserDefinedNamespaceName(
context: TCGCContext,
): [string, readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();
const rootNamespaces = [...context.program.getGlobalNamespaceType().namespaces.values()];
tadelesh marked this conversation as resolved.
Show resolved Hide resolved

// We explicitly want namespaces that are used to define a client, not namespaces that define the service of a client
const clientNamespaces = listClients(context).map((x) =>
x.type.kind === "Namespace" ? x.type : x.type.namespace,
);
const globalNamespaces: string[] = [];
for (const namespace of clientNamespaces) {
const segments = [];
let currNamespace: Namespace | undefined = namespace;
while (currNamespace) {
segments.unshift(currNamespace.name);
if (rootNamespaces.includes(currNamespace)) {
globalNamespaces.push(segments.join("."));
break;
}
currNamespace = currNamespace.namespace;
}
}
// if we override with namespace flag, we should override the global namespace to the namespace flag
if (globalNamespaces.length !== 1) {
diagnostics.add(
createDiagnostic({
code: "unclear-namespace-overriding",
messageId: "default",
format: {},
target: context.program.getGlobalNamespaceType(),
}),
);
Comment on lines +629 to +636
Copy link
Member

@tadelesh tadelesh Feb 8, 2025

Choose a reason for hiding this comment

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

can we just report there is multiple user defined ns? coupling this function with namespace override config seems not good.

}
return diagnostics.wrap(globalNamespaces[0] || "");
}
7 changes: 7 additions & 0 deletions packages/typespec-client-generator-core/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const EmitterOptionsSchema: JSONSchemaType<SdkEmitterOptions> = {
"examples-directory": { type: "string", nullable: true },
"examples-dir": { type: "string", nullable: true },
"emitter-name": { type: "string", nullable: true },
namespace: { type: "string", nullable: true },
iscai-msft marked this conversation as resolved.
Show resolved Hide resolved
},
};

Expand Down Expand Up @@ -256,6 +257,12 @@ export const $lib = createTypeSpecLibrary({
default: paramMessage`Invalid 'initializedBy' value. ${"message"}`,
},
},
"unclear-namespace-overriding": {
severity: "error",
messages: {
default: paramMessage`Since there are multiple user-defined root namespaces, it is unclear what namespace you are attempting to override with the namespace flag config. Please either consolidate your tsp so there is only one root namespace in your spec, or remove the namespace flag from your config`,
},
},
},
emitter: {
options: EmitterOptionsSchema as JSONSchemaType<SdkEmitterOptions>,
Expand Down
34 changes: 20 additions & 14 deletions packages/typespec-client-generator-core/src/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import {
SdkClient,
SdkClientInitializationType,
SdkClientType,
SdkContext,
SdkEmitterOptions,
SdkEndpointParameter,
SdkEndpointType,
SdkEnumType,
Expand Down Expand Up @@ -582,7 +584,7 @@ function getSdkInitializationType(
access,
usage: UsageFlags.Input,
crossLanguageDefinitionId: `${getNamespaceFullName(client.service.namespace!)}.${name}`,
clientNamespace: getClientNamespace(context, client.type),
clientNamespace: diagnostics.pipe(getClientNamespace(context, client.type)),
apiVersions: context.__tspTypeToApiVersions.get(client.type)!,
decorators: [],
serializationOptions: {},
Expand Down Expand Up @@ -850,7 +852,7 @@ function getSdkEndpointParameter<TServiceOperation extends SdkServiceOperation =
name: createGeneratedName(context, rawClient.service, "Endpoint"),
isGeneratedName: true,
crossLanguageDefinitionId: `${getCrossLanguageDefinitionId(context, rawClient.service)}.Endpoint`,
clientNamespace: getClientNamespace(context, rawClient.service),
clientNamespace: diagnostics.pipe(getClientNamespace(context, rawClient.service)),
decorators: [],
} as SdkUnionType<SdkEndpointType>;
} else {
Expand Down Expand Up @@ -886,8 +888,8 @@ function createSdkClientType<TServiceOperation extends SdkServiceOperation>(
summary: getSummary(context.program, client.type),
methods: [],
apiVersions: context.__tspTypeToApiVersions.get(client.type)!,
nameSpace: getClientNamespaceStringHelper(context, client.service)!,
clientNamespace: getClientNamespace(context, client.type),
nameSpace: getClientNamespaceStringHelper(context, client.service)!, // eslint-disable-line @typescript-eslint/no-deprecated
clientNamespace: diagnostics.pipe(getClientNamespace(context, client.type)),
initialization: diagnostics.pipe(getSdkInitializationType(context, client)),
clientInitialization: diagnostics.pipe(createSdkClientInitializationType(context, client)),
decorators: diagnostics.pipe(getTypeDecorators(context, client.type)),
Expand All @@ -913,7 +915,7 @@ function addDefaultClientParameters<
const defaultClientParamters = [];
// there will always be an endpoint property
defaultClientParamters.push(diagnostics.pipe(getSdkEndpointParameter(context, client)));
const credentialParam = getSdkCredentialParameter(context, client.__raw);
const credentialParam = diagnostics.pipe(getSdkCredentialParameter(context, client.__raw));
if (credentialParam) {
defaultClientParamters.push(credentialParam);
}
Expand Down Expand Up @@ -985,17 +987,20 @@ function populateApiVersionInformation(context: TCGCContext): void {
}
}

export function getSdkPackage<TServiceOperation extends SdkServiceOperation>(
context: TCGCContext,
export function getSdkPackage<
TOptions extends Record<string, any> = SdkEmitterOptions,
TServiceOperation extends SdkServiceOperation = SdkHttpOperation,
>(
context: SdkContext<TOptions, TServiceOperation>,
): [SdkPackage<TServiceOperation>, readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();
populateApiVersionInformation(context);
diagnostics.pipe(handleAllTypes(context));
const crossLanguagePackageId = diagnostics.pipe(getCrossLanguagePackageId(context));
const allReferencedTypes = getAllReferencedTypes(context);
const sdkPackage: SdkPackage<TServiceOperation> = {
name: getClientNamespaceString(context)!,
rootNamespace: getClientNamespaceString(context)!,
name: getClientNamespaceString(context)!, // eslint-disable-line @typescript-eslint/no-deprecated
rootNamespace: getClientNamespaceString(context)!, // eslint-disable-line @typescript-eslint/no-deprecated
clients: listClients(context).map((c) => diagnostics.pipe(createSdkClientType(context, c))),
models: allReferencedTypes.filter((x): x is SdkModelType => x.kind === "model"),
enums: allReferencedTypes.filter((x): x is SdkEnumType => x.kind === "enum"),
Expand All @@ -1005,13 +1010,14 @@ export function getSdkPackage<TServiceOperation extends SdkServiceOperation>(
crossLanguagePackageId,
namespaces: [],
};
organizeNamespaces(sdkPackage);
organizeNamespaces(context, sdkPackage);
return diagnostics.wrap(sdkPackage);
}

function organizeNamespaces<TServiceOperation extends SdkServiceOperation>(
sdkPackage: SdkPackage<TServiceOperation>,
) {
function organizeNamespaces<
TOptions extends Record<string, any> = SdkEmitterOptions,
TServiceOperation extends SdkServiceOperation = SdkHttpOperation,
>(context: SdkContext<TOptions, TServiceOperation>, sdkPackage: SdkPackage<TServiceOperation>) {
const clients = [...sdkPackage.clients];
while (clients.length > 0) {
const client = clients.shift()!;
Expand All @@ -1032,7 +1038,7 @@ function organizeNamespaces<TServiceOperation extends SdkServiceOperation>(
}
}

function getSdkNamespace<TServiceOperation extends SdkServiceOperation>(
function getSdkNamespace<TServiceOperation extends SdkServiceOperation = SdkHttpOperation>(
sdkPackage: SdkPackage<TServiceOperation>,
namespace: string,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,15 @@ export function isApiVersion(context: TCGCContext, type: { name: string }): bool
/**
* Get the client's namespace for generation. If package-name is passed in config, we return
* that value as our namespace. Otherwise, we default to the TypeSpec service namespace.
*
* @deprecated Access namespace information by iterating through `sdkPackage.namespaces` instead
*
* @param program
* @param context
* @returns
*/
export function getClientNamespaceString(context: TCGCContext): string | undefined {
return getClientNamespaceStringHelper(context, listServices(context.program)[0]?.type);
return getClientNamespaceStringHelper(context, listServices(context.program)[0]?.type); // eslint-disable-line @typescript-eslint/no-deprecated
}

/**
Expand Down
Loading
Loading