Skip to content

Commit

Permalink
http-client-java, accessor to subclient (microsoft#5027)
Browse files Browse the repository at this point in the history
Link Azure/autorest.java#2972

Current limitation
- sub client need to be in same namespace of its parent.

Feature behind `enable-subclient` option (`false` means
client/operationGroup; `true` means all client, with client having
accessor to get subclient)

unbranded openai https://github.com/joseharriaga/openai-in-typespec

## Bug to be fixed in other PRs (relate to unbranded openai, but
unrelated to this feature -- moved to
microsoft#5047)
- microsoft#5026
- microsoft#5030
- microsoft#5028
- microsoft#5031

## Test

1. cadl-ranch PR Azure/cadl-ranch#727 (also
tried on test with more than 2 level subclient -- nested namespaces)
2. openai unbranded
https://github.com/joseharriaga/openai-in-typespec/compare/main...weidongxu-microsoft:java-sub-client?expand=1
([apiview](https://apiview.dev/Assemblies/Review/b974bacf316c495aa5c30ce0b7e07b1c/d1d7278c251d4805b189fd238714897d#com.openai.OpenAIClient))
3. Face API Azure/azure-sdk-for-java#42875
  • Loading branch information
weidongxu-microsoft authored Nov 20, 2024
1 parent 53a475d commit 432ca32
Show file tree
Hide file tree
Showing 33 changed files with 1,637 additions and 173 deletions.
258 changes: 137 additions & 121 deletions packages/http-client-java/emitter/src/code-model-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,106 +496,120 @@ export class CodeModelBuilder {

const sdkPackage = this.sdkContext.sdkPackage;
for (const client of sdkPackage.clients) {
let clientName = client.name;
let javaNamespace = this.getJavaNamespace(this.namespace);
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 codeModelClient = new CodeModelClient(clientName, client.doc ?? "", {
summary: client.summary,
language: {
default: {
namespace: this.namespace,
},
java: {
namespace: javaNamespace,
},
this.processClient(client);
}
}

private processClient(client: SdkClientType<SdkHttpOperation>): CodeModelClient {
let clientName = client.name;
let javaNamespace = this.getJavaNamespace(this.namespace);
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 codeModelClient = new CodeModelClient(clientName, client.doc ?? "", {
summary: client.summary,
language: {
default: {
namespace: this.namespace,
},
java: {
namespace: javaNamespace,
},
},

// at present, use global security definition
security: this.codeModel.security,
});
codeModelClient.crossLanguageDefinitionId = client.crossLanguageDefinitionId;
// at present, use global security definition
security: this.codeModel.security,
});
codeModelClient.crossLanguageDefinitionId = client.crossLanguageDefinitionId;

// versioning
const versions = client.apiVersions;
if (versions && versions.length > 0) {
if (!this.sdkContext.apiVersion || ["all", "latest"].includes(this.sdkContext.apiVersion)) {
this.apiVersion = versions[versions.length - 1];
} else {
this.apiVersion = versions.find((it: string) => it === this.sdkContext.apiVersion);
if (!this.apiVersion) {
throw new Error("Unrecognized api-version: " + this.sdkContext.apiVersion);
}
// versioning
const versions = client.apiVersions;
if (versions && versions.length > 0) {
if (!this.sdkContext.apiVersion || ["all", "latest"].includes(this.sdkContext.apiVersion)) {
this.apiVersion = versions[versions.length - 1];
} else {
this.apiVersion = versions.find((it: string) => it === this.sdkContext.apiVersion);
if (!this.apiVersion) {
throw new Error("Unrecognized api-version: " + this.sdkContext.apiVersion);
}
}

codeModelClient.apiVersions = [];
for (const version of this.getFilteredApiVersions(
this.apiVersion,
versions,
this.options["service-version-exclude-preview"],
)) {
const apiVersion = new ApiVersion();
apiVersion.version = version;
codeModelClient.apiVersions.push(apiVersion);
}
codeModelClient.apiVersions = [];
for (const version of this.getFilteredApiVersions(
this.apiVersion,
versions,
this.options["service-version-exclude-preview"],
)) {
const apiVersion = new ApiVersion();
apiVersion.version = version;
codeModelClient.apiVersions.push(apiVersion);
}
}

// client initialization
let baseUri = "{endpoint}";
let hostParameters: Parameter[] = [];
client.initialization.properties.forEach((initializationProperty) => {
if (initializationProperty.kind === "endpoint") {
let sdkPathParameters: SdkPathParameter[] = [];
if (initializationProperty.type.kind === "union") {
if (initializationProperty.type.variantTypes.length === 2) {
// only get the sdkPathParameters from the endpoint whose serverUrl is not {"endpoint"}
for (const endpointType of initializationProperty.type.variantTypes) {
if (endpointType.kind === "endpoint" && endpointType.serverUrl !== "{endpoint}") {
sdkPathParameters = endpointType.templateArguments;
baseUri = endpointType.serverUrl;
}
// client initialization
let baseUri = "{endpoint}";
let hostParameters: Parameter[] = [];
client.initialization.properties.forEach((initializationProperty) => {
if (initializationProperty.kind === "endpoint") {
let sdkPathParameters: SdkPathParameter[] = [];
if (initializationProperty.type.kind === "union") {
if (initializationProperty.type.variantTypes.length === 2) {
// only get the sdkPathParameters from the endpoint whose serverUrl is not {"endpoint"}
for (const endpointType of initializationProperty.type.variantTypes) {
if (endpointType.kind === "endpoint" && endpointType.serverUrl !== "{endpoint}") {
sdkPathParameters = endpointType.templateArguments;
baseUri = endpointType.serverUrl;
}
} else if (initializationProperty.type.variantTypes.length > 2) {
throw new Error("Multiple server url defined for one client is not supported yet.");
}
} else if (initializationProperty.type.kind === "endpoint") {
sdkPathParameters = initializationProperty.type.templateArguments;
baseUri = initializationProperty.type.serverUrl;
} else if (initializationProperty.type.variantTypes.length > 2) {
throw new Error("Multiple server url defined for one client is not supported yet.");
}

hostParameters = this.processHostParameters(sdkPathParameters);
codeModelClient.addGlobalParameters(hostParameters);
} else if (initializationProperty.type.kind === "endpoint") {
sdkPathParameters = initializationProperty.type.templateArguments;
baseUri = initializationProperty.type.serverUrl;
}
});

const clientContext = new ClientContext(
baseUri,
hostParameters,
codeModelClient.globalParameters!,
codeModelClient.apiVersions,
);

// preprocess operation groups and operations
// operations without operation group
const serviceMethodsWithoutSubClient = this.listServiceMethodsUnderClient(client);
let codeModelGroup = new OperationGroup("");
for (const serviceMethod of serviceMethodsWithoutSubClient) {
if (!this.needToSkipProcessingOperation(serviceMethod.__raw, clientContext)) {
codeModelGroup.addOperation(this.processOperation(serviceMethod, clientContext, ""));
}
hostParameters = this.processHostParameters(sdkPathParameters);
codeModelClient.addGlobalParameters(hostParameters);
}
if (codeModelGroup.operations?.length > 0) {
codeModelClient.operationGroups.push(codeModelGroup);
});

const clientContext = new ClientContext(
baseUri,
hostParameters,
codeModelClient.globalParameters!,
codeModelClient.apiVersions,
);

const enableSubclient: boolean = Boolean(this.options["enable-subclient"]);

// preprocess operation groups and operations
// operations without operation group
const serviceMethodsWithoutSubClient = this.listServiceMethodsUnderClient(client);
let codeModelGroup = new OperationGroup("");
for (const serviceMethod of serviceMethodsWithoutSubClient) {
if (!this.needToSkipProcessingOperation(serviceMethod.__raw, clientContext)) {
codeModelGroup.addOperation(this.processOperation(serviceMethod, clientContext, ""));
}
}
if (codeModelGroup.operations?.length > 0 || enableSubclient) {
codeModelClient.operationGroups.push(codeModelGroup);
}

const subClients = this.listSubClientsUnderClient(client, !enableSubclient);
if (enableSubclient) {
// subclient, no operation group
for (const subClient of subClients) {
const codeModelSubclient = this.processClient(subClient);
codeModelClient.addSubClient(codeModelSubclient);
}
} else {
// operations under operation groups
const subClients = this.listSubClientsUnderClient(client, true, true);
for (const subClient of subClients) {
const serviceMethods = this.listServiceMethodsUnderClient(subClient);
// operation group with no operation is skipped
Expand All @@ -611,48 +625,51 @@ export class CodeModelBuilder {
codeModelClient.operationGroups.push(codeModelGroup);
}
}
this.codeModel.clients.push(codeModelClient);
}

this.codeModel.clients.push(codeModelClient);

// postprocess for ServiceVersion
let apiVersionSameForAllClients = true;
let sharedApiVersions = undefined;
// postprocess for ServiceVersion
let apiVersionSameForAllClients = true;
let sharedApiVersions = undefined;
for (const client of this.codeModel.clients) {
const apiVersions = client.apiVersions;
if (!apiVersions) {
// client does not have apiVersions
apiVersionSameForAllClients = false;
} else if (!sharedApiVersions) {
// first client, set it to sharedApiVersions
sharedApiVersions = apiVersions;
} else {
apiVersionSameForAllClients = isEqual(sharedApiVersions, apiVersions);
}
if (!apiVersionSameForAllClients) {
break;
}
}
if (apiVersionSameForAllClients) {
const serviceVersion = getServiceVersion(this.codeModel);
for (const client of this.codeModel.clients) {
const apiVersions = client.apiVersions;
if (!apiVersions) {
// client does not have apiVersions
apiVersionSameForAllClients = false;
} else if (!sharedApiVersions) {
// first client, set it to sharedApiVersions
sharedApiVersions = apiVersions;
} else {
apiVersionSameForAllClients = isEqual(sharedApiVersions, apiVersions);
}
if (!apiVersionSameForAllClients) {
break;
}
client.serviceVersion = serviceVersion;
}
if (apiVersionSameForAllClients) {
const serviceVersion = getServiceVersion(this.codeModel);
for (const client of this.codeModel.clients) {
client.serviceVersion = serviceVersion;
}
} else {
for (const client of this.codeModel.clients) {
const apiVersions = client.apiVersions;
if (apiVersions) {
client.serviceVersion = getServiceVersion(client);
}
} else {
for (const client of this.codeModel.clients) {
const apiVersions = client.apiVersions;
if (apiVersions) {
client.serviceVersion = getServiceVersion(client);
}
}
}

return codeModelClient;
}

private listSubClientsUnderClient(
client: SdkClientType<SdkHttpOperation>,
includeNestedOperationGroups: boolean,
isRootClient: boolean,
includeNestedSubClients: boolean,
): SdkClientType<SdkHttpOperation>[] {
const operationGroups: SdkClientType<SdkHttpOperation>[] = [];
const isRootClient = !client.parent;
const subClients: SdkClientType<SdkHttpOperation>[] = [];
for (const method of client.methods) {
if (method.kind === "clientaccessor") {
const subClient = method.response;
Expand All @@ -661,19 +678,18 @@ export class CodeModelBuilder {
subClient.name =
removeClientSuffix(client.name) + removeClientSuffix(pascalCase(subClient.name));
}
operationGroups.push(subClient);
if (includeNestedOperationGroups) {
subClients.push(subClient);
if (includeNestedSubClients) {
for (const operationGroup of this.listSubClientsUnderClient(
subClient,
includeNestedOperationGroups,
false,
includeNestedSubClients,
)) {
operationGroups.push(operationGroup);
subClients.push(operationGroup);
}
}
}
}
return operationGroups;
return subClients;
}

private listServiceMethodsUnderClient(
Expand Down
18 changes: 18 additions & 0 deletions packages/http-client-java/emitter/src/common/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export interface Client extends Aspect, CrossLanguageDefinition {
security: Security;

serviceVersion?: ServiceVersion; // apiVersions is in

parent?: Client;
subClients: Array<Client>;
buildMethodPublic: boolean;
parentAccessorPublic: boolean;
}

export class Client extends Aspect implements Client {
Expand All @@ -19,6 +24,9 @@ export class Client extends Aspect implements Client {

this.operationGroups = [];
this.security = new Security(false);
this.subClients = [];
this.buildMethodPublic = true;
this.parentAccessorPublic = false;

this.applyTo(this, objectInitializer);
}
Expand All @@ -30,6 +38,16 @@ export class Client extends Aspect implements Client {
addGlobalParameters(parameters: Parameter[]) {
this.globals.push(...parameters);
}

addSubClient(subClient: Client) {
subClient.parent = this;
subClient.buildMethodPublic = false;
subClient.parentAccessorPublic = true;
this.subClients.push(subClient);

// at present, sub client must have same namespace of its parent client
subClient.language.java!.namespace = this.language.java!.namespace;
}
}

export class ServiceVersion extends Metadata {
Expand Down
6 changes: 5 additions & 1 deletion packages/http-client-java/emitter/src/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export interface EmitterOptions {

"group-etag-headers"?: boolean;

"enable-subclient"?: boolean;

"advanced-versioning"?: boolean;
"api-version"?: string;
"service-version-exclude-preview"?: boolean;
Expand Down Expand Up @@ -78,7 +80,7 @@ const EmitterOptionsSchema: JSONSchemaType<EmitterOptions> = {

"enable-sync-stack": { type: "boolean", nullable: true, default: true },
"stream-style-serialization": { type: "boolean", nullable: true, default: true },
"use-object-for-unknown": { type: "boolean", nullable: true, default: true },
"use-object-for-unknown": { type: "boolean", nullable: true, default: false },

// customization
"partial-update": { type: "boolean", nullable: true, default: false },
Expand All @@ -90,6 +92,8 @@ const EmitterOptionsSchema: JSONSchemaType<EmitterOptions> = {

"group-etag-headers": { type: "boolean", nullable: true },

"enable-subclient": { type: "boolean", nullable: true, default: false },

"advanced-versioning": { type: "boolean", nullable: true, default: false },
"api-version": { type: "string", nullable: true },
"service-version-exclude-preview": { type: "boolean", nullable: true, default: false },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ protected JavaPackage writeToTemplates(CodeModel codeModel, Client client, JavaS
// Test
if (settings.isDataPlaneClient() && settings.isGenerateTests()) {
if (!client.getSyncClients().isEmpty()
&& client.getSyncClients().iterator().next().getClientBuilder() != null) {
&& client.getSyncClients().stream().anyMatch(c -> c.getClientBuilder() != null)) {
List<ServiceClient> serviceClients = client.getServiceClients();
if (CoreUtils.isNullOrEmpty(serviceClients)) {
serviceClients = Collections.singletonList(client.getServiceClient());
Expand Down
Loading

0 comments on commit 432ca32

Please sign in to comment.