Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tcgc] new spread behavior #886

Merged
merged 35 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6e428a4
remove get effective model part
tadelesh May 21, 2024
bce5411
Merge branch 'main' of https://github.com/Azure/typespec-azure into n…
May 21, 2024
895f5fd
change some of the tests
May 21, 2024
b560333
change logic for spread
tadelesh May 23, 2024
d24a5ab
Merge branch 'main' of https://github.com/Azure/typespec-azure into n…
May 23, 2024
5d62ace
Merge branch 'new_spread' of https://github.com/Azure/typespec-azure …
May 23, 2024
3e924a3
format
May 23, 2024
098457f
add changeset
May 23, 2024
2cd77d2
Merge branch 'main' into new_spread
iscai-msft May 23, 2024
4d4333a
Merge remote-tracking branch 'origin/main' into new_spread
tadelesh May 29, 2024
8d0f327
Merge remote-tracking branch 'origin/main' into new_spread
tadelesh Jun 4, 2024
1d1b4d6
change spread logic and fix tests
tadelesh Jun 5, 2024
af7a50e
update test
tadelesh Jun 7, 2024
585d5a4
Merge remote-tracking branch 'origin/main' into new_spread
tadelesh Jun 7, 2024
e0f32bf
Merge remote-tracking branch 'origin/main' into new_spread
tadelesh Jun 12, 2024
ce45bac
Merge remote-tracking branch 'origin/main' into new_spread
tadelesh Jun 13, 2024
967f134
refine logic and fix test
tadelesh Jun 13, 2024
385b7b1
add test
tadelesh Jun 13, 2024
223e57f
Merge remote-tracking branch 'origin/main' into new_spread
tadelesh Jun 18, 2024
68c8096
fix test
tadelesh Jun 18, 2024
acc6432
Merge remote-tracking branch 'origin/main' into new_spread
tadelesh Jun 18, 2024
1529c67
fix
tadelesh Jun 18, 2024
3e563d1
Merge branch 'main' into new_spread
tadelesh Jun 19, 2024
a50a7d5
revert change for response
tadelesh Jun 19, 2024
46d6a22
format
tadelesh Jun 19, 2024
cc6bbb8
refine some tests
tadelesh Jun 19, 2024
92fa8b8
Merge remote-tracking branch 'origin/main' into new_spread
tadelesh Jun 25, 2024
139e3b9
refine logic
tadelesh Jun 25, 2024
32a9449
add comment
tadelesh Jun 25, 2024
de92375
refine logic
tadelesh Jun 25, 2024
d695968
Merge branch 'main' into new_spread
tadelesh Jun 26, 2024
fcd036f
Merge branch 'main' into new_spread
tadelesh Jun 27, 2024
0a2e7a6
refine test
tadelesh Jun 27, 2024
5d77ba3
Merge branch 'main' into new_spread
tadelesh Jul 1, 2024
c69bb3c
Merge branch 'main' into new_spread
tadelesh Jul 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .chronus/changes/new_spread-2024-4-23-14-11-6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: breaking
packages:
- "@azure-tools/typespec-client-generator-core"
---

always spread models and aliases with `...`
121 changes: 66 additions & 55 deletions packages/typespec-client-generator-core/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,17 +127,17 @@ function getSdkHttpParameters(
const correspondingMethodParams: SdkModelPropertyType[] = [];
if (tspBody && tspBody?.bodyKind !== "multipart") {
// if there's a param on the body, we can just rely on getSdkHttpParameter
if (tspBody.parameter && !isNeverOrVoidType(tspBody.parameter.type)) {
if (tspBody.property && !isNeverOrVoidType(tspBody.property.type)) {
const getParamResponse = diagnostics.pipe(
getSdkHttpParameter(context, tspBody.parameter, httpOperation.operation, "body")
getSdkHttpParameter(context, tspBody.property, httpOperation.operation, "body")
);
if (getParamResponse.kind !== "body") {
diagnostics.add(
createDiagnostic({
code: "unexpected-http-param-type",
target: tspBody.parameter,
target: tspBody.property,
format: {
paramName: tspBody.parameter.name,
paramName: tspBody.property.name,
expectedType: "body",
actualType: getParamResponse.kind,
},
Expand Down Expand Up @@ -505,62 +505,27 @@ export function getCorrespondingMethodParams(
}
return diagnostics.wrap([context.__subscriptionIdParameter]);
}
const correspondingMethodParameter = methodParameters.find((x) => x.name === serviceParam.name);
if (correspondingMethodParameter) {
return diagnostics.wrap([correspondingMethodParameter]);
}

const serviceParamType = serviceParam.type;
if (serviceParam.kind === "body" && serviceParamType.kind === "model") {
const serviceParamPropertyNames = Array.from(serviceParamType.properties.values())
.filter((x) => x.kind === "property")
.map((x) => x.name);
// Here we have a spread method parameter

// easy case is if the spread method parameter directly has the entire body as a property
const directBodyProperty = methodParameters
.map((x) => x.type)
.filter((x): x is SdkModelType => x.kind === "model")
.flatMap((x) => x.properties)
.find((x) => x.type === serviceParamType);
if (directBodyProperty) return diagnostics.wrap([directBodyProperty]);
let correspondingProperties: SdkModelPropertyType[] = methodParameters.filter((x) =>
serviceParamPropertyNames.includes(x.name)
);
for (const methodParam of methodParameters) {
const methodParamIterable =
methodParam.type.kind === "model" ? methodParam.type.properties : [methodParam];
correspondingProperties = correspondingProperties.concat(
methodParamIterable.filter(
(x) =>
serviceParamPropertyNames.includes(x.name) &&
!correspondingProperties.find((e) => e.name === x.name)
)
);
}
if (correspondingProperties.length === serviceParamPropertyNames.length)
return diagnostics.wrap(correspondingProperties);
diagnostics.add(
createDiagnostic({
code: "no-corresponding-method-param",
target: serviceParam.__raw!,
format: {
paramName: serviceParam.name,
methodName: operation.name,
},
})
);
return diagnostics.wrap([]);
// to see if the service parameter is a method parameter or a property of a method parameter
const directMapping = findMapping(methodParameters, serviceParam);
if (directMapping) {
return diagnostics.wrap([directMapping]);
}
for (const methodParam of methodParameters) {
if (methodParam.type.kind === "model") {
for (const prop of methodParam.type.properties) {
if (prop.name === serviceParam.name) {
return diagnostics.wrap([prop]);
}

// to see if all the property of service parameter could be mapped to a method parameter or a property of a method parameter
if (serviceParam.kind === "body" && serviceParam.type.kind === "model") {
const retVal = [];
for (const serviceParamProp of serviceParam.type.properties) {
const propertyMapping = findMapping(methodParameters, serviceParamProp);
if (propertyMapping) {
retVal.push(propertyMapping);
}
}
if (retVal.length === serviceParam.type.properties.length) {
return diagnostics.wrap(retVal);
}
}

diagnostics.add(
createDiagnostic({
code: "no-corresponding-method-param",
Expand All @@ -574,6 +539,52 @@ export function getCorrespondingMethodParams(
return diagnostics.wrap([]);
}

function findMapping(
methodParameters: SdkModelPropertyType[],
serviceParam: SdkHttpParameter | SdkModelPropertyType
): SdkModelPropertyType | undefined {
const queue: SdkModelPropertyType[] = [...methodParameters];
const visited: Set<SdkModelType> = new Set();
while (queue.length > 0) {
const methodParam = queue.shift()!;
// http operation parameter/body parameter/property of body parameter could either from an operation parameter directly or from a property of an operation parameter
if (
methodParam.__raw &&
serviceParam.__raw &&
(methodParam.__raw === serviceParam.__raw ||
methodParam.__raw === serviceParam.__raw.sourceProperty)
tadelesh marked this conversation as resolved.
Show resolved Hide resolved
) {
return methodParam;
}
// this following two hard code mapping is for the case that TCGC help to add content type and accept header is not exist
if (
serviceParam.kind === "header" &&
serviceParam.serializedName === "Content-Type" &&
methodParam.name === "contentType"
) {
return methodParam;
}
if (
serviceParam.kind === "header" &&
serviceParam.serializedName === "Accept" &&
methodParam.name === "accept"
) {
return methodParam;
}
weidongxu-microsoft marked this conversation as resolved.
Show resolved Hide resolved
if (methodParam.type.kind === "model" && !visited.has(methodParam.type)) {
visited.add(methodParam.type);
let current: SdkModelType | undefined = methodParam.type;
while (current) {
for (const prop of methodParam.type.properties) {
queue.push(prop);
}
current = current.baseModel;
}
}
}
return undefined;
}

function getCollectionFormat(
context: TCGCContext,
type: ModelProperty
Expand Down
2 changes: 2 additions & 0 deletions packages/typespec-client-generator-core/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,4 +598,6 @@ export enum UsageFlags {
JsonMergePatch = 1 << 4,
// Input will also be set when MultipartFormData is set.
MultipartFormData = 1 << 5,
// Used in spread.
Spread = 1 << 6,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not relevant to this pr, just want to clarify UsageFlags, if a model is used in

  • patch operation for input
  • another operation for spread input
  • another operation for output

How many medels we will get from TCGC?

Copy link
Member Author

@tadelesh tadelesh Jul 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two. one original model with path|output, one anonymous model with spread.

}
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ export interface TCGCContext {
modelsMap?: Map<Type, SdkModelType | SdkEnumType>;
operationModelsMap?: Map<Operation, Map<Type, SdkModelType | SdkEnumType>>;
generatedNames?: Map<Union | Model | TspLiteralType, string>;
spreadModels?: Map<Model, SdkModelType>;
httpOperationCache?: Map<Operation, HttpOperation>;
unionsMap?: Map<Union, SdkUnionType>;
__namespaceToApiVersionParameter: Map<Interface | Namespace, SdkParameter>;
Expand Down
39 changes: 5 additions & 34 deletions packages/typespec-client-generator-core/src/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ import {
getCrossLanguageDefinitionId,
getCrossLanguagePackageId,
getDefaultApiVersion,
getEffectivePayloadType,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deco getEffectivePayloadType?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think java won't use this. So, agree on deprecation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is still used in response.

getHttpOperationWithCache,
getLibraryName,
} from "./public-utils.js";
Expand Down Expand Up @@ -217,7 +216,7 @@ function getSdkMethodResponse<
crossLanguageDefinitionId: getCrossLanguageDefinitionId(context, operation),
decorators: [],
};
} else if (responseTypes) {
} else if (responseTypes.size === 1) {
type = allResponseBodies[0];
}
if (nonBodyExists && type) {
Expand Down Expand Up @@ -249,38 +248,10 @@ function getSdkBasicServiceMethod<
operation,
getLocationOfOperation(operation)
);
const httpOperation = getHttpOperationWithCache(context, operation);
const parameters = httpOperation.parameters;
// path/query/header parameters
for (const param of parameters.parameters) {
if (isNeverOrVoidType(param.param.type)) continue;
methodParameters.push(diagnostics.pipe(getSdkMethodParameter(context, param.param, operation)));
}
// body parameters
if (
parameters.body?.bodyKind !== "multipart" &&
parameters.body?.property &&
!isNeverOrVoidType(parameters.body.property.type)
) {
methodParameters.push(
diagnostics.pipe(getSdkMethodParameter(context, parameters.body?.property, operation))
);
} else if (parameters.body && !isNeverOrVoidType(parameters.body.type)) {
if (parameters.body.type.kind === "Model") {
const type = getEffectivePayloadType(context, parameters.body.type);
// spread case
if (type.name === "") {
for (const prop of type.properties.values()) {
methodParameters.push(diagnostics.pipe(getSdkMethodParameter(context, prop, operation)));
}
} else {
methodParameters.push(diagnostics.pipe(getSdkMethodParameter(context, type, operation)));
}
} else {
methodParameters.push(
diagnostics.pipe(getSdkMethodParameter(context, parameters.body.type, operation))
);
}

for (const param of operation.parameters.properties.values()) {
if (isNeverOrVoidType(param.type)) continue;
methodParameters.push(diagnostics.pipe(getSdkMethodParameter(context, param, operation)));
}

const serviceOperation = diagnostics.pipe(
Expand Down
28 changes: 7 additions & 21 deletions packages/typespec-client-generator-core/src/public-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,7 @@ import {
listServices,
resolveEncodedName,
} from "@typespec/compiler";
import {
HttpOperation,
getHeaderFieldName,
getHttpOperation,
getPathParamName,
getQueryParamName,
isMetadata,
isStatusCode,
} from "@typespec/http";
import { HttpOperation, getHttpOperation, isMetadata } from "@typespec/http";
import { Version, getVersions } from "@typespec/versioning";
import { pascalCase } from "change-case";
import pluralize from "pluralize";
Expand Down Expand Up @@ -119,16 +111,7 @@ export function getEffectivePayloadType(context: TCGCContext, type: Model): Mode
return type;
}

function isSchemaProperty(property: ModelProperty) {
const program = context.program;
const headerInfo = getHeaderFieldName(program, property);
const queryInfo = getQueryParamName(program, property);
const pathInfo = getPathParamName(program, property);
const statusCodeinfo = isStatusCode(program, property);
return !(headerInfo || queryInfo || pathInfo || statusCodeinfo);
}

const effective = getEffectiveModelType(program, type, isSchemaProperty);
const effective = getEffectiveModelType(program, type, (t) => !isMetadata(context.program, t));
if (effective.name) {
return effective;
}
Expand Down Expand Up @@ -404,9 +387,13 @@ function getContextPath(
for (const response of httpOperation.responses) {
for (const innerResponse of response.responses) {
if (innerResponse.body?.type) {
const body =
innerResponse.body.type.kind === "Model"
? getEffectivePayloadType(context, innerResponse.body.type)
: innerResponse.body.type;
visited.clear();
result = [{ name: root.name }];
if (dfsModelProperties(typeToFind, innerResponse.body.type, "Response")) {
if (dfsModelProperties(typeToFind, body, "Response")) {
return result;
}
}
Expand Down Expand Up @@ -475,7 +462,6 @@ function getContextPath(
const dictOrArrayItemType: Type = currentType.indexer.value;
return dfsModelProperties(expectedType, dictOrArrayItemType, pluralize.singular(displayName));
} else if (currentType.kind === "Model") {
currentType = getEffectivePayloadType(context, currentType);
// handle model
result.push({ name: displayName, type: currentType });
for (const property of currentType.properties.values()) {
Expand Down
48 changes: 42 additions & 6 deletions packages/typespec-client-generator-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,6 @@ export function getSdkModelWithDiagnostics(
operation?: Operation
): [SdkModelType, readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();
type = getEffectivePayloadType(context, type);
let sdkType = context.modelsMap?.get(type) as SdkModelType | undefined;

if (sdkType) {
Expand Down Expand Up @@ -1309,8 +1308,26 @@ function updateTypesFromOperation(
getClientTypeWithDiagnostics(context, httpBody.type, operation)
);
if (generateConvenient) {
// spread body model should be none usage
if (sdkType.kind !== "model" || !sdkType.isGeneratedName) {
// Special logic for spread body model:
// If body is from spread, then it should be an anonymous model.
// Also all model properties should be
// either equal to one of operation parameters (for case spread from model without property with metadata decorator)
// or its source property equal to one of operation parameters (for case spread from model with property with metadata decorator)
if (
httpBody.type.kind === "Model" &&
httpBody.type.name === "" &&
[...httpBody.type.properties.keys()].every(
(k) =>
operation.parameters.properties.has(k) &&
(operation.parameters.properties.get(k) ===
(httpBody.type as Model).properties.get(k) ||
operation.parameters.properties.get(k) ===
(httpBody.type as Model).properties.get(k)?.sourceProperty)
) &&
!context.spreadModels?.has(httpBody.type)
) {
context.spreadModels?.set(httpBody.type as Model, sdkType as SdkModelType);
} else {
updateUsageOfModel(context, UsageFlags.Input, sdkType);
}
if (httpBody.contentTypes.includes("application/merge-patch+json")) {
Expand All @@ -1326,9 +1343,11 @@ function updateTypesFromOperation(
for (const response of httpOperation.responses) {
for (const innerResponse of response.responses) {
if (innerResponse.body?.type && !isNeverOrVoidType(innerResponse.body.type)) {
const sdkType = diagnostics.pipe(
getClientTypeWithDiagnostics(context, innerResponse.body.type, operation)
);
const body =
innerResponse.body.type.kind === "Model"
? getEffectivePayloadType(context, innerResponse.body.type)
: innerResponse.body.type;
const sdkType = diagnostics.pipe(getClientTypeWithDiagnostics(context, body, operation));
if (generateConvenient) {
updateUsageOfModel(context, UsageFlags.Output, sdkType);
}
Expand Down Expand Up @@ -1403,6 +1422,18 @@ function updateAccessOfModel(context: TCGCContext): void {
}
}

function updateSpreadModelUsageAndAccess(context: TCGCContext): void {
for (const sdkType of context.spreadModels?.values() ?? []) {
updateUsageOfModel(context, UsageFlags.Spread, sdkType);
}
for (const sdkType of context.modelsMap?.values() ?? []) {
// if a type only has spread usage, then it could be internal
if (sdkType.usage === UsageFlags.Spread) {
sdkType.access = "internal";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a model is only used for spread, does that mean the TCGC would not return that model in getAllModels by default?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will return it but with access internal

}
}
}

interface GetAllModelsOptions {
input?: boolean;
output?: boolean;
Expand Down Expand Up @@ -1484,6 +1515,9 @@ export function getAllModelsWithDiagnostics(
if (context.operationModelsMap === undefined) {
context.operationModelsMap = new Map<Operation, Map<Type, SdkModelType | SdkEnumType>>();
}
if (context.spreadModels === undefined) {
context.spreadModels = new Map<Model, SdkModelType>();
}
for (const client of listClients(context)) {
for (const operation of listOperationsInOperationGroup(context, client)) {
// operations on a client
Expand Down Expand Up @@ -1542,6 +1576,8 @@ export function getAllModelsWithDiagnostics(
}
// update access
updateAccessOfModel(context);
// update spread model
updateSpreadModelUsageAndAccess(context);
// filter out models
filterOutModels(context);
let filter = 0;
Expand Down
Loading
Loading