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] do not replace operation with @override when listOperationsInOperationGroup #1858

Merged
merged 4 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/fix_override-2024-10-13-15-32-53.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@azure-tools/typespec-client-generator-core"
---

do not replace operation with `@override` when `listOperationsInOperationGroup`
Copy link
Member

@weidongxu-microsoft weidongxu-microsoft Nov 15, 2024

Choose a reason for hiding this comment

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

Does it only affect listOperationsInOperationGroup?

Java emitter currently list SdkServiceMethod under client.methods.

From the context, this seems also should not be the @override operation -- SdkServiceMethod should have the signature of the override operation in SdkServiceMethod, while having the signature of the original/overrided operation in SdkServiceOperation

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 just a bug for previous @override impl. for SdkServiceMethod it will use the @override operation, for SdkServiceOperation it will use the original operation, these two logic is right for the impl.

7 changes: 7 additions & 0 deletions .chronus/changes/fix_override-2024-10-15-14-8-26.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-client-generator-core"
---

add `isOverride` flag to `SdkServiceMethod`.
13 changes: 10 additions & 3 deletions packages/typespec-client-generator-core/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,9 +584,12 @@ export function listOperationsInOperationGroup(
}

for (const op of current.operations.values()) {
// Skip templated operations
if (!isTemplateDeclarationOrInstance(op)) {
operations.push(getOverriddenClientMethod(context, op) ?? op);
// Skip templated operations and omit operations
if (
!isTemplateDeclarationOrInstance(op) &&
!context.program.stateMap(omitOperation).get(op)
) {
operations.push(op);
Copy link
Contributor

Choose a reason for hiding this comment

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

is this the decided way to proceed until every language can fully support overrides? I feel it would be better to still return these instead of the original, and we could tell emitter authors that it's overridden, and we can point back to the original operation as well

Copy link
Member Author

Choose a reason for hiding this comment

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

listOperationsInOperationGroup is a decorator helper, which return the raw typespec operation. if you return the override one, then http payload will be changed. it is a bug, not a design change.

Copy link
Contributor

Choose a reason for hiding this comment

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

ahh i see, approving

}
}

Expand Down Expand Up @@ -898,6 +901,7 @@ export function getClientNameOverride(
}

const overrideKey = createStateSymbol("override");
const omitOperation = createStateSymbol("omitOperation");

// Recursive function to collect parameter names
function collectParams(
Expand Down Expand Up @@ -946,6 +950,9 @@ export const $override = (
override: Operation,
scope?: LanguageScopes,
) => {
// omit all override operation
context.program.stateMap(omitOperation).set(override, true);

// Extract and sort parameter names
const originalParams = collectParams(original.parameters.properties).sort((a, b) =>
a.name.localeCompare(b.name),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,7 @@ interface SdkServiceMethodBase<TServiceOperation extends SdkServiceOperation>
exception?: SdkMethodResponse;
generateConvenient: boolean;
generateProtocol: boolean;
isOverride: boolean;
}

export interface SdkBasicServiceMethod<TServiceOperation extends SdkServiceOperation>
Expand Down
1 change: 1 addition & 0 deletions packages/typespec-client-generator-core/src/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ function getSdkBasicServiceMethod<TServiceOperation extends SdkServiceOperation>
decorators: diagnostics.pipe(getTypeDecorators(context, operation)),
generateConvenient: shouldGenerateConvenient(context, operation),
generateProtocol: shouldGenerateProtocol(context, operation),
isOverride: override !== undefined,
});
}

Expand Down
305 changes: 0 additions & 305 deletions packages/typespec-client-generator-core/test/decorators.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { AzureCoreTestLibrary } from "@azure-tools/typespec-azure-core/testing";
import { Interface, Model, Operation } from "@typespec/compiler";
import { expectDiagnostics } from "@typespec/compiler/testing";
import { deepStrictEqual, ok, strictEqual } from "assert";
Expand Down Expand Up @@ -2229,310 +2228,6 @@ describe("typespec-client-generator-core: decorators", () => {
});
});

describe("@override", () => {
it("basic", async () => {
await runner.compileWithCustomization(
`
@service
namespace MyService;
model Params {
foo: string;
bar: string;
}

op func(...Params): void;
`,
`
namespace MyCustomizations;

op func(params: MyService.Params): void;

@@override(MyService.func, MyCustomizations.func);
`,
);
const sdkPackage = runner.context.sdkPackage;

const paramsModel = sdkPackage.models.find((x) => x.name === "Params");
ok(paramsModel);

const client = sdkPackage.clients[0];
strictEqual(client.methods.length, 1);
const method = client.methods[0];

strictEqual(method.kind, "basic");
strictEqual(method.name, "func");
strictEqual(method.parameters.length, 2);
const contentTypeParam = method.parameters.find((x) => x.name === "contentType");
ok(contentTypeParam);
const paramsParam = method.parameters.find((x) => x.name === "params");
ok(paramsParam);
strictEqual(paramsModel, paramsParam.type);

ok(method.operation.bodyParam);
strictEqual(method.operation.bodyParam.correspondingMethodParams.length, 1);
strictEqual(method.operation.bodyParam.correspondingMethodParams[0], paramsParam);
});

it("basic with scope", async () => {
const mainCode = `
@service
namespace MyService;
model Params {
foo: string;
bar: string;
}

op func(...Params): void;
`;

const customizationCode = `
namespace MyCustomizations;

op func(params: MyService.Params): void;

@@override(MyService.func, MyCustomizations.func, "csharp");
`;
await runner.compileWithCustomization(mainCode, customizationCode);
// runner has python scope, so shouldn't be overridden

ok(runner.context.sdkPackage.models.find((x) => x.name === "Params"));
const sdkPackage = runner.context.sdkPackage;
const client = sdkPackage.clients[0];
strictEqual(client.methods.length, 1);
const method = client.methods[0];
strictEqual(method.kind, "basic");
strictEqual(method.name, "func");
strictEqual(method.parameters.length, 3);

const contentTypeParam = method.parameters.find((x) => x.name === "contentType");
ok(contentTypeParam);

const fooParam = method.parameters.find((x) => x.name === "foo");
ok(fooParam);

const barParam = method.parameters.find((x) => x.name === "bar");
ok(barParam);

const httpOp = method.operation;
strictEqual(httpOp.parameters.length, 1);
strictEqual(httpOp.parameters[0].correspondingMethodParams[0], contentTypeParam);

ok(httpOp.bodyParam);
strictEqual(httpOp.bodyParam.correspondingMethodParams.length, 2);
strictEqual(httpOp.bodyParam.correspondingMethodParams[0], fooParam);
strictEqual(httpOp.bodyParam.correspondingMethodParams[1], barParam);

const runnerWithCsharp = await createSdkTestRunner({
emitterName: "@azure-tools/typespec-csharp",
});
await runnerWithCsharp.compileWithCustomization(mainCode, customizationCode);
ok(runnerWithCsharp.context.sdkPackage.models.find((x) => x.name === "Params"));

const sdkPackageWithCsharp = runnerWithCsharp.context.sdkPackage;
strictEqual(sdkPackageWithCsharp.clients.length, 1);

strictEqual(sdkPackageWithCsharp.clients[0].methods.length, 1);
const methodWithCsharp = sdkPackageWithCsharp.clients[0].methods[0];
strictEqual(methodWithCsharp.kind, "basic");
strictEqual(methodWithCsharp.name, "func");
strictEqual(methodWithCsharp.parameters.length, 2);
const contentTypeParamWithCsharp = methodWithCsharp.parameters.find(
(x) => x.name === "contentType",
);
ok(contentTypeParamWithCsharp);

const paramsParamWithCsharp = methodWithCsharp.parameters.find((x) => x.name === "params");
ok(paramsParamWithCsharp);
strictEqual(
sdkPackageWithCsharp.models.find((x) => x.name === "Params"),
paramsParamWithCsharp.type,
);

const httpOpWithCsharp = methodWithCsharp.operation;
strictEqual(httpOpWithCsharp.parameters.length, 1);
strictEqual(
httpOpWithCsharp.parameters[0].correspondingMethodParams[0],
contentTypeParamWithCsharp,
);
ok(httpOpWithCsharp.bodyParam);
strictEqual(httpOpWithCsharp.bodyParam.correspondingMethodParams.length, 1);
strictEqual(httpOpWithCsharp.bodyParam.correspondingMethodParams[0], paramsParamWithCsharp);
});

it("regrouping", async () => {
const mainCode = `
@service
namespace MyService;
model Params {
foo: string;
bar: string;
fooBar: string;
}

op func(...Params): void;
`;

const customizationCode = `
namespace MyCustomizations;

model ParamsCustomized {
...PickProperties<MyService.Params, "foo" | "bar">;
}

op func(params: MyCustomizations.ParamsCustomized, ...PickProperties<MyService.Params, "fooBar">): void;

@@override(MyService.func, MyCustomizations.func);
`;
await runner.compileWithCustomization(mainCode, customizationCode);
// runner has python scope, so shouldn't be overridden

ok(!runner.context.sdkPackage.models.find((x) => x.name === "Params"));
const sdkPackage = runner.context.sdkPackage;
const client = sdkPackage.clients[0];
strictEqual(client.methods.length, 1);
const method = client.methods[0];
strictEqual(method.kind, "basic");
strictEqual(method.name, "func");
strictEqual(method.parameters.length, 3);

const contentTypeParam = method.parameters.find((x) => x.name === "contentType");
ok(contentTypeParam);

const paramsParam = method.parameters.find((x) => x.name === "params");
ok(paramsParam);

const fooBarParam = method.parameters.find((x) => x.name === "fooBar");
ok(fooBarParam);

const httpOp = method.operation;
strictEqual(httpOp.parameters.length, 1);
strictEqual(httpOp.parameters[0].correspondingMethodParams[0], contentTypeParam);

ok(httpOp.bodyParam);
strictEqual(httpOp.bodyParam.correspondingMethodParams.length, 2);
strictEqual(httpOp.bodyParam.correspondingMethodParams[0], paramsParam);
strictEqual(httpOp.bodyParam.correspondingMethodParams[1], fooBarParam);
});

it("params mismatch", async () => {
const mainCode = `
@service
namespace MyService;
model Params {
foo: string;
bar: string;
}

op func(...Params): void;
`;

const customizationCode = `
namespace MyCustomizations;

model ParamsCustomized {
foo: string;
bar: string;
}

op func(params: MyCustomizations.ParamsCustomized): void;

@@override(MyService.func, MyCustomizations.func);
`;
const diagnostics = (
await runner.compileAndDiagnoseWithCustomization(mainCode, customizationCode)
)[1];
expectDiagnostics(diagnostics, {
code: "@azure-tools/typespec-client-generator-core/override-method-parameters-mismatch",
});
});

it("recursive params", async () => {
await runner.compileWithCustomization(
`
@service
namespace MyService;
model Params {
foo: string;
params: Params[];
}

op func(...Params): void;
`,
`
namespace MyCustomizations;

op func(input: MyService.Params): void;

@@override(MyService.func, MyCustomizations.func);
`,
);
const sdkPackage = runner.context.sdkPackage;

const paramsModel = sdkPackage.models.find((x) => x.name === "Params");
ok(paramsModel);

const client = sdkPackage.clients[0];
strictEqual(client.methods.length, 1);
const method = client.methods[0];

strictEqual(method.kind, "basic");
strictEqual(method.name, "func");
strictEqual(method.parameters.length, 2);
const contentTypeParam = method.parameters.find((x) => x.name === "contentType");
ok(contentTypeParam);
const inputParam = method.parameters.find((x) => x.name === "input");
ok(inputParam);
strictEqual(paramsModel, inputParam.type);

ok(method.operation.bodyParam);
strictEqual(method.operation.bodyParam.correspondingMethodParams.length, 1);
strictEqual(method.operation.bodyParam.correspondingMethodParams[0], inputParam);
});

it("core template", async () => {
const runnerWithCore = await createSdkTestRunner({
librariesToAdd: [AzureCoreTestLibrary],
autoUsings: ["Azure.Core"],
emitterName: "@azure-tools/typespec-java",
});
await runnerWithCore.compileWithCustomization(
`
@useDependency(Versions.v1_0_Preview_2)
@server("http://localhost:3000", "endpoint")
@service()
namespace My.Service;

model Params {
foo: string;
params: Params[];
}

@route("/template")
op templateOp is Azure.Core.RpcOperation<
Params,
Params
>;
`,
`
namespace My.Customizations;

op templateOp(params: My.Service.Params): My.Service.Params;

@@override(My.Service.templateOp, My.Customizations.templateOp);
`,
);
const sdkPackage = runnerWithCore.context.sdkPackage;
const method = sdkPackage.clients[0].methods[0];
strictEqual(method.parameters.length, 3);
ok(method.parameters.find((x) => x.name === "contentType"));
ok(method.parameters.find((x) => x.name === "accept"));

const paramsParam = method.parameters.find((x) => x.name === "params");
ok(paramsParam);
strictEqual(paramsParam.type.kind, "model");
strictEqual(paramsParam.type.name, "Params");
});
});
describe("@clientInitialization", () => {
it("main client", async () => {
await runner.compileWithCustomization(
Expand Down
Loading
Loading