@typespec/http-client-java
Version:
TypeSpec library for emitting Java client from the TypeSpec REST protocol binding
976 lines (975 loc) • 128 kB
JavaScript
import { AnySchema, ApiVersion, ArraySchema, BinaryResponse, BinarySchema, BooleanSchema, ByteArraySchema, ChoiceValue, DateSchema, DateTimeSchema, DictionarySchema, Discriminator, GroupProperty, GroupSchema, HttpHeader, HttpParameter, ImplementationLocation, KeySecurityScheme, Language, License, Metadata, NumberSchema, OAuth2SecurityScheme, ObjectSchema, OperationGroup, Parameter, ParameterLocation, Property, Relations, Response, SchemaResponse, SchemaType, Security, SerializationStyle, StringSchema, TimeSchema, UnixTimeSchema, UriSchema, UuidSchema, VirtualParameter, } from "@autorest/codemodel";
import { KnownMediaType } from "@azure-tools/codegen";
import { InitializedByFlags, createSdkContext, getAllModels, getClientNameOverride, getHttpOperationParameter, isHttpMetadata, isSdkBuiltInKind, isSdkIntKind, } from "@azure-tools/typespec-client-generator-core";
import { NoTarget, getDoc, getNamespaceFullName, getOverloadedOperation, getSummary, isArrayModelType, isRecordModelType, listServices, } from "@typespec/compiler";
import { Visibility, getAuthentication, } from "@typespec/http";
import { getSegment } from "@typespec/rest";
import { getAddedOnVersions } from "@typespec/versioning";
import { fail } from "assert";
import pkg from "lodash";
import { Client as CodeModelClient, PageableContinuationToken, } from "./common/client.js";
import { CodeModel } from "./common/code-model.js";
import { LongRunningMetadata } from "./common/long-running-metadata.js";
import { Operation as CodeModelOperation, ConvenienceApi, Request } from "./common/operation.js";
import { ChoiceSchema, SealedChoiceSchema } from "./common/schemas/choice.js";
import { ConstantSchema, ConstantValue } from "./common/schemas/constant.js";
import { OrSchema } from "./common/schemas/relationship.js";
import { DurationSchema } from "./common/schemas/time.js";
import { SchemaContext } from "./common/schemas/usage.js";
import { createPollOperationDetailsSchema, getFileDetailsSchema } from "./external-schemas.js";
import { createDiagnostic, reportDiagnostic } from "./lib.js";
import { ClientContext } from "./models.js";
import { CONTENT_TYPE_KEY, ORIGIN_API_VERSION, SPECIAL_HEADER_NAMES, cloneOperationParameter, findResponsePropertySegments, getServiceVersion, isKnownContentType, isLroNewPollingStrategy, operationIsJsonMergePatch, operationIsMultipart, operationIsMultipleContentTypes, } from "./operation-utils.js";
import { LIB_NAME } from "./options.js";
import { BYTES_KNOWN_ENCODING, DATETIME_KNOWN_ENCODING, DURATION_KNOWN_ENCODING, ProcessingCache, getAccess, getDurationFormat, getExternalJavaClassName, getNonNullSdkType, getPropertySerializedName, getUnionDescription, getUsage, getXmlSerializationFormat, modelIs, pushDistinct, } from "./type-utils.js";
import { DiagnosticError, escapeJavaKeywords, getNamespace, optionBoolean, pascalCase, removeClientSuffix, stringArrayContainsIgnoreCase, trace, } from "./utils.js";
import { getFilteredApiVersions, getServiceApiVersions } from "./versioning-utils.js";
const { isEqual } = pkg;
const AZURE_CORE_FOUNDATIONS_ERROR_ID = "Azure.Core.Foundations.Error";
export class CodeModelBuilder {
program;
typeNameOptions;
namespace;
baseJavaNamespace;
sdkContext;
options;
codeModel;
emitterContext;
serviceNamespace;
javaNamespaceCache = new Map();
schemaCache = new ProcessingCache((type, name) => this.processSchemaImpl(type, name));
// current apiVersion name to generate code
apiVersion;
constructor(program1, context) {
this.options = context.options;
this.program = program1;
this.emitterContext = context;
if (this.options["skip-special-headers"]) {
this.options["skip-special-headers"].forEach((it) => SPECIAL_HEADER_NAMES.add(it.toLowerCase()));
}
const service = listServices(this.program)[0];
if (!service) {
reportDiagnostic(this.program, {
code: "no-service",
target: NoTarget,
});
}
this.serviceNamespace = service?.type ?? this.program.getGlobalNamespaceType();
this.namespace = getNamespaceFullName(this.serviceNamespace) || "Client";
const namespace1 = this.namespace;
this.typeNameOptions = {
// shorten type names by removing TypeSpec and service namespace
namespaceFilter(ns) {
const name = getNamespaceFullName(ns);
return name !== "TypeSpec" && name !== namespace1;
},
};
// init code model
const title = this.options["service-name"] ?? this.serviceNamespace.name;
const description = this.getDoc(this.serviceNamespace);
this.codeModel = new CodeModel(title, false, {
info: {
description: description,
},
language: {
default: {
name: title,
description: description,
summary: this.getSummary(this.serviceNamespace),
namespace: this.namespace,
},
java: {},
},
});
}
async build() {
if (this.program.hasError()) {
return this.codeModel;
}
this.sdkContext = await createSdkContext(this.emitterContext, LIB_NAME, {
additionalDecorators: ["Azure\\.ClientGenerator\\.Core\\.@override"],
versioning: { previewStringRegex: /$/ },
}); // include all versions and do the filter by ourselves
this.program.reportDiagnostics(this.sdkContext.diagnostics);
// license
if (this.sdkContext.sdkPackage.licenseInfo) {
this.codeModel.info.license = new License(this.sdkContext.sdkPackage.licenseInfo.name, {
url: this.sdkContext.sdkPackage.licenseInfo.link,
extensions: {
header: this.sdkContext.sdkPackage.licenseInfo.header,
company: this.sdkContext.sdkPackage.licenseInfo.company,
},
});
}
// 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.
if (this.options.namespace) {
this.baseJavaNamespace = this.options.namespace;
}
else {
this.baseJavaNamespace = this.getBaseJavaNamespace();
}
this.codeModel.language.java.namespace = this.baseJavaNamespace;
// auth
// TODO: it is not very likely, but different client could have different auth
const auth = getAuthentication(this.program, this.serviceNamespace);
if (auth) {
this.processAuth(auth, this.serviceNamespace);
}
if (this.sdkContext.arm) {
// ARM
this.codeModel.arm = true;
this.options["group-etag-headers"] = false;
}
this.processClients();
this.processModels();
this.processSchemaUsage();
this.deduplicateSchemaName();
return this.codeModel;
}
processHostParameters(sdkPathParameters) {
const hostParameters = [];
let parameter;
sdkPathParameters.forEach((arg) => {
if (this.isApiVersionParameter(arg)) {
parameter = this.createApiVersionParameter(arg.name, ParameterLocation.Uri);
}
else {
const schema = this.processSchema(arg.type, arg.name);
this.trackSchemaUsage(schema, {
usage: [SchemaContext.Input, SchemaContext.Output, SchemaContext.Public],
});
parameter = new Parameter(arg.name, arg.doc ?? "", schema, {
implementation: ImplementationLocation.Client,
origin: "modelerfour:synthesized/host",
required: true,
protocol: {
http: new HttpParameter(ParameterLocation.Uri),
},
language: {
default: {
serializedName: arg.serializedName,
},
},
extensions: {
"x-ms-skip-url-encoding": arg.allowReserved,
},
clientDefaultValue: arg.clientDefaultValue,
});
}
hostParameters.push(this.codeModel.addGlobalParameter(parameter));
});
return hostParameters;
}
processAuth(auth, serviceNamespace) {
const securitySchemes = [];
for (const option of auth.options) {
for (const scheme of option.schemes) {
switch (scheme.type) {
case "oauth2":
{
const oauth2Scheme = new OAuth2SecurityScheme({
scopes: [],
});
scheme.flows.forEach((it) => oauth2Scheme.scopes.push(...it.scopes.map((it) => it.value)));
oauth2Scheme.flows = scheme.flows;
securitySchemes.push(oauth2Scheme);
}
break;
case "apiKey":
{
if (scheme.in === "header") {
const keyScheme = new KeySecurityScheme({
name: scheme.name,
});
securitySchemes.push(keyScheme);
}
else {
reportDiagnostic(this.program, {
code: "auth-scheme-not-supported",
messageId: "apiKeyLocation",
target: serviceNamespace,
});
}
}
break;
case "http":
{
let schemeOrApiKeyPrefix = scheme.scheme;
if (schemeOrApiKeyPrefix === "basic" || schemeOrApiKeyPrefix === "bearer") {
// HTTP Authentication should use "Basic token" or "Bearer token"
schemeOrApiKeyPrefix = pascalCase(schemeOrApiKeyPrefix);
if (this.isBranded()) {
// Azure would not allow BasicAuth or BearerAuth
reportDiagnostic(this.program, {
code: "auth-scheme-not-supported",
messageId: "basicAuthBranded",
format: { scheme: scheme.scheme },
target: serviceNamespace,
});
continue;
}
}
const keyScheme = new KeySecurityScheme({
name: "authorization",
});
keyScheme.prefix = schemeOrApiKeyPrefix; // TODO: modify KeySecurityScheme, after design stable
securitySchemes.push(keyScheme);
}
break;
}
}
}
if (securitySchemes.length > 0) {
this.codeModel.security = new Security(true, {
schemes: securitySchemes,
});
}
}
isBranded() {
return (this.options["flavor"]?.toLocaleLowerCase() === "azure" ||
this.options["flavor"]?.toLocaleLowerCase() === "azurev2");
}
isAzureV1() {
return this.options["flavor"]?.toLocaleLowerCase() === "azure";
}
isAzureV2() {
return this.options["flavor"]?.toLocaleLowerCase() === "azurev2";
}
processModels() {
const processedSdkModels = new Set();
// cache resolved value of access/usage for the namespace
// the value can be set as undefined
// it resolves the value from that namespace and its parent namespaces
const accessCache = new Map();
const usageCache = new Map();
const sdkModels = getAllModels(this.sdkContext);
// process sdk models
for (const model of sdkModels) {
if (!processedSdkModels.has(model)) {
const access = getAccess(model.__raw, accessCache);
if (access === "public") {
const schema = this.processSchema(model, "");
this.trackSchemaUsage(schema, {
usage: [SchemaContext.Public],
});
}
else if (access === "internal") {
const schema = this.processSchema(model, model.name);
this.trackSchemaUsage(schema, {
usage: [SchemaContext.Internal],
});
}
const usage = getUsage(model.__raw, usageCache);
if (usage) {
const schema = this.processSchema(model, "");
this.trackSchemaUsage(schema, {
usage: usage,
});
}
processedSdkModels.add(model);
}
}
}
processSchemaUsage() {
this.codeModel.schemas.objects?.forEach((it) => this.propagateSchemaUsage(it));
// post process for schema usage
this.codeModel.schemas.objects?.forEach((it) => this.resolveSchemaUsage(it));
this.codeModel.schemas.groups?.forEach((it) => this.resolveSchemaUsage(it));
this.codeModel.schemas.choices?.forEach((it) => this.resolveSchemaUsage(it));
this.codeModel.schemas.sealedChoices?.forEach((it) => this.resolveSchemaUsage(it));
this.codeModel.schemas.ors?.forEach((it) => this.resolveSchemaUsage(it));
this.codeModel.schemas.constants?.forEach((it) => this.resolveSchemaUsage(it));
}
deduplicateSchemaName() {
// deduplicate model name
// packages to skip
const packagesToSkip = [];
if (!this.isBranded() || this.isAzureV2()) {
// clientcore
packagesToSkip.push("io.clientcore.core.");
}
if (this.isAzureV2()) {
// core v2
packagesToSkip.push("com.azure.v2.core.");
}
if (this.isAzureV1()) {
// core
packagesToSkip.push("com.azure.core.");
}
const nameCount = new Map();
const deduplicateName = (schema) => {
// skip models under "com.azure.core." etc. in java, or "Azure." in typespec, if branded
// skip models under "io.clientcore.core." in java, if unbranded
const skipDeduplicate = (this.isBranded() && schema.language.default?.namespace?.startsWith("Azure.")) ||
(schema.language.java?.namespace &&
packagesToSkip.some((it) => schema.language.java?.namespace.startsWith(it)));
const name = schema.language.default.name;
if (name && !skipDeduplicate) {
if (!nameCount.has(name)) {
nameCount.set(name, 1);
}
else {
const count = nameCount.get(name);
nameCount.set(name, count + 1);
schema.language.default.name = name + count;
}
}
};
this.codeModel.schemas.objects?.forEach((it) => deduplicateName(it));
this.codeModel.schemas.groups?.forEach((it) => deduplicateName(it)); // it may contain RequestConditions under "com.azure.core."
this.codeModel.schemas.choices?.forEach((it) => deduplicateName(it));
this.codeModel.schemas.sealedChoices?.forEach((it) => deduplicateName(it));
this.codeModel.schemas.ors?.forEach((it) => deduplicateName(it));
this.codeModel.schemas.constants?.forEach((it) => deduplicateName(it));
}
resolveSchemaUsage(schema) {
if (schema instanceof ObjectSchema ||
schema instanceof GroupSchema ||
schema instanceof ChoiceSchema ||
schema instanceof SealedChoiceSchema ||
schema instanceof OrSchema ||
schema instanceof ConstantSchema) {
const schemaUsage = schema.usage;
// Public override Internal
if (schemaUsage?.includes(SchemaContext.Public)) {
const index = schemaUsage.indexOf(SchemaContext.Internal);
if (index >= 0) {
schemaUsage.splice(index, 1);
}
}
// Internal on PublicSpread, but Public takes precedence
if (schemaUsage?.includes(SchemaContext.PublicSpread)) {
// remove PublicSpread as it now served its purpose
schemaUsage.splice(schemaUsage.indexOf(SchemaContext.PublicSpread), 1);
// Public would override PublicSpread, hence do nothing if this schema is Public
if (!schemaUsage?.includes(SchemaContext.Public)) {
// set the model as Internal, so that it is not exposed to user
if (!schemaUsage.includes(SchemaContext.Internal)) {
schemaUsage.push(SchemaContext.Internal);
}
}
}
}
}
processClients() {
// preprocess group-etag-headers
this.options["group-etag-headers"] = this.options["group-etag-headers"] ?? true;
const sdkPackage = this.sdkContext.sdkPackage;
for (const client of sdkPackage.clients) {
this.processClient(client);
}
}
processClient(client) {
let clientName = client.name;
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(".").toLowerCase();
javaNamespace = javaNamespace + "." + clientSubNamespace;
}
if (this.isArm()) {
if (this.options["service-name"] &&
client.__raw &&
client.__raw.type &&
!getClientNameOverride(this.sdkContext, client.__raw.type)) {
// When no `@clientName` override, use "service-name" to infer the client name
clientName = this.options["service-name"].replace(/\s+/g, "") + "ManagementClient";
}
}
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.language.default.crossLanguageDefinitionId = client.crossLanguageDefinitionId;
// versioning
const versions = getServiceApiVersions(this.program, client);
if (versions && versions.length > 0) {
if (!this.sdkContext.apiVersion || ["all", "latest"].includes(this.sdkContext.apiVersion)) {
this.apiVersion = versions[versions.length - 1].value;
}
else {
this.apiVersion = versions.find((it) => it.value === this.sdkContext.apiVersion)?.value;
if (!this.apiVersion) {
reportDiagnostic(this.program, {
code: "invalid-api-version",
format: { apiVersion: this.sdkContext.apiVersion },
target: NoTarget,
});
}
}
codeModelClient.apiVersions = [];
for (const version of getFilteredApiVersions(this.program, this.apiVersion, versions, this.options["service-version-exclude-preview"])) {
const apiVersion = new ApiVersion();
apiVersion.version = version.value;
codeModelClient.apiVersions.push(apiVersion);
}
}
// client initialization
let baseUri = "{endpoint}";
let hostParameters = [];
client.clientInitialization.parameters.forEach((initializationProperty) => {
if (initializationProperty.kind === "endpoint") {
let sdkPathParameters = [];
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) {
reportDiagnostic(this.program, {
code: "multiple-server-not-supported",
target: initializationProperty.type.__raw ?? NoTarget,
});
}
}
else if (initializationProperty.type.kind === "endpoint") {
sdkPathParameters = initializationProperty.type.templateArguments;
baseUri = initializationProperty.type.serverUrl;
}
hostParameters = this.processHostParameters(sdkPathParameters);
codeModelClient.addGlobalParameters(hostParameters);
}
});
const clientContext = new ClientContext(baseUri, hostParameters, codeModelClient.globalParameters, codeModelClient.apiVersions);
const enableSubclient = optionBoolean(this.options["enable-subclient"]) ?? false;
// preprocess operation groups and operations
// operations without operation group
const serviceMethodsWithoutSubClient = client.methods;
let codeModelGroup = new OperationGroup("");
codeModelGroup.language.default.crossLanguageDefinitionId = client.crossLanguageDefinitionId;
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);
const buildMethodPublic = Boolean(subClient.clientInitialization.initializedBy & InitializedByFlags.Individually);
const parentAccessorPublic = Boolean(subClient.clientInitialization.initializedBy & InitializedByFlags.Parent ||
subClient.clientInitialization.initializedBy === InitializedByFlags.Default);
codeModelClient.addSubClient(codeModelSubclient, buildMethodPublic, parentAccessorPublic);
}
}
else {
// operations under operation groups
for (const subClient of subClients) {
const serviceMethods = subClient.methods;
// operation group with no operation is skipped
if (serviceMethods.length > 0) {
codeModelGroup = new OperationGroup(subClient.name);
codeModelGroup.language.default.crossLanguageDefinitionId =
subClient.crossLanguageDefinitionId;
for (const serviceMethod of serviceMethods) {
if (!this.needToSkipProcessingOperation(serviceMethod.__raw, clientContext)) {
codeModelGroup.addOperation(this.processOperation(serviceMethod, clientContext, subClient.name));
}
}
codeModelClient.operationGroups.push(codeModelGroup);
}
}
}
this.codeModel.clients.push(codeModelClient);
// 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) {
client.serviceVersion = serviceVersion;
}
}
else {
for (const client of this.codeModel.clients) {
const apiVersions = client.apiVersions;
if (apiVersions) {
client.serviceVersion = getServiceVersion(client);
}
}
}
return codeModelClient;
}
listSubClientsUnderClient(client, includeNestedSubClients) {
const isRootClient = !client.parent;
const subClients = [];
if (client.children) {
for (const subClient of client.children) {
if (!isRootClient) {
// if it is not root client, append the parent client's name
subClient.name =
removeClientSuffix(client.name) + removeClientSuffix(pascalCase(subClient.name));
}
subClients.push(subClient);
if (includeNestedSubClients) {
for (const operationGroup of this.listSubClientsUnderClient(subClient, includeNestedSubClients)) {
subClients.push(operationGroup);
}
}
}
}
return subClients;
}
needToSkipProcessingOperation(operation, clientContext) {
// don't generate protocol and convenience method for overloaded operations
// issue link: https://github.com/Azure/autorest.java/issues/1958#issuecomment-1562558219 we will support generate overload methods for non-union type in future (TODO issue: https://github.com/Azure/autorest.java/issues/2160)
if (operation === undefined) {
return true;
}
if (getOverloadedOperation(this.program, operation)) {
this.trace(`Operation '${operation.name}' is temporary skipped, as it is an overloaded operation`);
return true;
}
return false;
}
/**
* Whether we support advanced versioning in non-breaking fashion.
*/
supportsAdvancedVersioning() {
return optionBoolean(this.options["advanced-versioning"]) ?? false;
}
getOperationExample(sdkMethod) {
const httpOperationExamples = sdkMethod.operation.examples;
if (httpOperationExamples && httpOperationExamples.length > 0) {
const operationExamples = {};
for (const example of httpOperationExamples) {
const operationExample = example.rawExample;
// example.filePath is relative path from sdkContext.examplesDir
// this is not a URL format (file:// or https://)
operationExample["x-ms-original-file"] = example.filePath;
operationExamples[operationExample.title ?? operationExample.operationId ?? sdkMethod.name] = operationExample;
}
return operationExamples;
}
else {
return undefined;
}
}
processOperation(sdkMethod, clientContext, groupName) {
const operationName = sdkMethod.name;
const httpOperation = sdkMethod.operation;
const operationId = groupName ? `${groupName}_${operationName}` : `${operationName}`;
const operationExamples = this.getOperationExample(sdkMethod);
const codeModelOperation = new CodeModelOperation(operationName, sdkMethod.doc ?? "", {
operationId: operationId,
summary: sdkMethod.summary,
extensions: {
"x-ms-examples": operationExamples,
},
});
codeModelOperation.language.default.crossLanguageDefinitionId =
sdkMethod.crossLanguageDefinitionId;
codeModelOperation.internalApi = sdkMethod.access === "internal";
const convenienceApiName = this.getConvenienceApiName(sdkMethod);
let generateConvenienceApi = sdkMethod.generateConvenient;
let generateProtocolApi = sdkMethod.generateProtocol;
let diagnostic = undefined;
if (generateConvenienceApi) {
// check if the convenience API need to be disabled for some special cases
if (operationIsMultipart(httpOperation)) {
// do not generate protocol method for multipart/form-data, as it be very hard for user to prepare the request body as BinaryData
generateProtocolApi = false;
diagnostic = createDiagnostic({
code: "protocol-api-not-generated",
messageId: "multipartFormData",
format: { operationName: operationName },
target: sdkMethod.__raw ?? NoTarget,
});
this.program.reportDiagnostic(diagnostic);
}
else if (operationIsMultipleContentTypes(httpOperation)) {
// and multiple content types
// issue link: https://github.com/Azure/autorest.java/issues/1958#issuecomment-1562558219
generateConvenienceApi = false;
diagnostic = createDiagnostic({
code: "convenience-api-not-generated",
messageId: "multipleContentType",
format: { operationName: operationName },
target: sdkMethod.__raw ?? NoTarget,
});
this.program.reportDiagnostic(diagnostic);
}
else if (operationIsJsonMergePatch(httpOperation) &&
this.options["stream-style-serialization"] === false) {
// do not generate convenient method for json merge patch operation if stream-style-serialization is not enabled
generateConvenienceApi = false;
diagnostic = createDiagnostic({
code: "convenience-api-not-generated",
messageId: "jsonMergePatch",
format: { operationName: operationName },
target: sdkMethod.__raw ?? NoTarget,
});
this.program.reportDiagnostic(diagnostic);
}
}
if (generateConvenienceApi && convenienceApiName) {
codeModelOperation.convenienceApi = new ConvenienceApi(convenienceApiName);
}
if (diagnostic) {
codeModelOperation.language.java = new Language();
codeModelOperation.language.java.comment = diagnostic.message;
}
// check for generating protocol api or not
codeModelOperation.generateProtocolApi = generateProtocolApi && !codeModelOperation.internalApi;
codeModelOperation.addRequest(new Request({
protocol: {
http: {
path: httpOperation.path,
method: httpOperation.verb,
uri: clientContext.baseUri,
},
},
}));
// host
clientContext.hostParameters.forEach((it) => codeModelOperation.addParameter(it));
// path/query/header parameters
for (const param of httpOperation.parameters) {
if (param.kind === "cookie") {
// ignore cookie parameter
continue;
}
// if it's paged operation with request body, skip content-type header added by TCGC, as next link call should not have content type header
if ((sdkMethod.kind === "paging" || sdkMethod.kind === "lropaging") &&
httpOperation.bodyParam &&
param.kind === "header") {
if (param.serializedName.toLocaleLowerCase() === CONTENT_TYPE_KEY) {
continue;
}
}
// if the request body is optional, skip content-type header added by TCGC
// TODO: add optional content type to code-model, and support optional content-type from codegen, https://github.com/Azure/autorest.java/issues/2930
if (httpOperation.bodyParam && httpOperation.bodyParam.optional) {
if (param.serializedName.toLocaleLowerCase() === CONTENT_TYPE_KEY) {
continue;
}
}
this.processParameter(codeModelOperation, param, clientContext);
}
// body
let bodyParameterFlattened = false;
if (httpOperation.bodyParam && httpOperation.__raw && httpOperation.bodyParam.type.__raw) {
bodyParameterFlattened = this.processParameterBody(codeModelOperation, sdkMethod, httpOperation.bodyParam);
}
if (generateConvenienceApi) {
this.processParameterGrouping(codeModelOperation, sdkMethod, bodyParameterFlattened);
}
// lro metadata
let lroMetadata = new LongRunningMetadata(false);
if (sdkMethod.kind === "lro" || sdkMethod.kind === "lropaging") {
lroMetadata = this.processLroMetadata(codeModelOperation, sdkMethod);
}
// responses
for (const response of sdkMethod.operation.responses) {
this.processResponse(codeModelOperation, response.statusCodes, response, lroMetadata.longRunning, false);
}
// exception
for (const response of sdkMethod.operation.exceptions) {
this.processResponse(codeModelOperation, response.statusCodes, response, lroMetadata.longRunning, true);
}
// check for paged
this.processRouteForPaged(codeModelOperation, sdkMethod);
// check for long-running operation
this.processRouteForLongRunning(codeModelOperation, lroMetadata);
return codeModelOperation;
}
processRouteForPaged(op, sdkMethod) {
if (sdkMethod.kind !== "paging" && sdkMethod.kind !== "lropaging") {
return;
}
// TCGC should already verified that there is 1 response, and response body is a model
const responses = sdkMethod.operation.responses;
if (responses.length === 0) {
return;
}
const response = responses[0];
const bodyType = response.type;
if (!bodyType || bodyType.kind !== "model") {
return;
}
op.responses?.forEach((r) => {
if (r instanceof SchemaResponse) {
this.trackSchemaUsage(r.schema, { usage: [SchemaContext.Paged] });
}
});
// pageItems
const pageItemsResponseProperty = findResponsePropertySegments(op, sdkMethod.response.resultSegments);
// "sdkMethod.response.resultSegments" should not be empty for "paging"/"lropaging"
// "itemSerializedName" take 1st property for backward compatibility
const itemSerializedName = pageItemsResponseProperty && pageItemsResponseProperty.length > 0
? pageItemsResponseProperty[0].serializedName
: undefined;
if (this.isAzureV1() &&
(pageItemsResponseProperty === undefined || pageItemsResponseProperty.length > 1)) {
// TCGC should have verified that pageItems exists
// Azure V1 does not support nested page items
reportDiagnostic(this.program, {
code: "nested-page-items-not-supported",
target: sdkMethod.response.resultSegments?.[sdkMethod.response.resultSegments.length - 1]
?.__raw ?? NoTarget,
});
return;
}
// nextLink
// TODO: nextLink can also be a response header, similar to "sdkMethod.pagingMetadata.continuationTokenResponseSegments"
const nextLinkResponseProperty = findResponsePropertySegments(op, sdkMethod.pagingMetadata.nextLinkSegments);
// "nextLinkSerializedName" take 1st property for backward compatibility
const nextLinkSerializedName = nextLinkResponseProperty && nextLinkResponseProperty.length > 0
? nextLinkResponseProperty[0].serializedName
: undefined;
// continuationToken
let continuationTokenParameter;
let continuationTokenResponseProperty;
let continuationTokenResponseHeader;
if (!this.isAzureV1()) {
// parameter would either be query or header parameter, so taking the last segment would be enough
const continuationTokenParameterSegment = sdkMethod.pagingMetadata.continuationTokenParameterSegments?.at(-1);
// response could be response header, where the last segment would do; or it be json path in the response body, where we use "findResponsePropertySegments" to find them
const continuationTokenResponseSegment = sdkMethod.pagingMetadata.continuationTokenResponseSegments?.at(-1);
if (continuationTokenParameterSegment && op.parameters) {
// for now, continuationToken is either request query or header parameter
const parameter = getHttpOperationParameter(sdkMethod, continuationTokenParameterSegment);
if (parameter) {
for (const param of op.parameters) {
if (param.protocol.http?.in === parameter.kind) {
if (parameter.kind === "header" &&
param.language.default.serializedName.toLowerCase() ===
parameter.serializedName.toLowerCase()) {
continuationTokenParameter = param;
break;
}
else if (parameter.kind === "query" &&
param.language.default.serializedName === parameter.serializedName) {
continuationTokenParameter = param;
break;
}
}
}
}
}
if (continuationTokenResponseSegment && op.responses) {
if (continuationTokenResponseSegment?.kind === "responseheader") {
// continuationToken is response header
for (const response of op.responses) {
if (response instanceof SchemaResponse && response.protocol.http) {
for (const header of response.protocol.http.headers) {
if (header.header.toLowerCase() ===
continuationTokenResponseSegment.serializedName.toLowerCase()) {
continuationTokenResponseHeader = header;
break;
}
}
}
if (continuationTokenResponseHeader) {
break;
}
}
}
else if (continuationTokenResponseSegment?.kind === "property") {
// continuationToken is response body property
continuationTokenResponseProperty = findResponsePropertySegments(op, sdkMethod.pagingMetadata.continuationTokenResponseSegments);
}
}
}
// nextLinkReInjectedParameters
let nextLinkReInjectedParameters;
if (this.isBranded()) {
// nextLinkReInjectedParameters is only supported in Azure
if (sdkMethod.pagingMetadata.nextLinkReInjectedParametersSegments &&
sdkMethod.pagingMetadata.nextLinkReInjectedParametersSegments.length > 0) {
nextLinkReInjectedParameters = [];
for (const parameterSegments of sdkMethod.pagingMetadata
.nextLinkReInjectedParametersSegments) {
const nextLinkReInjectedParameterSegment = parameterSegments?.at(-1);
if (nextLinkReInjectedParameterSegment && op.parameters) {
const parameter = getHttpOperationParameter(sdkMethod, nextLinkReInjectedParameterSegment);
if (parameter) {
// find the corresponding parameter in the code model operation
for (const opParam of op.parameters) {
if (opParam.protocol.http?.in === parameter.kind &&
opParam.language.default.serializedName ===
(parameter.kind === "property"
? getPropertySerializedName(parameter)
: parameter.serializedName)) {
nextLinkReInjectedParameters.push(opParam);
break;
}
}
}
}
}
}
}
op.extensions = op.extensions ?? {};
op.extensions["x-ms-pageable"] = {
// this part need to be compatible with modelerfour
itemName: itemSerializedName,
nextLinkName: nextLinkSerializedName,
// this part is only available in TypeSpec
pageItemsProperty: pageItemsResponseProperty,
nextLinkProperty: nextLinkResponseProperty,
continuationToken: continuationTokenParameter
? new PageableContinuationToken(continuationTokenParameter, continuationTokenResponseProperty, continuationTokenResponseHeader)
: undefined,
nextLinkReInjectedParameters: nextLinkReInjectedParameters,
nextLinkVerb: sdkMethod.pagingMetadata.nextLinkVerb ?? "GET",
};
}
processLroMetadata(op, sdkMethod) {
const trackConvenienceApi = Boolean(op.convenienceApi);
const lroMetadata = sdkMethod.lroMetadata;
if (lroMetadata && lroMetadata.pollingStep) {
let pollingSchema = undefined;
let finalSchema = undefined;
let pollingStrategy = undefined;
let finalResultPropertySerializedName = undefined;
const verb = sdkMethod.operation.verb;
const useNewPollStrategy = isLroNewPollingStrategy(sdkMethod.operation, lroMetadata);
if (useNewPollStrategy) {
// use OperationLocationPollingStrategy
pollingStrategy = new Metadata({
language: {
java: {
name: "OperationLocationPollingStrategy",
namespace: this.baseJavaNamespace + ".implementation",
},
},
});
}
// pollingSchema
if (lroMetadata.pollingStep.responseBody &&
modelIs(lroMetadata.pollingStep.responseBody, "OperationStatus", "Azure.Core.Foundations")) {
pollingSchema = this.pollResultSchema;
}
else {
const pollType = lroMetadata.pollingStep.responseBody;
if (pollType) {
pollingSchema = this.processSchema(pollType, "pollResult");
}
}
// finalSchema
if (verb !== "delete" &&
lroMetadata.finalResponse &&
lroMetadata.finalResponse.result &&
lroMetadata.finalResponse.envelopeResult) {
const finalResult = useNewPollStrategy
? lroMetadata.finalResponse.result
: lroMetadata.finalResponse.envelopeResult;
finalSchema = this.processSchema(finalResult, "finalResult");
if (useNewPollStrategy &&
lroMetadata.finalStep &&
lroMetadata.finalStep.kind === "pollingSuccessProperty" &&
lroMetadata.finalResponse.resultSegments) {
// TODO: in future the property could be nested, so that the "resultSegments" would contain more than 1 element
const lastSegment = lroMetadata.finalResponse.resultSegments[lroMetadata.finalResponse.resultSegments.length - 1];
finalResultPropertySerializedName = getPropertySerializedName(lastSegment);
}
}
// track usage
if (pollingSchema) {
this.trackSchemaUsage(pollingSchema, { usage: [SchemaContext.Output] });
if (trackConvenienceApi) {
this.trackSchemaUsage(pollingSchema, {
usage: [op.internalApi ? SchemaContext.Internal : SchemaContext.Public],
});
}
}
if (finalSchema) {
this.trackSchemaUsage(finalSchema, { usage: [SchemaContext.Output] });
if (trackConvenienceApi) {
this.trackSchemaUsage(finalSchema, {
usage: [op.internalApi ? SchemaContext.Internal : SchemaContext.Public],
});
}
}
op.lroMetadata = new LongRunningMetadata(true, pollingSchema, finalSchema, pollingStrategy, finalResultPropertySerializedName);
return op.lroMetadata;
}
return new LongRunningMetadata(false);
}
processRouteForLongRunning(op, lroMetadata) {
if (lroMetadata.longRunning) {
op.extensions = op.extensions ?? {};
op.extensions["x-ms-long-running-operation"] = true;
return;
}
}
processParameter(op, param, clientContext) {
if (clientContext.apiVersions && this.isApiVersionParameter(param) && param.kind !== "cookie") {
// pre-condition for "isApiVersion": the client supports ApiVersions
if (this.isArm()) {
// Currently we assume ARM tsp only have one client and one api-version.
// TODO: How will service define mixed api-versions(like those in Compute RP)?
const apiVersion = this.apiVersion;
if (!this._armApiVersionParameter) {
this._armApiVersionParameter = this.createApiVersionParameter("api-version", param.kind === "query" ? ParameterLocation.Query : ParameterLocation.Path, apiVersion);
clientContext.addGlobalParameter(this._armApiVersionParameter);
}
op.addParameter(this._armApiVersionParameter);
}
else {
const parameter = this.getApiVersionParameter(param);
op.addParameter(parameter);
clientContext.addGlobalParameter(parameter);
}
}
else if (param.kind === "path" && param.onClient && this.isSubscriptionId(param)) {
const parameter = this.subscriptionIdParameter(param);
op.addParameter(parameter);
clientContext.addGlobalParameter(parameter);
}
else if (param.kind === "header" &&
SPECIAL_HEADER_NAMES.has(param.serializedName.toLowerCase())) {
// special headers
op.specialHeaders = op.specialHeaders ?? [];
if (!stringArrayContainsIgnoreCase(op.specialHeaders, param.serializedName)) {
op.specialHeaders.push(param.serializedName);
}
}
else {
// schema
const sdkType = getNonNullSdkType(param.type);
const schema = this.processSchema(sdkType, param.name);
let extensions = undefined;
if (param.kind === "path") {
if (param.allowReserved) {
extensions = extensions ?? {};
extensions["x-ms-skip-url-encoding"] = true;