Skip to content

Commit

Permalink
Http type array (#4212)
Browse files Browse the repository at this point in the history
Fixes #3992
  • Loading branch information
m-nash authored Aug 20, 2024
1 parent 2d4f55f commit 9961dbe
Show file tree
Hide file tree
Showing 35 changed files with 4,110 additions and 29 deletions.
1 change: 0 additions & 1 deletion packages/http-client-csharp/eng/scripts/Generate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ $failingSpecs = @(
Join-Path 'http' 'server' 'versions' 'versioned'
Join-Path 'http' 'special-headers' 'conditional-request'
Join-Path 'http' 'special-headers' 'repeatability'
Join-Path 'http' 'type' 'array'
Join-Path 'http' 'type' 'dictionary'
Join-Path 'http' 'type' 'scalar'
Join-Path 'http' 'type' 'union'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ private MethodProvider BuildFromEnumerableBinaryDataMethod()
new ForeachStatement("item", enumerableParameter.As<IEnumerable<BinaryData>>(), out var item)
{
new IfElseStatement(
item.InvokeEquals(Null),
item.Equal(Null),
writer.WriteNullValue(),
writer.WriteBinaryData(item))
},
Expand Down Expand Up @@ -138,7 +138,7 @@ private MethodProvider BuildFromReadOnlySpanMethod()
body.AddRange(
[
writer.WriteStartArray(),
Declare<int>("i", Int(0), out var i),
Declare("i", Int(0), out var i),
new ForStatement(null, i.LessThan(spanParameter.Property(nameof(ReadOnlySpan<byte>.Length))), i.Increment())
{
writer.WriteObjectValue(new IndexerExpression(spanParameter, i).As(tType), ModelSerializationExtensionsSnippets.Wire)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Generator.CSharp.ClientModel.Primitives;
using Microsoft.Generator.CSharp.ClientModel.Snippets;
Expand Down Expand Up @@ -75,17 +76,23 @@ private MethodProvider BuildConvenienceMethod(MethodProvider protocolMethod, boo
MethodBodyStatement[] methodBody;
if (responseBodyType is null)
{
methodBody = [Return(This.Invoke(protocolMethod.Signature, [.. GetParamConversions(ConvenienceMethodParameters), Null], isAsync))];
methodBody =
[
.. GetStackVariablesForProtocolParamConversion(ConvenienceMethodParameters, out var paramDeclarations),
Return(This.Invoke(protocolMethod.Signature, [.. GetParamConversions(ConvenienceMethodParameters, paramDeclarations), Null], isAsync))
];
}
else
{
methodBody =
[
Declare("result", This.Invoke(protocolMethod.Signature, [.. GetParamConversions(ConvenienceMethodParameters), Null], isAsync).As<ClientResult>(), out ScopedApi<ClientResult> result),
.. GetStackVariablesForProtocolParamConversion(ConvenienceMethodParameters, out var paramDeclarations),
Declare("result", This.Invoke(protocolMethod.Signature, [.. GetParamConversions(ConvenienceMethodParameters, paramDeclarations), Null], isAsync).As<ClientResult>(), out ScopedApi<ClientResult> result),
.. GetStackVariablesForReturnValueConversion(result, responseBodyType, isAsync, out var declarations),
Return(Static<ClientResult>().Invoke(
nameof(ClientResult.FromValue),
[
responseBodyType.Equals(typeof(string)) ? result.GetRawResponse().Content().InvokeToString() : GetResultConversion(result, responseBodyType),
responseBodyType.Equals(typeof(string)) ? result.GetRawResponse().Content().InvokeToString() : GetResultConversion(result, responseBodyType, declarations),
result.Invoke("GetRawResponse")
])),
];
Expand All @@ -96,41 +103,129 @@ private MethodProvider BuildConvenienceMethod(MethodProvider protocolMethod, boo
return convenienceMethod;
}

private ValueExpression GetResultConversion(ScopedApi<ClientResult> result, CSharpType resultType)
private IEnumerable<MethodBodyStatement> GetStackVariablesForProtocolParamConversion(IReadOnlyList<ParameterProvider> convenienceMethodParameters, out Dictionary<string, ValueExpression> declarations)
{
List<MethodBodyStatement> statements = new List<MethodBodyStatement>();
declarations = new Dictionary<string, ValueExpression>();
foreach (var parameter in convenienceMethodParameters)
{
if (parameter.Location == ParameterLocation.Body)
{
if (parameter.Type.IsReadOnlyMemory)
{
statements.Add(UsingDeclare("content", BinaryContentHelperSnippets.FromReadOnlyMemory(parameter), out var content));
declarations["content"] = content;
}
else if (parameter.Type.IsList)
{
statements.Add(UsingDeclare("content", BinaryContentHelperSnippets.FromEnumerable(parameter), out var content));
declarations["content"] = content;
}
}
}
return statements;
}

private IEnumerable<MethodBodyStatement> GetStackVariablesForReturnValueConversion(ScopedApi<ClientResult> result, CSharpType responseBodyType, bool isAsync, out Dictionary<string, ValueExpression> declarations)
{
if (responseBodyType.IsList)
{
var elementType = responseBodyType.Arguments[0];
if (!elementType.IsFrameworkType || elementType.Equals(typeof(TimeSpan)) || elementType.Equals(typeof(BinaryData)))
{
var valueDeclaration = Declare("value", New.Instance(new CSharpType(typeof(List<>), elementType)).As(responseBodyType), out var value);
MethodBodyStatement[] statements =
[
valueDeclaration,
UsingDeclare("document", JsonDocumentSnippets.Parse(result.GetRawResponse().ContentStream(), isAsync), out var document),
ForeachStatement.Create("item", document.RootElement().EnumerateArray(), out ScopedApi<JsonElement> item)
.Add(GetElementConversion(elementType, item, value))
];
declarations = new Dictionary<string, ValueExpression>
{
{ "value", value }
};
return statements;
}
}

declarations = [];
return [];
}

private MethodBodyStatement GetElementConversion(CSharpType elementType, ScopedApi<JsonElement> item, ScopedApi value)
{
if (elementType.Equals(typeof(TimeSpan)))
{
return value.Add(item.Invoke("GetTimeSpan", Literal("P")));
}
else if (elementType.Equals(typeof(BinaryData)))
{
return new IfElseStatement(
item.ValueKind().Equal(JsonValueKindSnippets.Null),
value.Add(Null),
value.Add(BinaryDataSnippets.FromString(item.GetRawText())));
}
else
{
return value.Add(Static(elementType).Invoke($"Deserialize{elementType.Name}", item, ModelSerializationExtensionsSnippets.Wire));
}
}

private ValueExpression GetResultConversion(ScopedApi<ClientResult> result, CSharpType responseBodyType, Dictionary<string, ValueExpression> declarations)
{
if (resultType.Equals(typeof(BinaryData)))
if (responseBodyType.Equals(typeof(BinaryData)))
{
return result.GetRawResponse().Content();
}
return result.CastTo(resultType);
if (responseBodyType.IsList)
{
if (!responseBodyType.Arguments[0].IsFrameworkType || responseBodyType.Arguments[0].Equals(typeof(TimeSpan)) || responseBodyType.Arguments[0].Equals(typeof(BinaryData)))
{
return declarations["value"];
}
else
{
return result.GetRawResponse().Content().ToObjectFromJson(responseBodyType);
}
}
return result.CastTo(responseBodyType);
}

private IReadOnlyList<ValueExpression> GetParamConversions(IReadOnlyList<ParameterProvider> convenienceMethodParameters)
private IReadOnlyList<ValueExpression> GetParamConversions(IReadOnlyList<ParameterProvider> convenienceMethodParameters, Dictionary<string, ValueExpression> declarations)
{
List<ValueExpression> conversions = new List<ValueExpression>();
foreach (var param in convenienceMethodParameters)
{
if (param.Type.IsEnum)
if (param.Location == ParameterLocation.Body)
{
if (param.Location == ParameterLocation.Body)
if (param.Type.IsReadOnlyMemory || param.Type.IsList)
{
conversions.Add(declarations["content"]);
}
else if (param.Type.IsEnum)
{
conversions.Add(BinaryContentSnippets.Create(BinaryDataSnippets.FromObjectAsJson(param.Type.ToSerial(param))));
}
else if (param.Type.Equals(typeof(BinaryData)))
{
conversions.Add(BinaryContentSnippets.Create(param));
}
else if (param.Type.Equals(typeof(string)))
{
var bdExpression = Operation.RequestBodyMediaType == BodyMediaType.Json
? BinaryDataSnippets.FromObjectAsJson(param)
: BinaryDataSnippets.FromString(param);
conversions.Add(BinaryContentSnippets.Create(bdExpression));
}
else
{
conversions.Add(param.Type.ToSerial(param));
conversions.Add(param);
}
}
else if (param.Type.Equals(typeof(BinaryData)) && param.Location == ParameterLocation.Body)
{
conversions.Add(BinaryContentSnippets.Create(param));
}
else if (param.Location == ParameterLocation.Body && param.Type.Equals(typeof(string)))
else if (param.Type.IsEnum)
{
var bdExpression = Operation.RequestBodyMediaType == BodyMediaType.Json
? BinaryDataSnippets.FromObjectAsJson(param)
: BinaryDataSnippets.FromString(param);
conversions.Add(BinaryContentSnippets.Create(bdExpression));
conversions.Add(param.Type.ToSerial(param));
}
else
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.ClientModel;
using Microsoft.Generator.CSharp.ClientModel.Providers;
using Microsoft.Generator.CSharp.Expressions;
using Microsoft.Generator.CSharp.Snippets;
using static Microsoft.Generator.CSharp.Snippets.Snippet;

namespace Microsoft.Generator.CSharp.ClientModel.Snippets
{
internal static class BinaryContentHelperSnippets
{
public static ScopedApi<BinaryContent> FromEnumerable(ValueExpression body)
=> Static<BinaryContentHelperDefinition>().Invoke("FromEnumerable", body).As<BinaryContent>();

public static ScopedApi<BinaryContent> FromReadOnlyMemory(ValueExpression body)
=> Static<BinaryContentHelperDefinition>().Invoke("FromReadOnlyMemory", body).As<BinaryContent>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Text.Json;
using Microsoft.Generator.CSharp.Expressions;
using Microsoft.Generator.CSharp.Snippets;
using Microsoft.Generator.CSharp.Statements;

namespace Microsoft.Generator.CSharp.ClientModel.Snippets
{
internal static class JsonSerializerOptionsSnippets
{
public static MethodBodyStatement AddConverter(this ScopedApi<JsonSerializerOptions> options, ValueExpression converter)
=> options.Property(nameof(JsonSerializerOptions.Converters)).Invoke(nameof(IList<object>.Add), converter).Terminate();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ namespace Microsoft.Generator.CSharp
/// </summary>
public class Configuration
{
private static readonly string[] _badNamespaces =
[
"Type",
"Array"
];

private const string ConfigurationFileName = "Configuration.json";

// for mocking
Expand Down Expand Up @@ -45,11 +51,67 @@ private Configuration(
GenerateTestProject = generateTestProject;
LibraryName = libraryName;
UseModelNamespace = useModelNamespace;
RootNamespace = libraryNamespace;
ModelNamespace = useModelNamespace ? $"{libraryNamespace}.Models" : libraryNamespace;
RootNamespace = GetCleanNameSpace(libraryNamespace);
ModelNamespace = useModelNamespace ? $"{RootNamespace}.Models" : RootNamespace;
DisableXmlDocs = disableXmlDocs;
}

private string GetCleanNameSpace(string libraryNamespace)
{
Span<char> dest = stackalloc char[libraryNamespace.Length + GetSegmentCount(libraryNamespace)];
var source = libraryNamespace.AsSpan();
var destIndex = 0;
var nextDot = source.IndexOf('.');
while (nextDot != -1)
{
var segment = source.Slice(0, nextDot);
if (IsSpecialSegment(segment))
{
dest[destIndex] = '_';
destIndex++;
}
segment.CopyTo(dest.Slice(destIndex));
destIndex += segment.Length;
dest[destIndex] = '.';
destIndex++;
source = source.Slice(nextDot + 1);
nextDot = source.IndexOf('.');
}
if (IsSpecialSegment(source))
{
dest[destIndex] = '_';
destIndex++;
}
source.CopyTo(dest.Slice(destIndex));
destIndex += source.Length;
return dest.Slice(0, destIndex).ToString();
}

private bool IsSpecialSegment(ReadOnlySpan<char> readOnlySpan)
{
for (int i = 0; i < _badNamespaces.Length; i++)
{
if (readOnlySpan.Equals(_badNamespaces[i], StringComparison.Ordinal))
{
return true;
}
}
return false;
}

private static int GetSegmentCount(string libraryNamespace)
{
int count = 0;
for (int i = 0; i < libraryNamespace.Length; i++)
{
if (libraryNamespace[i] == '.')
{
count++;
}
}
return ++count;
}

/// <summary>
/// Contains the known set of configuration options.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@
"commandName": "Executable",
"executablePath": "$(SolutionDir)/../dist/generator/Microsoft.Generator.CSharp.exe"
},
"http-type-array": {
"commandLineArgs": "$(SolutionDir)/TestProjects/CadlRanch/http/type/array -p StubLibraryPlugin",
"commandName": "Executable",
"executablePath": "$(SolutionDir)/../dist/generator/Microsoft.Generator.CSharp.exe"
},
"Unbranded-TypeSpec": {
"commandLineArgs": "$(SolutionDir)/TestProjects/Local/Unbranded-TypeSpec -p ClientModelPlugin",
"commandName": "Executable",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ namespace Microsoft.Generator.CSharp.Snippets
{
public static class BinaryDataSnippets
{
public static ScopedApi ToObjectFromJson(this ScopedApi<BinaryData> binaryData, Type responseType)
=> binaryData.Invoke(nameof(BinaryData.ToObjectFromJson), [], [new CSharpType(responseType)], false).As(responseType);
public static ScopedApi ToObjectFromJson(this ScopedApi<BinaryData> binaryData, CSharpType responseType)
=> binaryData.Invoke(nameof(BinaryData.ToObjectFromJson), [], [responseType], false).As(responseType);

public static ScopedApi ToObjectFromJson(this ScopedApi<BinaryData> binaryData, CSharpType responseType, ValueExpression jsonSerializerOptions)
=> binaryData.Invoke(nameof(BinaryData.ToObjectFromJson), [jsonSerializerOptions], [responseType], false).As(responseType);

public static ScopedApi<byte[]> ToArray(this ScopedApi<BinaryData> binaryData) => binaryData.Invoke(nameof(BinaryData.ToArray)).As<byte[]>();

Expand Down
2 changes: 1 addition & 1 deletion packages/http-client-csharp/generator/Packages.Data.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<PackageReference Update="Microsoft.Build" Version="17.9.5" />
<PackageReference Update="NuGet.Configuration" Version="6.9.1" />
<PackageReference Update="System.ComponentModel.Composition" Version="8.0.0" />
<PackageReference Update="System.ClientModel" Version="1.1.0-beta.4" />
<PackageReference Update="System.ClientModel" Version="1.1.0-beta.7" />
<PackageReference Update="System.Memory.Data" Version="8.0.0" />
</ItemGroup>
</Project>
Loading

0 comments on commit 9961dbe

Please sign in to comment.