UNPKG

@typespec/http-client-java

Version:

TypeSpec library for emitting Java client from the TypeSpec REST protocol binding

976 lines (975 loc) 128 kB
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;