Skip to content
Closed
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
@@ -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`)
87 changes: 79 additions & 8 deletions packages/typespec-metadata/src/collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, EmitterRegistration> = {
"@azure-tools/typespec-csharp": { language: "csharp", parser: parseCSharp },
"@azure-tools/typespec-java": { language: "java", parser: parseJava },
Expand All @@ -86,6 +139,7 @@ interface LanguageParserResult {
type LanguageParser = (
options: Record<string, unknown>,
params: Record<string, unknown>,
typespecType?: "data" | "management",
) => LanguageParserResult;

/**
Expand Down Expand Up @@ -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<string, unknown>,
params: Record<string, unknown>,
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 {
Expand Down Expand Up @@ -257,6 +324,7 @@ export interface LanguageCollectionResult {
export async function collectLanguagePackages(
program: Program,
baseOutputDir: string,
typespecType?: "data" | "management",
): Promise<LanguageCollectionResult> {
const optionMap = program.compilerOptions.options ?? {};
const params = extractParameters(optionMap);
Expand All @@ -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,
};
}
Expand Down Expand Up @@ -419,6 +487,7 @@ export function buildLanguageMetadata(
params: Record<string, unknown>,
baseOutputDir: string,
defaultServiceDir?: string,
typespecType?: "data" | "management",
): Record<string, LanguagePackageMetadata> {
const languagesDict: Record<string, LanguagePackageMetadata> = {};

Expand All @@ -429,6 +498,7 @@ export function buildLanguageMetadata(
params,
baseOutputDir,
defaultServiceDir,
typespecType,
);
const language = inferLanguageFromEmitterName(emitterName);
languagesDict[language] = metadata;
Expand All @@ -443,6 +513,7 @@ function createLanguageMetadata(
params: Record<string, unknown>,
baseOutputDir: string,
defaultServiceDir?: string,
typespecType?: "data" | "management",
): LanguagePackageMetadata {
const normalizedOptions = normalizeOptionsObject(emitterOptions);

Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion packages/typespec-metadata/src/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function $onEmit(context: EmitContext<MetadataEmitterOptions>): 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,
Expand Down
125 changes: 125 additions & 0 deletions packages/typespec-metadata/test/collector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Record<string, unknown>> = {
"@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<string, Record<string, unknown>> = {
"@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<string, Record<string, unknown>> = {
"@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<string, Record<string, unknown>> = {
"@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<string, Record<string, unknown>> = {
"@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<string, Record<string, unknown>> = {
"@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<string, Record<string, unknown>> = {
"@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<string, Record<string, unknown>> = {
"@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<string, Record<string, unknown>> = {
"@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<string, Record<string, unknown>> = {
"@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";
Expand Down