Skip to content

Commit

Permalink
http-client-java, namespace from client.tsp (#5051)
Browse files Browse the repository at this point in the history
link Azure/autorest.java#2968

current limitation:
- operation group be in same package of client
- subclient be in same package of client

mostly this due to the fact that ctor of the client is package private,
so it won't work if client and its builder or client of accessor method
are in different packages

---

At this PR, unbranded would still have `.models` in package for models.
We can tweak this later (by setting `modelsSubPackage` in JavaSetting to
empty, when unbranded).

---

tested and it be backward compatible, if `namespace` is provided in
emitter option
https://github.com/Azure/azure-sdk-for-java/pull/43055/files
  • Loading branch information
weidongxu-microsoft authored Nov 22, 2024
1 parent 0f8b1e2 commit 2e1c857
Show file tree
Hide file tree
Showing 260 changed files with 1,513 additions and 582 deletions.
135 changes: 96 additions & 39 deletions packages/http-client-java/emitter/src/code-model-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ import {
} from "@azure-tools/typespec-client-generator-core";
import {
EmitContext,
Interface,
Model,
ModelProperty,
Namespace,
Expand Down Expand Up @@ -168,11 +167,13 @@ export class CodeModelBuilder {
private program: Program;
private typeNameOptions: TypeNameOptions;
private namespace: string;
private baseJavaNamespace: string = ""; // it will be set at the start of "build" function
private legacyJavaNamespace: boolean = false; // backward-compatible mode, that emitter ignores clientNamespace from TCGC
private sdkContext!: SdkContext;
private options: EmitterOptions;
private codeModel: CodeModel;
private emitterContext: EmitContext<EmitterOptions>;
private serviceNamespace: Namespace | Interface | Operation;
private serviceNamespace: Namespace;

private loggingEnabled: boolean = false;

Expand Down Expand Up @@ -205,8 +206,6 @@ export class CodeModelBuilder {
this.serviceNamespace = service.type;

this.namespace = getNamespaceFullName(this.serviceNamespace) || "Azure.Client";
// java namespace
const javaNamespace = this.getJavaNamespace(this.namespace);

const namespace1 = this.namespace;
this.typeNameOptions = {
Expand All @@ -232,9 +231,7 @@ export class CodeModelBuilder {
summary: this.getSummary(this.serviceNamespace),
namespace: this.namespace,
},
java: {
namespace: javaNamespace,
},
java: {},
},
});
}
Expand All @@ -244,6 +241,21 @@ export class CodeModelBuilder {
versioning: { previewStringRegex: /$/ },
}); // include all versions and do the filter by ourselves

// java namespace
if (this.options.namespace) {
// legacy mode, clientNamespace from TCGC will be ignored
this.legacyJavaNamespace = true;
this.baseJavaNamespace = this.options.namespace;
} else {
this.legacyJavaNamespace = false;
// baseJavaNamespace is used for model from Azure.Core/Azure.ResourceManager but cannot be mapped to azure-core,
// or some model (e.g. Options, FileDetails) that is created in this emitter.
// otherwise, the clientNamespace from SdkType will be used.
this.baseJavaNamespace = this.getBaseJavaNamespace();
}

this.codeModel.language.java!.namespace = this.baseJavaNamespace;

// TODO: reportDiagnostics from TCGC temporary disabled
// issue https://github.com/Azure/typespec-azure/issues/1675
// this.program.reportDiagnostics(this.sdkContext.diagnostics);
Expand Down Expand Up @@ -502,13 +514,13 @@ export class CodeModelBuilder {

private processClient(client: SdkClientType<SdkHttpOperation>): CodeModelClient {
let clientName = client.name;
let javaNamespace = this.getJavaNamespace(this.namespace);
let javaNamespace = this.getJavaNamespace(client);
const clientFullName = client.name;
const clientNameSegments = clientFullName.split(".");
if (clientNameSegments.length > 1) {
clientName = clientNameSegments.at(-1)!;
const clientSubNamespace = clientNameSegments.slice(0, -1).join(".");
javaNamespace = this.getJavaNamespace(this.namespace + "." + clientSubNamespace);
const clientSubNamespace = clientNameSegments.slice(0, -1).join(".").toLowerCase();
javaNamespace = javaNamespace + "." + clientSubNamespace;
}

const codeModelClient = new CodeModelClient(clientName, client.doc ?? "", {
Expand Down Expand Up @@ -978,7 +990,7 @@ export class CodeModelBuilder {
language: {
java: {
name: "OperationLocationPollingStrategy",
namespace: this.getJavaNamespace(this.namespace) + ".implementation",
namespace: this.baseJavaNamespace + ".implementation",
},
},
});
Expand Down Expand Up @@ -1505,7 +1517,7 @@ export class CodeModelBuilder {
namespace: namespace,
},
java: {
namespace: this.getJavaNamespace(namespace),
namespace: this.getJavaNamespace(),
},
},
}),
Expand Down Expand Up @@ -1975,7 +1987,7 @@ export class CodeModelBuilder {
namespace: namespace,
},
java: {
namespace: this.getJavaNamespace(namespace),
namespace: this.getJavaNamespace(type),
},
},
});
Expand Down Expand Up @@ -2075,7 +2087,7 @@ export class CodeModelBuilder {
namespace: namespace,
},
java: {
namespace: this.getJavaNamespace(namespace),
namespace: this.getJavaNamespace(type),
},
},
});
Expand Down Expand Up @@ -2165,7 +2177,7 @@ export class CodeModelBuilder {
if (type.kind === "Model") {
const effective = getEffectiveModelType(program, type, isSchemaProperty);
if (this.isArm() && getNamespace(effective as Model)?.startsWith("Azure.ResourceManager")) {
// Catalog is TrackedResource<CatalogProperties>
// e.g. typespec: Catalog is TrackedResource<CatalogProperties>
return type;
} else if (effective.name) {
return effective;
Expand Down Expand Up @@ -2257,7 +2269,7 @@ export class CodeModelBuilder {
namespace: namespace,
},
java: {
namespace: this.getJavaNamespace(namespace),
namespace: this.getJavaNamespace(),
},
},
});
Expand Down Expand Up @@ -2343,15 +2355,20 @@ export class CodeModelBuilder {

private processMultipartFormDataFilePropertySchema(property: SdkBodyModelPropertyType): Schema {
const processSchemaFunc = (type: SdkType) => this.processSchema(type, "");
if (property.type.kind === "bytes" || property.type.kind === "model") {
const processNamespaceFunc = (type: SdkBuiltInType | SdkModelType) => {
const namespace =
property.type.kind === "model"
? (getNamespace(property.type.__raw) ?? this.namespace)
: this.namespace;
type.kind === "model" ? (getNamespace(type.__raw) ?? this.namespace) : this.namespace;
const javaNamespace =
type.kind === "model" ? this.getJavaNamespace(type) : this.getJavaNamespace();
return { namespace, javaNamespace };
};

if (property.type.kind === "bytes" || property.type.kind === "model") {
const namespaceTuple = processNamespaceFunc(property.type);
return getFileDetailsSchema(
property,
getNamespace(property.type.__raw) ?? this.namespace,
namespace,
namespaceTuple.namespace,
namespaceTuple.javaNamespace,
this.codeModel.schemas,
this.binarySchema,
this.stringSchema,
Expand All @@ -2361,17 +2378,14 @@ export class CodeModelBuilder {
property.type.kind === "array" &&
(property.type.valueType.kind === "bytes" || property.type.valueType.kind === "model")
) {
const namespace =
property.type.valueType.kind === "model"
? (getNamespace(property.type.valueType.__raw) ?? this.namespace)
: this.namespace;
const namespaceTuple = processNamespaceFunc(property.type.valueType);
return new ArraySchema(
property.name,
property.doc ?? "",
getFileDetailsSchema(
property,
namespace,
this.getJavaNamespace(namespace),
namespaceTuple.namespace,
namespaceTuple.javaNamespace,
this.codeModel.schemas,
this.binarySchema,
this.stringSchema,
Expand Down Expand Up @@ -2467,18 +2481,61 @@ export class CodeModelBuilder {
}
}

private getJavaNamespace(namespace: string | undefined): string | undefined {
const tspNamespace = this.namespace;
const baseJavaNamespace = this.emitterContext.options.namespace;
if (!namespace) {
return undefined;
} else if (
baseJavaNamespace &&
(namespace === tspNamespace || namespace.startsWith(tspNamespace + "."))
) {
return baseJavaNamespace + namespace.slice(tspNamespace.length).toLowerCase();
private getBaseJavaNamespace(): string {
// hack, just find the shortest clientNamespace among all clients
// hopefully it is the root namespace of the SDK
let baseJavaNamespace: string | undefined = undefined;
this.sdkContext.sdkPackage.clients
.map((it) => it.clientNamespace)
.forEach((it) => {
if (baseJavaNamespace === undefined || baseJavaNamespace.length > it.length) {
baseJavaNamespace = it;
}
});
// fallback if there is no client
if (!baseJavaNamespace) {
baseJavaNamespace = this.namespace;
}
return baseJavaNamespace.toLowerCase();
}

private getJavaNamespace(
type:
| SdkModelType
| SdkEnumType
| SdkUnionType
| SdkClientType<SdkHttpOperation>
| undefined = undefined,
): string | undefined {
// clientNamespace from TCGC
const clientNamespace: string | undefined = type?.clientNamespace;

if (this.isBranded() && type) {
// special handling for namespace of model that cannot be mapped to azure-core
if (type.crossLanguageDefinitionId === "TypeSpec.Http.File") {
// TypeSpec.Http.File
return this.baseJavaNamespace;
} else if (type.crossLanguageDefinitionId === "Azure.Core.Foundations.OperationState") {
// Azure.Core.OperationState
return this.baseJavaNamespace;
} else if (
type.crossLanguageDefinitionId === "Azure.Core.ResourceOperationStatus" ||
type.crossLanguageDefinitionId === "Azure.Core.Foundations.OperationStatus"
) {
// Azure.Core.ResourceOperationStatus<>
// Azure.Core.Foundations.OperationStatus<>
// usually this model will not be generated, but javadoc of protocol method requires it be in SDK namespace
return this.baseJavaNamespace;
} else if (type.crossLanguageDefinitionId.startsWith("Azure.ResourceManager.")) {
// models in Azure.ResourceManager
return this.baseJavaNamespace;
}
}

if (this.legacyJavaNamespace || !clientNamespace) {
return this.baseJavaNamespace;
} else {
return namespace.toLowerCase();
return clientNamespace.toLowerCase();
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/http-client-java/emitter/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export function pascalCase(name: string): string {
export function getNamespace(type: Type | undefined): string | undefined {
if (
type &&
(type.kind === "Model" ||
(type.kind === "Interface" ||
type.kind === "Model" ||
type.kind === "Enum" ||
type.kind === "Union" ||
type.kind === "Operation")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-core</artifactId>
<version>1.54.0</version>
<version>1.54.1</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,17 @@ public final String getPackage() {
* @return The package name with the provided package suffixes appended.
*/
public final String getPackage(String... packageSuffixes) {
return getPackageName(packageName, packageSuffixes);
}

/**
* Get the package name with the provided package suffixes appended.
*
* @param packageName the package name.
* @param packageSuffixes The package suffixes to append to the package name.
* @return The package name with the provided package suffixes appended.
*/
public final String getPackageName(String packageName, String... packageSuffixes) {
StringBuilder packageBuilder = new StringBuilder(packageName);
if (packageSuffixes != null) {
for (String packageSuffix : packageSuffixes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,22 @@ public static IType createEnumType(ChoiceSchema enumType, boolean expandable, bo
if (enumTypeName == null || enumTypeName.isEmpty() || enumTypeName.equals("enum")) {
return ClassType.STRING;
} else {
String enumPackage = settings.getPackage(settings.getModelsSubpackage());
String enumPackage = settings.getPackage();
String[] packageSuffixes;
if (settings.isCustomType(enumTypeName)) {
enumPackage = settings.getPackage(settings.getCustomTypesSubpackage());
packageSuffixes = new String[] { settings.getCustomTypesSubpackage() };
} else if (settings.isDataPlaneClient()
&& (enumType.getUsage() != null && enumType.getUsage().contains(SchemaContext.INTERNAL))) {
// internal type, which is not exposed to user
enumPackage
= settings.getPackage(settings.getImplementationSubpackage(), settings.getModelsSubpackage());
packageSuffixes
= new String[] { settings.getImplementationSubpackage(), settings.getModelsSubpackage() };
} else {
packageSuffixes = new String[] { settings.getModelsSubpackage() };
}
if (!CoreUtils.isNullOrEmpty(enumType.getLanguage().getJava().getNamespace())) {
enumPackage = settings.getPackageName(enumType.getLanguage().getJava().getNamespace(), packageSuffixes);
} else {
enumPackage = settings.getPackage(packageSuffixes);
}

String summary = enumType.getSummary();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,7 @@ private static boolean hasFlattenedProperty(ObjectSchema compositeType,
* @return Whether the type is predefined.
*/
protected boolean isPredefinedModel(ClassType compositeType) {
if (JavaSettings.getInstance().isDataPlaneClient()) {
if (JavaSettings.getInstance().isDataPlaneClient() && JavaSettings.getInstance().isBranded()) {
// see ObjectMapper.mapPredefinedModel
// this might be too simplified, and Android might require a different implementation
return compositeType.getPackage().startsWith(ExternalPackage.CORE.getPackageName() + ".");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

package com.microsoft.typespec.http.client.generator.core.mapper;

import com.azure.core.util.CoreUtils;
import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema;
import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.SchemaContext;
import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings;
Expand Down Expand Up @@ -45,27 +46,38 @@ private ClassType createClassType(ObjectSchema compositeType) {
}

String classPackage;
String[] packageSuffixes;
String className = compositeType.getLanguage().getJava().getName();
if (settings.isCustomType(compositeType.getLanguage().getJava().getName())) {
classPackage = settings.getPackage(settings.getCustomTypesSubpackage());
packageSuffixes = new String[] { settings.getCustomTypesSubpackage() };
} else if (settings.isFluent() && isInnerModel(compositeType)) {
className += "Inner";
classPackage = settings.getPackage(settings.getFluentModelsSubpackage());
packageSuffixes = new String[] { settings.getFluentModelsSubpackage() };
} else if (settings.isFluent() && compositeType.isFlattenedSchema()) {
// put class of flattened type to implementation package
classPackage = settings.getPackage(settings.getFluentModelsSubpackage());
// put class of flattened type to fluent package
packageSuffixes = new String[] { settings.getFluentModelsSubpackage() };
} else if (settings.isDataPlaneClient() && isInternalModel(compositeType)) {
// internal type is not exposed to user
classPackage = settings.getPackage(settings.getImplementationSubpackage(), settings.getModelsSubpackage());
packageSuffixes = new String[] { settings.getImplementationSubpackage(), settings.getModelsSubpackage() };
} else if (isPageModel(compositeType)) {
// put class of Page<> type to implementation package
// for DPG from TypeSpec, these are not generated to class

// this would not affect mgmt from Swagger, as the "usage" from m4 does not have this information.

classPackage = settings.getPackage(settings.getImplementationSubpackage(), settings.getModelsSubpackage());
packageSuffixes = new String[] { settings.getImplementationSubpackage(), settings.getModelsSubpackage() };
} else {
classPackage = settings.getPackage(settings.getModelsSubpackage());
packageSuffixes = new String[] { settings.getModelsSubpackage() };
}
/*
* For models with language.java.namespace, it would be used as the base package name to append suffixes.
* Otherwise, the packageName in JavaSetting is the base package name.
*/
if (!CoreUtils.isNullOrEmpty(compositeType.getLanguage().getJava().getNamespace())) {
classPackage
= settings.getPackageName(compositeType.getLanguage().getJava().getNamespace(), packageSuffixes);
} else {
classPackage = settings.getPackage(packageSuffixes);
}

return new ClassType.Builder().packageName(classPackage)
Expand All @@ -82,7 +94,11 @@ private ClassType createClassType(ObjectSchema compositeType) {
* @return The predefined type.
*/
protected ClassType mapPredefinedModel(ObjectSchema compositeType) {
return SchemaUtil.mapExternalModel(compositeType);
if (JavaSettings.getInstance().isBranded()) {
return SchemaUtil.mapExternalModel(compositeType);
} else {
return null;
}
}

/**
Expand Down
Loading

0 comments on commit 2e1c857

Please sign in to comment.