This document is the remaining case study on TCGC usage and access adoption, it does not include the spread cases as it already covers in another gist.
we will not track usage for response header schema, see code. thus we will not generate the model. But TCGC return the model with access public, so we will generate the model after adopting TCGC which is incorrect.
Test case:
op LroLongRunningRpcOperation<
TParams extends TypeSpec.Reflection.Model,
TResponse extends TypeSpec.Reflection.Model,
Traits extends {} = {}
> is Azure.Core.RpcOperation<
TParams & RepeatabilityRequestHeaders,
Foundations.AcceptedResponse<LroLongRunningStatusLocation<TResponse> &
Foundations.RetryAfterHeader> &
RepeatabilityResponseHeaders &
LroOperationStatus,
Traits
>;
@doc("Provides the 'Repeatability-*' headers to enable repeatable requests.")
model RepeatabilityResponseHeaders {
@visibility("read")
@header("Repeatability-Result")
@doc("Indicates whether the repeatable request was accepted or rejected.")
repeatabilityResult?: RepeatabilityResult;
}In above case, RepeatabilityResult will be generated after adopting TCGC.
we will not track usage for response model of LRO, see code Because we will track usage during process LRO Metadata. But TCGC will return the model as public access. So the model will be generated after adopting TCGC. Test case:
model LroOperationStatus<TStatusResult = never, TStatusError = Foundations.Error> {
@key("operationId")
@visibility("read")
id: Azure.Core.uuid;
@visibility("read")
@lroStatus
status: JobStatus;
@visibility("read")
createdDateTime?: utcDateTime;
@visibility("read")
expirationDateTime?: utcDateTime;
@visibility("read")
lastUpdateDateTime?: utcDateTime;
error?: TStatusError;
result?: TStatusResult;
}
op LroLongRunningRpcOperation<
TParams extends TypeSpec.Reflection.Model,
TResponse extends TypeSpec.Reflection.Model,
Traits extends {} = {}
> is Azure.Core.RpcOperation<
TParams & RepeatabilityRequestHeaders,
Foundations.AcceptedResponse<LroLongRunningStatusLocation<TResponse> &
Foundations.RetryAfterHeader> &
RepeatabilityResponseHeaders &
LroOperationStatus,
Traits
>;
@route("/jobs/{id}")
op getJob is LroLongRunningPollOperation<JobResult>;
@pollingOperation(LongRunning.getJob)
@route("/jobs")
op createJob is LroLongRunningRpcOperation<
{
@body body: JobData;
},
JobResult
>;LroOperationStatus model is LRO initial response, and should not be generated. But TCGC will return the model with name LroOperationStatusError and public access, thus after adopting TCGC's access, this model will be generated, which is incorrect.
similar case:
op delete is ResourceOperations.LongRunningResourceDelete<User>;
/**
* Provides status details for long running operations.
* @template StatusResult The type of the operation status result.
* @template StatusError The type of the operation status error. If not provided, the default error is used.
*/
@doc("Provides status details for long running operations.")
model OperationStatus<StatusResult = never, StatusError = Foundations.Error> {
@key("operationId")
@doc("The unique ID of the operation.")
id: string;
@doc("The status of the operation")
status: OperationState;
@doc("Error object that describes the error when status is \"Failed\".")
error?: StatusError;
@doc("The result of the operation.")
result?: StatusResult;
}
/**
* Long-running resource delete operation template.
* @template Resource Resource type.
* @template Traits Object describing the traits of the operation.
*/
#suppress "@azure-tools/typespec-azure-resource-manager/no-response-body" "We do support bodies in data plane service APIs..."
@Foundations.Private.ensureVerb("LongRunningResourceDelete", "DELETE")
@deletesResource(Resource)
LongRunningResourceDelete<
Resource extends TypeSpec.Reflection.Model,
Traits extends TypeSpec.Reflection.Model = {}
> is Foundations.ResourceOperation<
Resource,
TraitProperties<Traits & InterfaceTraits, TraitLocation.Parameters, TraitContext.Delete>,
Foundations.AcceptedResponse<Foundations.OperationStatus &
Foundations.LongRunningStatusLocation &
TraitProperties<Traits & InterfaceTraits, TraitLocation.Response, TraitContext.Delete>>,
Traits & InterfaceTraits,
ErrorResponse
>;
In above example, OperationStatusError and OperationState will be generated.
model Model {
literal: "literal";
optionalLiteral?: "optionalLiteral";
}
alias Request = {
@query
literalParam: "literalParam";
@query
optionalLiteralParam?: "optionalLiteralParam";
};
@route("/literal")
interface LiteralOp {
@put
@route("/put")
put(...Request, @body body: Model): Model;
}ModelOptionalLiteral is just a constant in TCGC which has no usage. But we will generate it as enum, so we need usage. When there is no usage, we will not generate the model, so after adopting TCGC, ModelOptionalLiteral is deleted.
model Builtin {
boolean: boolean;
string: string;
bytes: bytes;
int: int32;
safeint: safeint;
decimal: decimal;
long: int64;
float: float32;
double: float64;
duration: duration;
date: plainDate;
dateTime: utcDateTime;
stringList: string[];
bytesDict: Record<bytes>;
url: url;
nullableFloatDict: Record<float64 | null>;
encoded: Encoded;
}
@encode(DurationKnownEncoding.seconds, float32)
scalar myDuration extends duration;
model Encoded {
@encode(DurationKnownEncoding.seconds, int32)
timeInSeconds?: duration;
timeInSecondsFraction?: myDuration;
@encode(DateTimeKnownEncoding.rfc3339)
dateTime?: utcDateTime;
@encode(DateTimeKnownEncoding.rfc7231)
dateTimeRfc7231?: utcDateTime;
@encode(DateTimeKnownEncoding.unixTimestamp, int64)
unixTimestamp?: utcDateTime;
@encode(BytesKnownEncoding.base64)
base64?: bytes;
@encode(BytesKnownEncoding.base64url)
base64url?: bytes;
}
model Request {
@header("x-ms-date")
dateTime?: utcDateTime;
@query
filter?: string;
@query("query")
queryParam: string;
@query("query-encoded")
queryParamEncoded: url;
@query("query-opt")
queryParamOptional?: string;
@query("query-opt-encoded")
queryParamOptionalEncoded?: url;
}
@route("/builtin")
interface BuiltinOp {
read(...Request): {
@body body: Builtin;
};
@convenientAPI(false)
write(@body body: Builtin): OkResponse;
}Builtin and Encoded model are only used in output, seems TCGC is correct.
@usage(Usage.input) // tsp definition error?
model Operation {
name: "Read" | "Write";
best: true;
age: 50;
priority: Priority;
color: ColorModel;
unit: Unit;
priorityValue: Priority.Low;
colorValue: Color.Green;
colorModelValue: ColorModel.Blue;
unitValue?: Unit.Milligram;
}
enum OperationStateValues {
Running,
Completed,
Failed,
}
@route("/enum")
interface EnumOp {
@get
@route("operation/state")
getOperation(@query state: OperationStateValues): Operation;
}
Operation is only used in output, and the model is explicitly marked as @usage(Usage.input), but TCGC will return the model with output only.
// ProtocolInternalModel will not be generated, as the model is not used in convenient API
model ProtocolInternalModel {
name: string;
}
@route("/internal")
interface InternalOp {
// test ApiRequest with Access.public
@access(Access.public, "python")
@access(Access.internal, "client")
@post
postInternal(@body body: ApiRequest): ResponseInternal;
// test ApiResponse with Access.public
@get
@access(Access.internal, "java")
getInternal(): ApiResponse;
@route("/protocal-internal")
@post
@access(Access.internal)
@convenientAPI(false)
postProtocalInternal(@body body: ProtocolInternalModel): void;
}ProtocolInternalModel is only used in protocol method, but as TCGC return access as internal with usageFlags === 0, we will generate it.
@route("multiple/sharedroute/request")
interface MultipleContentTypesOnRequest {
@doc("one data type maps to multiple content types")
@route("upload/single-body-type")
@post
uploadBytesWithSingleBodyTypeForMultiContentTypes(
@body data: bytes,
@header contentType:
| "application/octet-stream"
| "image/jpeg"
| "image/png"
| "application/json-patch+json",
): void;
}We will treat multiple content types case as convenientApi === false, and the request body model/response body model/header enum will not be generated. e.g. for above case, UploadBytesWithSingleBodyTypeForMultiContentTypesRequestContentType enum from TCGC has public access, it will be generated. (If request body and response body are model, they will also be generated). For this case, we will need to override TCGC's usage and access.
@server(
"{endpoint}/client/structure/{client}",
"",
{
@doc("Need to be set as 'http://localhost:3000' in client.")
endpoint: url,
@doc("Need to be set as 'default', 'multi-client', 'renamed-operation', 'two-operation-group' in client.")
client: ClientType,
}
)
@service({
title: "MultiClient",
})
namespace Client.Structure.Service;
enum ClientType {
Default: "default",
MultiClient: "multi-client",
RenamedOperation: "renamed-operation",
TwoOperationGroup: "two-operation-group",
}ClientType has public access and usage, so it will get generated after adopting TCGC.
/**
* Test for the `@added` decorator.
*/
@service
@versioned(Versions)
@server(
"{endpoint}/versioning/added/api-version:{version}",
"Testserver endpoint",
{
/**
* Need to be set as 'http://localhost:3000' in client.
*/
endpoint: url,
/**
* Need to be set as 'v1' or 'v2' in client.
*/
version: Versions,
}
)
namespace Versioning.Added;
/**
* The version of the API.
*/
enum Versions {
/**
* The version v1.
*/
v1: "v1",
/**
* The version v2.
*/
v2: "v2",
}
In above case, Versions will be generated. We can stop tracking TCGC's usage and access if usage is ApiVersionEnum.
When a model is only used in protocol method's response body. TCGC returns the model usage as 256, not 0.
For example, for below case, ResourceD is only used in protocol method's response body, but its usage is 256. While ResourceC which is only used in protocol method's request body, its usage is 0. I think ResourceC's usage is correct, but ResourceD's usage is wrong.
@doc("When set protocol true and convenient false, only the protocol method should be generated, ResourceC and ResourceD should not be generated")
@route("onlyProtocol")
@post
@convenientAPI(false)
@protocolAPI(true)
onlyProtocol(@body body: ResourceC): ResourceD;detailed test case
add the logic to not track schema's usage if TCGC has usageFlags === 0
special handling for ApiVersionEnum
See if we want to use enum or still string. -> update in 2024/10/17 meeting: we will use enum
change the tsp definition. This definition is not allowed.
we will to override TCGC's usage and access.
- Option1: Modify client.tsp. Add
@convenientAPI(false)in client.tsp for the operation.
@@convenientAPI(MultipleContentTypesOp, false);- Option2: Handle from emitter side
This way is not workable.
A model used by multiple content types operation can also be used by other operations. So once TCGC returns the usage and access for a specific model, it combines the usage and access of the model from different operations. And emitter cannot remove the usage and access from multiple-content-types operation only.
e.g. If a model is used by a public multiple-content-types operation and internal non-multiple-content-types operation. TCGC returns the access as public, but actually the model's access is internal from java side, but we do not know unless we calculate access for the model again, but this means we did not benefit from TCGC's access calculation. Same as usage.
- Option3: Handle from TCGC side
TCGC provides an after-hook callback which gets called after TCGC processing every operation when createSdkContext. And we can set the operation's usage to 0 and access to internal if the operation is multiple-content-type operation. Code snippet as below:
this.sdkContext = await createSdkContext(this.emitterContext, "@typespec/http-client-java", (sdkMethod, bodyType, parameters) => {
if (isMultipleContentTypeOperation(sdkMethod)) {
if (bodyType.kind === "model" || bodyType.kind === "enum") {
bodyType.access = "internal"
bodyType.usage = 0;
}
for (let parameter of parameters) {
if (parameter.type.kind === "model" || parameter.type.kind === "enum") {
parameter.type.access = "internal";
parameter.type.usage = 0;
}
}
}
});Or TCGC provides a before-hook which gets called before TCGC processing every operation when createSdkContext. And we can set the operation's generateConvenientApi to false. Code snippet as below:
this.sdkContext = await createSdkContext(this.emitterContext, "@typespec/http-client-java", (sdkMethod, bodyType, parameters) => {
if (isMultipleContentTypeOperation(sdkMethod)) {
sdkMethod.generateConvenientApi = false;
}
});Update from 2024.1.24 meeting: we would prefer option1, and provide an error message that let user to add specific configuration to client.tsp when processing multiple-content-type operation.
- question to TCGC: polling schema different from final response schema and initial response schema, will polling schema have public access?
- case 1: request/response header schema should not be generated Azure/typespec-azure#1749
- case 2: Needs TCGC to review LRO related schema's usage and access: Azure/typespec-azure#1750
- case 3: prefer to let TCGC add usage and access on constant. Azure/typespec-azure#1747
- case 10: it is a TCGC bug, Azure/typespec-azure#1748
- Azure/typespec-azure#1752