Skip to content

Commit

Permalink
feat(dpg): support logicalPath of lro (#3839)
Browse files Browse the repository at this point in the history
- pass in `logicalPath` info from emitter
- in generator, generate conversion method to fetch the return value from `logicalPath`
- add test case

resolve #3837

---------

Co-authored-by: Mingzhe Huang (from Dev Box) <[email protected]>
  • Loading branch information
archerzz and Mingzhe Huang (from Dev Box) authored Oct 26, 2023
1 parent cf4fdcf commit 0a56abd
Show file tree
Hide file tree
Showing 18 changed files with 411 additions and 310 deletions.
3 changes: 2 additions & 1 deletion src/AutoRest.CSharp/Common/Input/CodeModelConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ public List<InputParameter> CreateOperationParameters(IReadOnlyCollection<Reques

return new OperationLongRunning(
FinalStateVia: operation.LongRunningFinalStateVia,
FinalResponse: CreateOperationResponse(operation.LongRunningFinalResponse)
FinalResponse: CreateOperationResponse(operation.LongRunningFinalResponse),
ResultPath: null
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,29 @@
// Licensed under the MIT License.

using Azure.Core;
using System.Linq;

namespace AutoRest.CSharp.Common.Input;

internal record OperationLongRunning(OperationFinalStateVia FinalStateVia, OperationResponse FinalResponse)
internal record OperationLongRunning(OperationFinalStateVia FinalStateVia, OperationResponse FinalResponse, string? ResultPath)
{
public OperationLongRunning() : this(FinalStateVia: OperationFinalStateVia.Location, FinalResponse: new OperationResponse()) { }
public OperationLongRunning() : this(FinalStateVia: OperationFinalStateVia.Location, FinalResponse: new OperationResponse(), null) { }

/// <summary>
/// Meaningful return type of the long running operation.
/// </summary>
public InputType? ReturnType
{
get
{
if (FinalResponse.BodyType is null)
return null;

if (ResultPath is null)
return FinalResponse.BodyType;

var rawResponseType = (InputModelType)FinalResponse.BodyType;
return rawResponseType.Properties.FirstOrDefault(p => p.SerializedName == ResultPath)!.Type;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ protected LongRunningOperation(InputOperation operation, BuildContext context, L
FinalStateVia = operation.LongRunning.FinalStateVia;

var finalResponse = operation.LongRunning.FinalResponse;
var finalResponseType = finalResponse.BodyType;
var returnType = operation.LongRunning.ReturnType;

if (finalResponseType != null)
if (returnType != null)
{
ResultType = TypeFactory.GetOutputType(context.TypeFactory.CreateType(finalResponseType with {IsNullable = false}));
ResultSerialization = SerializationBuilder.Build(finalResponse.BodyMediaType, finalResponseType, ResultType, null);
ResultType = TypeFactory.GetOutputType(context.TypeFactory.CreateType(returnType with {IsNullable = false}));
ResultSerialization = SerializationBuilder.Build(finalResponse.BodyMediaType, returnType, ResultType, null);

var paging = operation.Paging;
if (paging != null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections.Generic;
using AutoRest.CSharp.Generation.Types;
using AutoRest.CSharp.Output.Models;
using AutoRest.CSharp.Output.Models.Shared;

namespace AutoRest.CSharp.Common.Output.Models
{
/// <summary>
/// Method to fetch return value specified by `logicalPath` from raw response.
/// </summary>
/// <param name="ReturnType">Type of the return value specified by <see cref="ResultPath"/>.</param>
/// <param name="ResponseTypeName">Name of the raw response type.</param>
/// <param name="ResultPath">`logicalPath`.</param>
internal record LongRunningResultRetrievalMethod(CSharpType ReturnType, string ResponseTypeName, string ResultPath)
{
public MethodSignature MethodSignature => new(
Name: $"Fetch{ReturnType.Name}From{ResponseTypeName}",
Summary: null,
Description: null,
Modifiers: MethodSignatureModifiers.Private,
ReturnType: ReturnType,
ReturnDescription: null,
Parameters: new List<Parameter>() { KnownParameters.Response }
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ internal static class KnownParameters
private static readonly CSharpType RequestContentNullableType = new(Configuration.ApiTypes.RequestContentType, true);
private static readonly CSharpType RequestContextType = new(Configuration.ApiTypes.RequestContextType);
private static readonly CSharpType RequestContextNullableType = new(Configuration.ApiTypes.RequestContextType, true);
private static readonly CSharpType ResponseType = new(Configuration.ApiTypes.ResponseType);

public static readonly Parameter ClientDiagnostics = new("clientDiagnostics", $"The handler for diagnostic messaging in the client.", new CSharpType(Configuration.ApiTypes.ClientDiagnosticsType), null, ValidationType.AssertNotNull, null);
public static readonly Parameter Pipeline = new("pipeline", $"The HTTP pipeline for sending and receiving REST requests and responses", new CSharpType(Configuration.ApiTypes.HttpPipelineType), null, ValidationType.AssertNotNull, null);
Expand All @@ -42,5 +43,7 @@ internal static class KnownParameters

public static readonly Parameter CancellationTokenParameter = new("cancellationToken", $"The cancellation token to use", new CSharpType(typeof(CancellationToken)), Constant.NewInstanceOf(typeof(CancellationToken)), ValidationType.None, null);
public static readonly Parameter EnumeratorCancellationTokenParameter = new("cancellationToken", $"Enumerator cancellation token", typeof(CancellationToken), Constant.NewInstanceOf(typeof(CancellationToken)), ValidationType.None, null) { Attributes = new[] { new CSharpAttribute(typeof(EnumeratorCancellationAttribute)) } };

public static readonly Parameter Response = new("response", $"Response returned from backend service", ResponseType, null, ValidationType.None, null);
}
}
31 changes: 30 additions & 1 deletion src/AutoRest.CSharp/LowLevel/Generation/DpgClientWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using AutoRest.CSharp.Common.Generation.Writers;
using AutoRest.CSharp.Common.Input;
Expand Down Expand Up @@ -97,10 +98,20 @@ public void WriteClient()
WriteCancellationTokenToRequestContextMethod();
}
WriteResponseClassifierMethod(_writer, _client.ResponseClassifierTypes);
WriteLongRunningResultRetrievalMethods();
}
}
}

private void WriteLongRunningResultRetrievalMethods()
{
foreach (var method in _client.ClientMethods.Select(c => c.LongRunningResultRetrievalMethod).WhereNotNull())
{
_writer.Line();
WriteLroResultRetrievalMethod(method);
}
}

public static void WriteProtocolMethods(CodeWriter writer, ClientFields fields, LowLevelClientMethod clientMethod)
{
WriteRequestCreationMethod(writer, clientMethod.RequestMethod, fields);
Expand Down Expand Up @@ -357,12 +368,30 @@ private void WriteConvenienceLroMethod(LowLevelClientMethod clientMethod, Conven
.LineRaw(";");
// return ProtocolOperationHelpers.Convert(response, r => responseType.FromResponse(r), ClientDiagnostics, scopeName);
var diagnostic = convenienceMethod.Diagnostic ?? clientMethod.ProtocolMethodDiagnostic;
_writer.Line($"return {typeof(ProtocolOperationHelpers)}.{nameof(ProtocolOperationHelpers.Convert)}({responseVariable:I}, {responseType}.FromResponse, {fields.ClientDiagnosticsProperty.Name}, {diagnostic.ScopeName:L});");
_writer.Line($"return {typeof(ProtocolOperationHelpers)}.{nameof(ProtocolOperationHelpers.Convert)}({responseVariable:I}, {GetConversionMethodStatement(clientMethod.LongRunningResultRetrievalMethod, responseType)}, {fields.ClientDiagnosticsProperty.Name}, {diagnostic.ScopeName:L});");
}
}
_writer.Line();
}

private FormattableString GetConversionMethodStatement(LongRunningResultRetrievalMethod? convertMethod, CSharpType responseType)
{
if (convertMethod is null)
{
return $"{responseType}.FromResponse";
}
return $"{convertMethod.MethodSignature.Name}";
}

private void WriteLroResultRetrievalMethod(LongRunningResultRetrievalMethod method)
{
using (_writer.WriteMethodDeclaration(method.MethodSignature))
{
_writer.Line($"var resultJsonElement = {typeof(JsonDocument)}.{nameof(JsonDocument.Parse)}(response.{nameof(Response.Content)}).{nameof(JsonDocument.RootElement)}.{nameof(JsonElement.GetProperty)}(\"{method.ResultPath}\");");
_writer.Line($"return {method.ReturnType}.Deserialize{method.ReturnType.Name}(resultJsonElement);");
}
}

private void WriteConveniencePageableMethod(LowLevelClientMethod clientMethod, ConvenienceMethod convenienceMethod, ProtocolMethodPaging pagingInfo, ClientFields fields, bool async)
{
_writer.WritePageable(convenienceMethod, clientMethod.RequestMethod, pagingInfo.NextPageMethod, fields.ClientDiagnosticsProperty, fields.PipelineField, clientMethod.ProtocolMethodDiagnostic.ScopeName, pagingInfo.ItemName, pagingInfo.NextLinkName, async);
Expand Down
4 changes: 3 additions & 1 deletion src/AutoRest.CSharp/LowLevel/Output/LowLevelClientMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ internal record LowLevelClientMethod(
OperationLongRunning? LongRunning,
RequestConditionHeaders ConditionHeaderFlag,
IEnumerable<DpgOperationSample> Samples,
ConvenienceMethodOmittingMessage? ConvenienceMethodOmittingMessage)
ConvenienceMethodOmittingMessage? ConvenienceMethodOmittingMessage,
LongRunningResultRetrievalMethod? LongRunningResultRetrievalMethod) // TODO: move `LongRunningResultRetrievalMethod` under output model of long running, currently we're using
// input model of long running in DPG
{
public bool ShouldGenerateConvenienceMethodRef()
{
Expand Down
12 changes: 10 additions & 2 deletions src/AutoRest.CSharp/LowLevel/Output/OperationMethodChainBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public LowLevelClientMethod BuildOperationMethodChain()
// samples will build below
var samples = new List<DpgOperationSample>();

var method = new LowLevelClientMethod(protocolMethodSignature, convenienceMethod, _restClientMethod, requestBodyType, responseBodyType, diagnostic, _protocolMethodPaging, Operation.LongRunning, _conditionHeaderFlag, samples, convenienceMethodInfo.Message);
var method = new LowLevelClientMethod(protocolMethodSignature, convenienceMethod, _restClientMethod, requestBodyType, responseBodyType, diagnostic, _protocolMethodPaging, Operation.LongRunning, _conditionHeaderFlag, samples, convenienceMethodInfo.Message, GetLongRunningResultRetrievalMethod(Operation.LongRunning));

BuildSamples(method, samples);

Expand Down Expand Up @@ -162,6 +162,14 @@ private void BuildSamples(LowLevelClientMethod method, List<DpgOperationSample>
}
}

private LongRunningResultRetrievalMethod? GetLongRunningResultRetrievalMethod(OperationLongRunning? longRunning)
{
if (longRunning is { ResultPath: not null })
return new(_typeFactory.CreateType(longRunning.ReturnType!), longRunning.FinalResponse.BodyType!.Name, longRunning.ResultPath);

return null;
}

private ConvenienceMethodGenerationInfo ShouldGenerateConvenienceMethod()
{
// we do not generate convenience method if the emitter does not allow it.
Expand Down Expand Up @@ -327,7 +335,7 @@ private ReturnTypeChain BuildReturnTypes()
{
if (Operation.LongRunning != null)
{
return Operation.LongRunning.FinalResponse.BodyType;
return Operation.LongRunning.ReturnType;
}

var operationBodyTypes = Operation.Responses.Where(r => !r.IsErrorResponse).Select(r => r.BodyType).Distinct();
Expand Down
16 changes: 14 additions & 2 deletions src/TypeSpec.Extension/Emitter.Csharp/src/lib/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,11 @@ export function loadOperation(
if (op.verb !== "delete") {
const formattedType = getFormattedType(
program,
metadata.logicalResult
// TODO: we should check `logicalPath` or other ways to determine body type,
// after https://github.com/Azure/typespec-azure/issues/3725 is fixed
op.verb === "post"
? metadata.envelopeResult
: metadata.logicalResult
);
bodyType = getInputType(context, formattedType, models, enums);
}
Expand All @@ -401,7 +405,15 @@ export function loadOperation(
StatusCodes: op.verb === "delete" ? [204] : [200],
BodyType: bodyType,
BodyMediaType: BodyMediaType.Json
} as OperationResponse
} as OperationResponse,
ResultPath:
metadata.logicalPath ??
// TODO: roll back changes when `logicalPath` can be definitive
// https://github.com/Azure/typespec-azure/issues/3725
(metadata.envelopeResult != metadata.logicalResult &&
op.verb === "post"
? "result" // actually `result` is the only allowed path for now
: undefined)
} as OperationLongRunning;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ import { OperationResponse } from "./operationResponse.js";
export interface OperationLongRunning {
FinalStateVia: OperationFinalStateVia;
FinalResponse: OperationResponse;
ResultPath?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,5 @@ model Pet {

interface Projects {
@doc("Creates a new pet or updates an existing one.")
createOrUpdate is LongRunningResourceCreateOrUpdate<Pet>;
createOrUpdate is LongRunningResourceCreateOrReplace<Pet>;
}
Loading

0 comments on commit 0a56abd

Please sign in to comment.