Skip to content

Commit

Permalink
tsp, Union imply protocol API (#2249)
Browse files Browse the repository at this point in the history
  • Loading branch information
weidongxu-microsoft authored Jul 21, 2023
1 parent 4db1684 commit a49467d
Show file tree
Hide file tree
Showing 40 changed files with 547 additions and 1,372 deletions.
6 changes: 6 additions & 0 deletions typespec-extension/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Release History

## 0.8.6 (2023-07-21)

Compatible with compiler 0.46.

- Operation which refers `Union` type is treated as protocol API, i.e. with `convenientAPI(false)`.

## 0.8.6 (2023-07-18)

Compatible with compiler 0.46.
Expand Down
2 changes: 1 addition & 1 deletion typespec-extension/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@azure-tools/typespec-java",
"version": "0.8.7",
"version": "0.8.8",
"description": "TypeSpec library for emitting Java client from the TypeSpec REST protocol binding",
"keywords": [
"TypeSpec"
Expand Down
15 changes: 11 additions & 4 deletions typespec-extension/src/code-model-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,15 @@ import {
import {
getClientApiVersions,
getServiceVersion,
operationContainsJsonMergePatch,
operationIsJsonMergePatch,
isPayloadProperty,
originApiVersion,
specialHeaderNames,
loadExamples,
isLroNewPollingStrategy,
operationIsMultipleContentTypes,
cloneOperationParameter,
operationRefersUnion,
} from "./operation-utils.js";
import pkg from "lodash";
const { isEqual } = pkg;
Expand All @@ -169,7 +170,7 @@ export class CodeModelBuilder {
private codeModel: CodeModel;

readonly schemaCache = new ProcessingCache((type: Type, name: string) => this.processSchemaImpl(type, name));
readonly operationCache = new Map<Operation, CodeModelOperation>();
readonly typeUnionRefCache = new Map<Type, Union | null | undefined>(); // Union means it ref a Union type, null means it does not ref any Union, nndefined means type visited but not completed

private operationExamples: Map<Operation, any> = new Map<Operation, any>();

Expand Down Expand Up @@ -515,13 +516,19 @@ export class CodeModelBuilder {
},
});

if (operationContainsJsonMergePatch(op)) {
if (operationIsJsonMergePatch(op)) {
// do not generate convenience method for JSON Merge Patch
this.trace(`Operation '${op.operation.name}' contains 'application/merge-patch+json'`);
this.trace(`Operation '${op.operation.name}' is 'application/merge-patch+json'`);
} else if (operationIsMultipleContentTypes(op)) {
// and multiple content types
// issue link: https://github.com/Azure/autorest.java/issues/1958#issuecomment-1562558219
this.trace(`Operation '${op.operation.name}' is multiple content-type`);
} else if (
operationRefersUnion(this.program, op, this.typeUnionRefCache, (it: Type) => {
return getTypeName(it, this.typeNameOptions);
})
) {
// and Union
} else {
const convenienceApiName = this.getConvenienceApiName(operation);
if (convenienceApiName && !isInternal(this.sdkContext, operation)) {
Expand Down
67 changes: 61 additions & 6 deletions typespec-extension/src/operation-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ModelProperty, Operation, Program, ignoreDiagnostics, resolvePath } from "@typespec/compiler";
import { ModelProperty, Operation, Program, Type, Union, ignoreDiagnostics, resolvePath } from "@typespec/compiler";
import {
HttpOperation,
getHeaderFieldName,
Expand All @@ -14,7 +14,8 @@ import { Client as CodeModelClient, ServiceVersion } from "./common/client.js";
import { CodeModel } from "./common/code-model.js";
import { EmitterOptions } from "./emitter.js";
import { getVersion } from "@typespec/versioning";
import { getNamespace, logWarning, pascalCase } from "./utils.js";
import { getNamespace, logWarning, pascalCase, trace } from "./utils.js";
import { unionReferedByType } from "./type-utils.js";

export const specialHeaderNames = new Set([
"repeatability-request-id",
Expand Down Expand Up @@ -86,7 +87,7 @@ function pascalCaseForOperationId(name: string) {
.join("_");
}

export function operationContainsJsonMergePatch(op: HttpOperation): boolean {
export function operationIsJsonMergePatch(op: HttpOperation): boolean {
for (const param of op.parameters.parameters) {
if (param.type === "header" && param.name.toLowerCase() === "content-type") {
if (param.param.type.kind === "String" && param.param.type.value === "application/merge-patch+json") {
Expand All @@ -97,10 +98,10 @@ export function operationContainsJsonMergePatch(op: HttpOperation): boolean {
return false;
}

export function operationIsMultipleContentTypes(httpOperation: HttpOperation): boolean {
export function operationIsMultipleContentTypes(op: HttpOperation): boolean {
if (
httpOperation.parameters.parameters &&
httpOperation.parameters.parameters.some(
op.parameters.parameters &&
op.parameters.parameters.some(
(parameter) =>
parameter?.type === "header" &&
parameter?.name?.toLowerCase() === "content-type" &&
Expand All @@ -112,6 +113,60 @@ export function operationIsMultipleContentTypes(httpOperation: HttpOperation): b
return false;
}

export function operationRefersUnion(
program: Program,
op: HttpOperation,
cache: Map<Type, Union | null | undefined>,
getTypeName: (type: Type) => string,
): boolean {
// request parameters
for (const parameter of op.parameters.parameters) {
const ret = unionReferedByType(program, parameter.param.type, cache);
if (ret) {
trace(program, `Operation '${op.operation.name}' refers Union '${getUnionName(ret, getTypeName)}'`);
return true;
}
}
// request body
if (op.parameters.body) {
if (op.parameters.body.parameter) {
const ret = unionReferedByType(program, op.parameters.body.parameter.type, cache);
if (ret) {
trace(program, `Operation '${op.operation.name}' refers Union '${getUnionName(ret, getTypeName)}'`);
return true;
}
} else if (op.parameters.body.type) {
const ret = unionReferedByType(program, op.parameters.body.type, cache);
if (ret) {
trace(program, `Operation '${op.operation.name}' refers Union '${getUnionName(ret, getTypeName)}'`);
return true;
}
}
}
// response body
if (op.responses && op.responses.length > 0 && op.responses[0].type) {
const ret = unionReferedByType(program, op.responses[0].type, cache);
if (ret) {
trace(program, `Operation '${op.operation.name}' refers Union '${getUnionName(ret, getTypeName)}'`);
return true;
}
}
// TODO (weidxu): LRO response
return false;
}

function getUnionName(union: Union, getTypeName: (type: Type) => string) {
let name = union.name;
if (!name) {
const names: string[] = [];
union.variants.forEach((it) => {
names.push(getTypeName(it.type));
});
name = names.join(" | ");
}
return name;
}

export function isPayloadProperty(program: Program, property: ModelProperty) {
const headerInfo = getHeaderFieldName(program, property);
const queryInfo = getQueryParamName(program, property);
Expand Down
60 changes: 60 additions & 0 deletions typespec-extension/src/type-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import {
EncodeData,
IntrinsicScalarName,
Model,
Program,
Scalar,
TemplatedTypeBase,
Type,
Union,
UnionVariant,
isNullType,
isTemplateDeclaration,
Expand Down Expand Up @@ -130,3 +132,61 @@ export function hasScalarAsBase(type: Scalar, scalarName: IntrinsicScalarName):
}
return false;
}

export function unionReferedByType(
program: Program,
type: Type,
cache: Map<Type, Union | null | undefined>,
): Union | null {
if (cache.has(type)) {
const ret = cache.get(type);
if (ret) {
return ret;
} else {
return null;
}
}
cache.set(type, undefined);

if (type.kind === "Union") {
// ref CodeModelBuilder.processUnionSchema
const nonNullVariants = Array.from(type.variants.values()).filter((it) => !isNullType(it.type));
if (nonNullVariants.length === 1) {
// Type | null, follow that Type
const ret = unionReferedByType(program, nonNullVariants[0], cache);
if (ret) {
cache.set(type, ret);
return ret;
}
} else if (isSameLiteralTypes(nonNullVariants)) {
// "literal1" | "literal2" -> Enum
cache.set(type, null);
return null;
} else {
// found Union
cache.set(type, type);
return type;
}
} else if (type.kind === "Model") {
if (type.indexer) {
// follow indexer (for Array/Record)
const ret = unionReferedByType(program, type.indexer.value, cache);
if (ret) {
cache.set(type, ret);
return ret;
}
}
// follow properties
for (const property of type.properties.values()) {
const ret = unionReferedByType(program, property.type, cache);
if (ret) {
cache.set(type, ret);
return ret;
}
}
cache.set(type, null);
return null;
}
cache.set(type, null);
return null;
}
2 changes: 1 addition & 1 deletion typespec-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@typespec/openapi": ">=0.45.0 <1.0.0",
"@azure-tools/typespec-autorest": ">=0.31.0 <1.0.0",
"@azure-tools/cadl-ranch-specs": "0.17.1",
"@azure-tools/typespec-java": "file:/../typespec-extension/azure-tools-typespec-java-0.8.7.tgz"
"@azure-tools/typespec-java": "file:/../typespec-extension/azure-tools-typespec-java-0.8.8.tgz"
},
"devDependencies": {
"@typespec/prettier-plugin-typespec": ">=0.45.0 <1.0.0",
Expand Down
105 changes: 38 additions & 67 deletions typespec-tests/src/main/java/com/cadl/union/UnionAsyncClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,8 @@
import com.azure.core.http.rest.RequestOptions;
import com.azure.core.http.rest.Response;
import com.azure.core.util.BinaryData;
import com.azure.core.util.FluxUtil;
import com.azure.core.util.polling.PollerFlux;
import com.cadl.union.implementation.UnionClientImpl;
import com.cadl.union.models.InputModelBase;
import com.cadl.union.models.SendLongOptions;
import com.cadl.union.models.User;
import java.util.HashMap;
import java.util.Map;
import reactor.core.publisher.Mono;

/** Initializes a new instance of the asynchronous UnionClient type. */
Expand Down Expand Up @@ -108,85 +103,61 @@ public Mono<Response<Void>> sendLongWithResponse(String id, BinaryData request,
}

/**
* The send operation.
* The get operation.
*
* @param id A sequence of textual characters.
* @param input The input parameter.
* @param user The user parameter.
* @throws IllegalArgumentException thrown if parameters fail the validation.
* @throws HttpResponseException thrown if the request is rejected by server.
* @throws ClientAuthenticationException thrown if the request is rejected by server on status code 401.
* @throws ResourceNotFoundException thrown if the request is rejected by server on status code 404.
* @throws ResourceModifiedException thrown if the request is rejected by server on status code 409.
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent.
* @return A {@link Mono} that completes when a successful response is received.
*/
@Generated
@ServiceMethod(returns = ReturnType.SINGLE)
public Mono<Void> send(String id, InputModelBase input, User user) {
// Generated convenience method for sendWithResponse
RequestOptions requestOptions = new RequestOptions();
Map<String, Object> requestObj = new HashMap<>();
requestObj.put("user", user);
requestObj.put("input", input);
BinaryData request = BinaryData.fromObject(requestObj);
return sendWithResponse(id, request, requestOptions).flatMap(FluxUtil::toMono);
}

/**
* The send operation.
* <p><strong>Query Parameters</strong>
*
* @param id A sequence of textual characters.
* @param input The input parameter.
* @throws IllegalArgumentException thrown if parameters fail the validation.
* <table border="1">
* <caption>Query Parameters</caption>
* <tr><th>Name</th><th>Type</th><th>Required</th><th>Description</th></tr>
* <tr><td>data</td><td>DataModelBase</td><td>No</td><td>The data parameter</td></tr>
* </table>
*
* You can add these to a request with {@link RequestOptions#addQueryParam}
*
* @param requestOptions The options to configure the HTTP request before HTTP client sends it.
* @throws HttpResponseException thrown if the request is rejected by server.
* @throws ClientAuthenticationException thrown if the request is rejected by server on status code 401.
* @throws ResourceNotFoundException thrown if the request is rejected by server on status code 404.
* @throws ResourceModifiedException thrown if the request is rejected by server on status code 409.
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent.
* @return A {@link Mono} that completes when a successful response is received.
* @return the {@link Response} on successful completion of {@link Mono}.
*/
@Generated
@ServiceMethod(returns = ReturnType.SINGLE)
public Mono<Void> send(String id, InputModelBase input) {
// Generated convenience method for sendWithResponse
RequestOptions requestOptions = new RequestOptions();
Map<String, Object> requestObj = new HashMap<>();
requestObj.put("input", input);
BinaryData request = BinaryData.fromObject(requestObj);
return sendWithResponse(id, request, requestOptions).flatMap(FluxUtil::toMono);
public Mono<Response<Void>> getWithResponse(RequestOptions requestOptions) {
return this.serviceClient.getWithResponseAsync(requestOptions);
}

/**
* The sendLong operation.
* A long-running remote procedure call (RPC) operation.
*
* <p><strong>Response Body Schema</strong>
*
* @param options Options for sendLong API.
* @throws IllegalArgumentException thrown if parameters fail the validation.
* <pre>{@code
* {
* id: String (Required)
* status: String (Required)
* error (Optional): {
* code: String (Required)
* message: String (Required)
* target: String (Optional)
* details (Optional): [
* (recursive schema, see above)
* ]
* }
* }
* }</pre>
*
* @param requestOptions The options to configure the HTTP request before HTTP client sends it.
* @throws HttpResponseException thrown if the request is rejected by server.
* @throws ClientAuthenticationException thrown if the request is rejected by server on status code 401.
* @throws ResourceNotFoundException thrown if the request is rejected by server on status code 404.
* @throws ResourceModifiedException thrown if the request is rejected by server on status code 409.
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent.
* @return A {@link Mono} that completes when a successful response is received.
* @return the {@link PollerFlux} for polling of status details for long running operations.
*/
@Generated
@ServiceMethod(returns = ReturnType.SINGLE)
public Mono<Void> sendLong(SendLongOptions options) {
// Generated convenience method for sendLongWithResponse
RequestOptions requestOptions = new RequestOptions();
String id = options.getId();
String filter = options.getFilter();
Map<String, Object> requestObj = new HashMap<>();
requestObj.put("user", options.getUser());
requestObj.put("input", options.getInput());
requestObj.put("dataInt", options.getDataInt());
requestObj.put("dataUnion", options.getDataUnion());
requestObj.put("dataLong", options.getDataLong());
requestObj.put("data_float", options.getDataFloat());
BinaryData request = BinaryData.fromObject(requestObj);
if (filter != null) {
requestOptions.addQueryParam("filter", filter, false);
}
return sendLongWithResponse(id, request, requestOptions).flatMap(FluxUtil::toMono);
@ServiceMethod(returns = ReturnType.LONG_RUNNING_OPERATION)
public PollerFlux<BinaryData, BinaryData> beginGenerate(RequestOptions requestOptions) {
return this.serviceClient.beginGenerateAsync(requestOptions);
}
}
Loading

0 comments on commit a49467d

Please sign in to comment.