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

Add a property resultSegments in lroMetadata to show the downstream how to get the final result of a lro #2102

Merged
merged 9 commits into from
Jan 28, 2025
7 changes: 7 additions & 0 deletions .chronus/changes/fix-2072-2025-0-21-15-49-15.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 a `resultSegments` property to `SdkLroServiceFinalResponse` and deprecate `resultPath` property. Add a `resultSegments` property to `SdkMethodResponse`.
13 changes: 12 additions & 1 deletion packages/typespec-client-generator-core/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,11 @@ export interface SdkMethodResponse {
kind: "method";
type?: SdkType;
resultPath?: string; // if exists, tells you how to get from the service response to the method response.
/**
* An array of properties to fetch {result} from the {response} model. Note that this property is available only in some LRO patterns.
* Temporarily this is not enabled for paging now.
*/
resultSegments?: SdkModelPropertyType[];
}

export interface SdkServiceResponse {
Expand Down Expand Up @@ -726,8 +731,14 @@ export interface SdkLroServiceFinalResponse {
envelopeResult: SdkModelType;
/** Meaningful result type */
result: SdkModelType;
/** Property path to fetch {result} from {envelopeResult}. Note that this property is available only in some LRO patterns. */
/**
* Property path to fetch {result} from {envelopeResult}. Note that this property is available only in some LRO patterns.
*
* @deprecated This property will be removed in future releases. Use `resultSegments` for synthesized property information.
*/
resultPath?: string;
/** An array of properties to fetch {result} from the {envelopeResult} model. Note that this property is available only in some LRO patterns. */
resultSegments?: SdkModelPropertyType[];
}

export interface SdkLroServiceMethod<TServiceOperation extends SdkServiceOperation>
Expand Down
53 changes: 41 additions & 12 deletions packages/typespec-client-generator-core/src/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from "./decorators.js";
import { getSdkHttpOperation, getSdkHttpParameter } from "./http.js";
import {
SdkBodyModelPropertyType,
SdkClient,
SdkClientType,
SdkEndpointParameter,
Expand All @@ -36,6 +37,7 @@ import {
SdkHttpOperation,
SdkInitializationType,
SdkLroPagingServiceMethod,
SdkLroServiceFinalResponse,
SdkLroServiceMetadata,
SdkLroServiceMethod,
SdkMethod,
Expand Down Expand Up @@ -313,8 +315,11 @@ function getSdkLroServiceMethod<TServiceOperation extends SdkServiceOperation>(

basicServiceMethod.response.type = metadata.finalResponse?.result;

// eslint-disable-next-line @typescript-eslint/no-deprecated
basicServiceMethod.response.resultPath = metadata.finalResponse?.resultPath;

basicServiceMethod.response.resultSegments = metadata.finalResponse?.resultSegments;

return diagnostics.wrap({
...basicServiceMethod,
kind: "lro",
Expand Down Expand Up @@ -344,18 +349,7 @@ function getServiceMethodLroMetadata(
return {
__raw: rawMetadata,
finalStateVia: rawMetadata.finalStateVia,
finalResponse:
rawMetadata.finalEnvelopeResult !== undefined && rawMetadata.finalEnvelopeResult !== "void"
? {
envelopeResult: diagnostics.pipe(
getClientTypeWithDiagnostics(context, rawMetadata.finalEnvelopeResult),
) as SdkModelType,
result: diagnostics.pipe(
getClientTypeWithDiagnostics(context, rawMetadata.finalResult as Model),
) as SdkModelType,
resultPath: rawMetadata.finalResultPath,
}
: undefined,
finalResponse: getFinalResponse(),
finalStep:
rawMetadata.finalStep !== undefined ? { kind: rawMetadata.finalStep.kind } : undefined,
pollingStep: {
Expand All @@ -364,6 +358,41 @@ function getServiceMethodLroMetadata(
) as SdkModelType,
},
};

function getFinalResponse(): SdkLroServiceFinalResponse | undefined {
if (
rawMetadata?.finalEnvelopeResult === undefined ||
rawMetadata.finalEnvelopeResult === "void"
) {
return undefined;
}

const envelopeResult = diagnostics.pipe(
getClientTypeWithDiagnostics(context, rawMetadata.finalEnvelopeResult),
) as SdkModelType;
const result = diagnostics.pipe(
getClientTypeWithDiagnostics(context, rawMetadata.finalResult as Model),
) as SdkModelType;
const resultPath = rawMetadata.finalResultPath;
// find the property inside the envelope result using the final result path
let sdkProperty: SdkBodyModelPropertyType | undefined = undefined;
for (const property of envelopeResult.properties) {
if (property.__raw === undefined || property.kind !== "property") {
continue;
}
if (property.__raw?.name === resultPath) {
sdkProperty = property;
break;
}
iscai-msft marked this conversation as resolved.
Show resolved Hide resolved
}

return {
envelopeResult,
result,
resultPath,
resultSegments: sdkProperty !== undefined ? [sdkProperty] : undefined,
};
}
}

function getSdkMethodResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ describe("typespec-client-generator-core: long running operation metadata", () =
);
strictEqual(roundtripModel.serializationOptions.json?.name, "User");
assert.isUndefined(metadata.finalResponse?.resultPath);
assert.isUndefined(metadata.finalResponse?.resultSegments);
});

it("LongRunningResourceDelete", async () => {
Expand Down Expand Up @@ -235,6 +236,11 @@ describe("typespec-client-generator-core: long running operation metadata", () =
strictEqual(metadata.finalResponse?.envelopeResult, pollingModel);
strictEqual(metadata.finalResponse?.result, returnModel);
strictEqual(metadata.finalResponse?.resultPath, "result");
// find the property
const resultProperty = pollingModel.properties.find((p) => p.name === "result");
ok(metadata.finalResponse?.resultSegments);
strictEqual(metadata.finalResponse?.resultSegments[0], resultProperty);

assert.isTrue(
hasFlag(pollingModel.usage, UsageFlags.LroFinalEnvelope),
"the polling model here is also the final envelope model, it should have the usage of LroFinalEnvelope",
Expand Down Expand Up @@ -326,13 +332,94 @@ describe("typespec-client-generator-core: long running operation metadata", () =
strictEqual(metadata.finalResponse?.envelopeResult, pollingModel);
strictEqual(metadata.finalResponse?.result, returnModel);
strictEqual(metadata.finalResponse?.resultPath, "result");
// find the property
const resultProperty = pollingModel.properties.find((p) => p.name === "result");
ok(metadata.finalResponse?.resultSegments);
strictEqual(metadata.finalResponse?.resultSegments[0], resultProperty);
assert.isTrue(
hasFlag(pollingModel.usage, UsageFlags.LroFinalEnvelope),
"the polling model here is also the final envelope model, it should have the usage of LroFinalEnvelope",
);
});
});
describe("Custom LRO", () => {
it("@lroResult with client name and/or encoded name", async () => {
await runner.compileWithBuiltInAzureCoreService(`
op CustomLongRunningOperation<
TParams extends TypeSpec.Reflection.Model,
TResponse extends TypeSpec.Reflection.Model
> is Foundations.Operation<
TParams,
AcceptedResponse & {
@pollingLocation
@header("Operation-Location")
operationLocation: ResourceLocation<TResponse>;
}
>;

@resource("resources")
model Resource {
@visibility("read")
id: string;

@key
@visibility("read")
name: string;

description?: string;
type: string;
}

// no "result" property
model OperationDetails {
@doc("Operation ID")
@key
@visibility("read", "create")
id: uuid;

status: Azure.Core.Foundations.OperationState;
error?: Azure.Core.Foundations.Error;

@lroResult
@clientName("longRunningResult")
@encodedName("application/json", "lro_result")
result?: Resource;
}

@doc("Response")
@route("/response")
interface ResponseOp {

@route("/lro-result")
lroInvalidResult is CustomLongRunningOperation<
{
@body request: Resource;
},
OperationDetails
>;
}
`);

const client = runner.context.sdkPackage.clients[0];
const method = client.methods[0];
strictEqual(method.kind, "clientaccessor");
const resourceOpClient = method.response;
const lroMethod = resourceOpClient.methods[0];
strictEqual(lroMethod.kind, "lro");
const lroMetadata = lroMethod.lroMetadata;
ok(lroMetadata);
strictEqual(lroMetadata.finalResponse?.resultPath, "result"); // this is showing the typespec name, which is neither client name nor wire name
// find the model
const envelopeResult = runner.context.sdkPackage.models.find(
(m) => m.name === "OperationDetails",
);
const resultProperty = envelopeResult?.properties.find(
(p) => p.name === "longRunningResult",
);
ok(lroMetadata.finalResponse?.resultSegments);
strictEqual(resultProperty, lroMetadata.finalResponse?.resultSegments[0]);
});

it("@pollingOperation", async () => {
await runner.compileWithVersionedService(`
@resource("analyze/jobs")
Expand Down Expand Up @@ -789,6 +876,7 @@ describe("typespec-client-generator-core: long running operation metadata", () =
strictEqual(metadata.finalResponse?.envelopeResult, roundtripModel);
strictEqual(metadata.finalResponse?.result, roundtripModel);
assert.isUndefined(metadata.finalResponse.resultPath);
assert.isUndefined(metadata.finalResponse.resultSegments);
});

it("ArmResourceDeleteWithoutOkAsync", async () => {
Expand Down
Loading