diff --git a/.chronus/changes/typespec-metadata-java-namespace-prefix-2026-03-23.md b/.chronus/changes/typespec-metadata-java-namespace-prefix-2026-03-23.md new file mode 100644 index 0000000000..9594ea244e --- /dev/null +++ b/.chronus/changes/typespec-metadata-java-namespace-prefix-2026-03-23.md @@ -0,0 +1,11 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-metadata" +--- + +Update Java namespace to apply the correct Azure prefix based on service type and flavor: +- `com.azure.` for current data plane services +- `com.azure.resourcemanager.` for current ARM/management services +- `com.azure.v2.` for next-gen data plane services (flavor: `azurev2`) +- `com.azure.resourcemanager.v2.` for next-gen ARM services (flavor: `azurev2`) diff --git a/packages/typespec-metadata/src/collector.ts b/packages/typespec-metadata/src/collector.ts index da580152cc..f8548889ed 100644 --- a/packages/typespec-metadata/src/collector.ts +++ b/packages/typespec-metadata/src/collector.ts @@ -64,6 +64,59 @@ interface EmitterRegistration { parser: LanguageParser; } +/** + * Known Azure Java namespace prefixes in order of specificity. + * Used to strip and replace prefixes based on service type and flavor. + */ +const JAVA_AZURE_PREFIXES = [ + "com.azure.resourcemanager.v2.", + "com.azure.v2.", + "com.azure.resourcemanager.", + "com.azure.", +] as const; + +/** + * Determines the correct Java namespace prefix based on service type and flavor. + * - com.azure. : current data plane + * - com.azure.resourcemanager. : current ARM/management + * - com.azure.v2. : next-gen data plane (flavor: azurev2) + * - com.azure.resourcemanager.v2. : next-gen ARM (flavor: azurev2) + */ +function getJavaNamespacePrefix(isManagement: boolean, isAzureV2: boolean): string { + if (isManagement) { + return isAzureV2 ? "com.azure.resourcemanager.v2." : "com.azure.resourcemanager."; + } + return isAzureV2 ? "com.azure.v2." : "com.azure."; +} + +/** + * Applies the correct Java namespace prefix based on service type and flavor. + * If the namespace already has a known Azure prefix, it is replaced with the correct one. + * If the namespace does not start with a known Azure prefix, the correct prefix is prepended. + */ +function applyJavaNamespacePrefix( + namespace: string, + isManagement: boolean, + isAzureV2: boolean, +): string { + const targetPrefix = getJavaNamespacePrefix(isManagement, isAzureV2); + + // If namespace already has the correct prefix, no change needed + if (namespace.startsWith(targetPrefix)) { + return namespace; + } + + // Strip any existing Azure prefix and replace with the correct one + for (const prefix of JAVA_AZURE_PREFIXES) { + if (namespace.startsWith(prefix)) { + return targetPrefix + namespace.substring(prefix.length); + } + } + + // No known Azure prefix found — prepend the correct prefix + return targetPrefix + namespace; +} + const EMITTER_REGISTRY: Record = { "@azure-tools/typespec-csharp": { language: "csharp", parser: parseCSharp }, "@azure-tools/typespec-java": { language: "java", parser: parseJava }, @@ -86,6 +139,7 @@ interface LanguageParserResult { type LanguageParser = ( options: Record, params: Record, + typespecType?: "data" | "management", ) => LanguageParserResult; /** @@ -120,19 +174,32 @@ function parsePython( /** * Java-specific metadata parser. - * Strips 'com.' prefix from namespace if present for package name derivation. + * Applies the correct Azure Java namespace prefix based on service type and flavor: + * - com.azure. : current data plane + * - com.azure.resourcemanager. : current ARM/management + * - com.azure.v2. : next-gen data plane (flavor: azurev2) + * - com.azure.resourcemanager.v2. : next-gen ARM (flavor: azurev2) */ function parseJava( options: Record, params: Record, + typespecType?: "data" | "management", ): LanguageParserResult { let packageName = options["package-name"] ?? options["package_name"]; - const namespace = options["namespace"]; + const rawNamespace = options["namespace"]; - if (namespace && !packageName) { - const ns = String(namespace); - const stripped = ns.startsWith("com.") ? ns.substring(4) : ns; - packageName = stripped.replace(/\./g, "-"); + const flavor = options["flavor"]; + const isAzureV2 = flavor === "azurev2"; + const isManagement = typespecType === "management"; + + let namespace: string | undefined; + if (rawNamespace) { + namespace = applyJavaNamespacePrefix(String(rawNamespace), isManagement, isAzureV2); + + if (!packageName) { + const stripped = namespace.startsWith("com.") ? namespace.substring(4) : namespace; + packageName = stripped.replace(/\./g, "-"); + } } return { @@ -257,6 +324,7 @@ export interface LanguageCollectionResult { export async function collectLanguagePackages( program: Program, baseOutputDir: string, + typespecType?: "data" | "management", ): Promise { const optionMap = program.compilerOptions.options ?? {}; const params = extractParameters(optionMap); @@ -272,7 +340,7 @@ export async function collectLanguagePackages( } return { - languages: buildLanguageMetadata(optionMap, params, baseOutputDir, defaultServiceDir), + languages: buildLanguageMetadata(optionMap, params, baseOutputDir, defaultServiceDir, typespecType), sourceConfigPath: program.compilerOptions.config, }; } @@ -419,6 +487,7 @@ export function buildLanguageMetadata( params: Record, baseOutputDir: string, defaultServiceDir?: string, + typespecType?: "data" | "management", ): Record { const languagesDict: Record = {}; @@ -429,6 +498,7 @@ export function buildLanguageMetadata( params, baseOutputDir, defaultServiceDir, + typespecType, ); const language = inferLanguageFromEmitterName(emitterName); languagesDict[language] = metadata; @@ -443,6 +513,7 @@ function createLanguageMetadata( params: Record, baseOutputDir: string, defaultServiceDir?: string, + typespecType?: "data" | "management", ): LanguagePackageMetadata { const normalizedOptions = normalizeOptionsObject(emitterOptions); @@ -463,7 +534,7 @@ function createLanguageMetadata( const registration = EMITTER_REGISTRY[normalizedEmitterName]; if (registration) { - const result = registration.parser(normalizedOptions, params); + const result = registration.parser(normalizedOptions, params, typespecType); packageName = result.packageName; namespace = result.namespace; } else { diff --git a/packages/typespec-metadata/src/emitter.ts b/packages/typespec-metadata/src/emitter.ts index a4c5402049..e813971b46 100644 --- a/packages/typespec-metadata/src/emitter.ts +++ b/packages/typespec-metadata/src/emitter.ts @@ -18,7 +18,7 @@ export async function $onEmit(context: EmitContext): Pro // Get the common tsp-output directory (parent of this emitter's output dir) const commonOutputDir = getDirectoryPath(getDirectoryPath(context.emitterOutputDir)); - const languageResult = await collectLanguagePackages(context.program, commonOutputDir); + const languageResult = await collectLanguagePackages(context.program, commonOutputDir, typespecMetadata.type); const snapshot: MetadataSnapshot = { emitterVersion: SNAPSHOT_VERSION, diff --git a/packages/typespec-metadata/test/collector.test.ts b/packages/typespec-metadata/test/collector.test.ts index f512afd9fb..2f735185eb 100644 --- a/packages/typespec-metadata/test/collector.test.ts +++ b/packages/typespec-metadata/test/collector.test.ts @@ -183,6 +183,131 @@ describe("language-specific parsers", () => { }); }); +describe("Java namespace prefix handling", () => { + it("should keep data-plane namespace with com.azure. prefix unchanged when no flavor", () => { + const optionMap: Record> = { + "@azure-tools/typespec-java": { + namespace: "com.azure.security.keyvault.secrets", + }, + }; + + const result = buildLanguageMetadata(optionMap, {}, "/output", undefined, "data"); + expect(result["java"].namespace).toBe("com.azure.security.keyvault.secrets"); + expect(result["java"].packageName).toBe("azure-security-keyvault-secrets"); + }); + + it("should keep management-plane namespace with com.azure.resourcemanager. prefix unchanged when no flavor", () => { + const optionMap: Record> = { + "@azure-tools/typespec-java": { + namespace: "com.azure.resourcemanager.contoso", + }, + }; + + const result = buildLanguageMetadata(optionMap, {}, "/output", undefined, "management"); + expect(result["java"].namespace).toBe("com.azure.resourcemanager.contoso"); + expect(result["java"].packageName).toBe("azure-resourcemanager-contoso"); + }); + + it("should update data-plane namespace to com.azure.v2. when flavor is azurev2", () => { + const optionMap: Record> = { + "@azure-tools/typespec-java": { + namespace: "com.azure.security.keyvault.secrets", + flavor: "azurev2", + }, + }; + + const result = buildLanguageMetadata(optionMap, {}, "/output", undefined, "data"); + expect(result["java"].namespace).toBe("com.azure.v2.security.keyvault.secrets"); + expect(result["java"].packageName).toBe("azure-v2-security-keyvault-secrets"); + }); + + it("should update management-plane namespace to com.azure.resourcemanager.v2. when flavor is azurev2", () => { + const optionMap: Record> = { + "@azure-tools/typespec-java": { + namespace: "com.azure.resourcemanager.contoso", + flavor: "azurev2", + }, + }; + + const result = buildLanguageMetadata(optionMap, {}, "/output", undefined, "management"); + expect(result["java"].namespace).toBe("com.azure.resourcemanager.v2.contoso"); + expect(result["java"].packageName).toBe("azure-resourcemanager-v2-contoso"); + }); + + it("should update data-plane namespace to com.azure.resourcemanager. when typespecType is management", () => { + const optionMap: Record> = { + "@azure-tools/typespec-java": { + namespace: "com.azure.contoso", + }, + }; + + const result = buildLanguageMetadata(optionMap, {}, "/output", undefined, "management"); + expect(result["java"].namespace).toBe("com.azure.resourcemanager.contoso"); + }); + + it("should not change namespace if it already has the correct v2 prefix", () => { + const optionMap: Record> = { + "@azure-tools/typespec-java": { + namespace: "com.azure.v2.security.keyvault.secrets", + flavor: "azurev2", + }, + }; + + const result = buildLanguageMetadata(optionMap, {}, "/output", undefined, "data"); + expect(result["java"].namespace).toBe("com.azure.v2.security.keyvault.secrets"); + }); + + it("should not change namespace if it already has the correct resourcemanager.v2 prefix", () => { + const optionMap: Record> = { + "@azure-tools/typespec-java": { + namespace: "com.azure.resourcemanager.v2.contoso", + flavor: "azurev2", + }, + }; + + const result = buildLanguageMetadata(optionMap, {}, "/output", undefined, "management"); + expect(result["java"].namespace).toBe("com.azure.resourcemanager.v2.contoso"); + }); + + it("should prepend correct prefix to namespace without any Azure prefix (data plane)", () => { + const optionMap: Record> = { + "@azure-tools/typespec-java": { + namespace: "security.keyvault.secrets", + }, + }; + + const result = buildLanguageMetadata(optionMap, {}, "/output", undefined, "data"); + expect(result["java"].namespace).toBe("com.azure.security.keyvault.secrets"); + }); + + it("should prepend correct prefix to namespace without any Azure prefix (management plane, azurev2)", () => { + const optionMap: Record> = { + "@azure-tools/typespec-java": { + namespace: "contoso", + flavor: "azurev2", + }, + }; + + const result = buildLanguageMetadata(optionMap, {}, "/output", undefined, "management"); + expect(result["java"].namespace).toBe("com.azure.resourcemanager.v2.contoso"); + }); + + it("should preserve explicit package-name when namespace is updated", () => { + const optionMap: Record> = { + "@azure-tools/typespec-java": { + "package-name": "azure-security-keyvault-secrets", + namespace: "com.azure.security.keyvault.secrets", + flavor: "azurev2", + }, + }; + + const result = buildLanguageMetadata(optionMap, {}, "/output", undefined, "data"); + expect(result["java"].namespace).toBe("com.azure.v2.security.keyvault.secrets"); + // Explicit package-name should be preserved + expect(result["java"].packageName).toBe("azure-security-keyvault-secrets"); + }); +}); + describe("service-dir handling", () => { it("should use language-specific service-dir if present", () => { const languageServiceDir = "sdk/security/keyvault";