Skip to content

Commit

Permalink
feat: support additional property serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
jorgerangel-msft committed Sep 17, 2024
1 parent 9a28a9c commit 8bbc09f
Show file tree
Hide file tree
Showing 151 changed files with 13,366 additions and 150 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 @@ -76,7 +76,6 @@ $failingSpecs = @(
Join-Path 'http' 'type' 'model' 'inheritance' 'not-discriminated'
Join-Path 'http' 'type' 'model' 'inheritance' 'recursive'
Join-Path 'http' 'type' 'model' 'templated'
Join-Path 'http' 'type' 'property' 'additional-properties'
)

$cadlRanchLaunchProjects = @{}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,14 @@ public static ScopedApi<bool> TryGetInt64(this ScopedApi<JsonElement> jsonElemen
var invocation = new InvokeMethodExpression(jsonElement, nameof(JsonElement.TryGetInt64), [new DeclarationExpression(longValueDeclaration, true)]);
return invocation.As<bool>();
}

internal static ScopedApi<bool> TryGetValue<T>(this ScopedApi<JsonElement> jsonElement, string methodName, out ScopedApi<T> value)
{
var valueName = typeof(T).Name.ToVariableName();
var valueDeclaration = new VariableExpression(typeof(T), valueName);
value = valueDeclaration.As<T>();
var invocation = new InvokeMethodExpression(jsonElement, methodName, [new DeclarationExpression(valueDeclaration, true)]);
return invocation.As<bool>();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public static ScopedApi<string> Name(this ScopedApi<JsonProperty> jsonProperty)

public static ScopedApi<JsonElement> Value(this ScopedApi<JsonProperty> jsonProperty)
=> jsonProperty.Property(nameof(JsonProperty.Value)).As<JsonElement>();
public static ScopedApi<JsonElement> ValueKind(this ScopedApi<JsonProperty> jsonProperty)
=> Value(jsonProperty).Property(nameof(JsonProperty.Value.ValueKind)).As<JsonElement>();

public static ScopedApi<bool> NameEquals(this ScopedApi<JsonProperty> jsonProperty, string value)
=> jsonProperty.Invoke(nameof(JsonProperty.NameEquals), LiteralU8(value)).As<bool>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Generator.CSharp.ClientModel.Providers;
using Microsoft.Generator.CSharp.Input;
using Microsoft.Generator.CSharp.Tests.Common;
using NUnit.Framework;

namespace Microsoft.Generator.CSharp.ClientModel.Tests.Providers.MrwSerializationTypeDefinitions
{
internal class AdditionalPropertiesTest
{
[TestCaseSource(nameof(TestBuildDeserializationMethodTestCases))]
public void TestBuildDeserializationMethod(
InputType additionalPropsValueType,
string[] expectedValueTypeNames,
string[] expectedValueKindChecks)
{
var inputModel = InputFactory.Model("cat",
properties:
[
InputFactory.Property("color", InputPrimitiveType.String, isRequired: true),
],
additionalProperties: additionalPropsValueType);
MockHelpers.LoadMockPlugin(inputModels: () => [inputModel]);

var model = ClientModelPlugin.Instance.TypeFactory.CreateModel(inputModel);
var serializations = model!.SerializationProviders.FirstOrDefault() as MrwSerializationTypeDefinition;
Assert.IsNotNull(serializations);
var deserializationMethod = serializations!.BuildDeserializationMethod();
Assert.IsNotNull(deserializationMethod);

var signature = deserializationMethod?.Signature;

Assert.IsNotNull(signature);
Assert.AreEqual(model.Type, signature?.ReturnType);

var methodBody = deserializationMethod?.BodyStatements;
Assert.IsNotNull(methodBody);

var methodBodyString = methodBody!.ToDisplayString();
// validate the additional properties variable declarations
for (var i = 0; i < expectedValueTypeNames.Length; i++)
{
var expectedVariableName = i == 0 ? "additionalProperties" : $"additional{expectedValueTypeNames[i].ToCleanName()}Properties";
var expectedDeclaration = $"global::System.Collections.Generic.IDictionary<string, {expectedValueTypeNames[i].ToVariableName()}> {expectedVariableName}";
Assert.IsTrue(methodBodyString.Contains(expectedDeclaration, StringComparison.InvariantCultureIgnoreCase));
}

// validate the additional properties value kind check statements
foreach (var expectedValueKindCheck in expectedValueKindChecks)
{
Assert.IsTrue(methodBodyString.Contains(expectedValueKindCheck));
}

// validate return statement
if (expectedValueTypeNames.Length > 1)
{
// skip the first value type name as it is already included in the return statement
var additionalPropertiesVariables = "additionalProperties, " + string.Join(", ", expectedValueTypeNames.Skip(1).Select(v => $"additional{v.ToCleanName()}Properties,"));
var expectedReturnStatement = $"return new global::sample.namespace.Models.Cat(color, {additionalPropertiesVariables} additionalBinaryDataProperties);";
Assert.IsTrue(methodBodyString.Contains(expectedReturnStatement));
}
else
{
Assert.IsTrue(methodBodyString.Contains("return new global::sample.namespace.Models.Cat(color, additionalProperties, additionalBinaryDataProperties);"));
}
}

[TestCaseSource(nameof(TestBuildJsonModelWriteCoreTestCases))]
public void TestBuildJsonModelWriteCore(
InputType additionalPropsValueType)
{
var inputModel = InputFactory.Model("cat",
properties:
[
InputFactory.Property("color", InputPrimitiveType.String, isRequired: true),
],
additionalProperties: additionalPropsValueType);
MockHelpers.LoadMockPlugin(inputModels: () => [inputModel]);

var model = ClientModelPlugin.Instance.TypeFactory.CreateModel(inputModel);
var serializations = model!.SerializationProviders.FirstOrDefault() as MrwSerializationTypeDefinition;
Assert.IsNotNull(serializations);

var writeCoreMethod = serializations!.BuildJsonModelWriteCoreMethod();

Assert.IsNotNull(writeCoreMethod);
var methodBody = writeCoreMethod?.BodyStatements;
Assert.IsNotNull(methodBody);

var methodBodyString = methodBody!.ToDisplayString();
var additionalPropertiesProps = model.Properties.Where(p => p.BackingField != null && p.Name.StartsWith("Additional")).ToList();

// validate each additional property is serialized
foreach (var additionalProperty in additionalPropertiesProps)
{
var expectedSerializationStatement = $"foreach (var item in {additionalProperty.Name})";
Assert.IsTrue(methodBodyString.Contains(expectedSerializationStatement));
}
}

public static IEnumerable<TestCaseData> TestBuildDeserializationMethodTestCases
{
get
{
// string additional properties
yield return new TestCaseData(
InputPrimitiveType.String,
new string[] { "string" },
new string[] { "case global::System.Text.Json.JsonValueKind.String:", "additionalProperties.Add(prop.Name, prop.Value.GetString());" });
// bool additional properties
yield return new TestCaseData(
InputPrimitiveType.Boolean,
new string[] { "bool" },
new string[]
{
"case (global::System.Text.Json.JsonValueKind.True || global::System.Text.Json.JsonValueKind.False):",
"additionalProperties.Add(prop.Name, prop.Value.GetBoolean());"
});
// float additional properties
yield return new TestCaseData(
InputPrimitiveType.Float32,
new string[] { "float" },
new string[]
{
"case global::System.Text.Json.JsonValueKind.Number:",
"if (prop.Value.TryGetSingle(out float single))",
"additionalProperties.Add(prop.Name, single);"
});
// union additional properties
yield return new TestCaseData(
new InputUnionType("union", [InputPrimitiveType.String, InputPrimitiveType.Float64]),
new string[] { "string", "double" },
new string[]
{
"case global::System.Text.Json.JsonValueKind.String:",
"additionalProperties.Add(prop.Name, prop.Value.GetString());",
"case global::System.Text.Json.JsonValueKind.Number:",
"if (prop.Value.TryGetDouble(out double @double))",
"additionalDoubleProperties.Add(prop.Name, @double);"
});
}
}

public static IEnumerable<TestCaseData> TestBuildJsonModelWriteCoreTestCases
{
get
{
// string additional properties
yield return new TestCaseData(InputPrimitiveType.String);
// bool additional properties
yield return new TestCaseData(InputPrimitiveType.Boolean);
// float additional properties
yield return new TestCaseData(InputPrimitiveType.Float32);
// union additional properties
yield return new TestCaseData(new InputUnionType("union", [InputPrimitiveType.String, InputPrimitiveType.Float64]));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Generator.CSharp.Input;
using Microsoft.Generator.CSharp.Primitives;
using Microsoft.Generator.CSharp.Providers;
using Microsoft.Generator.CSharp.Statements;
using Microsoft.Generator.CSharp.Tests.Common;
Expand Down Expand Up @@ -163,9 +164,13 @@ public void DerivedShouldPassLiteralForKindToBase()
var outputLibrary = ClientModelPlugin.Instance.OutputLibrary;
var catModel = outputLibrary.TypeProviders.OfType<ModelProvider>().FirstOrDefault(t => t.Name == "Cat");
Assert.IsNotNull(catModel);
Assert.IsNotNull(catModel!.FullConstructor.Signature.Initializer);
Assert.IsFalse(catModel.FullConstructor.Signature.Initializer!.Arguments.Any(a => a.ToDisplayString().Contains("kind")));
Assert.IsTrue(catModel.FullConstructor.Signature.Initializer!.Arguments.Any(a => a.ToDisplayString() == "\"cat\""));
var publicCtor = catModel!.Constructors.FirstOrDefault(c => c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public));
Assert.IsNotNull(publicCtor);

var initializer = publicCtor!.Signature.Initializer;
Assert.IsNotNull(initializer);
Assert.IsFalse(initializer!.Arguments.Any(a => a.ToDisplayString().Contains("kind")));
Assert.IsTrue(initializer!.Arguments.Any(a => a.ToDisplayString() == "\"cat\""));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,17 @@ public void CamelCaseSerializedName()
var file = writer.Write();
Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
}

[Test]
public void MultipleAdditionalProperties()
{
var inputModel = InputFactory.Model("TestModel", properties: [InputFactory.Property("color", InputPrimitiveType.String, isRequired: true)],
additionalProperties: new InputUnionType("union", [InputPrimitiveType.String, InputPrimitiveType.Float64]));

var mrwProvider = new ModelProvider(inputModel).SerializationProviders.First();
var writer = new TypeProviderWriter(mrwProvider);
var file = writer.Write();
Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// <auto-generated/>

#nullable disable

using System;
using System.ClientModel.Primitives;
using System.Text.Json;

namespace sample.namespace.Models
{
/// <summary></summary>
public partial class TestModel : global::System.ClientModel.Primitives.IJsonModel<global::sample.namespace.Models.TestModel>
{
/// <param name="writer"> The JSON writer. </param>
/// <param name="options"> The client options for reading and writing models. </param>
protected virtual void JsonModelWriteCore(global::System.Text.Json.Utf8JsonWriter writer, global::System.ClientModel.Primitives.ModelReaderWriterOptions options)
{
string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel<global::sample.namespace.Models.TestModel>)this).GetFormatFromOptions(options) : options.Format;
if ((format != "J"))
{
throw new global::System.FormatException($"The model {nameof(global::sample.namespace.Models.TestModel)} does not support writing '{format}' format.");
}
writer.WritePropertyName("color"u8);
writer.WriteStringValue(Color);
foreach (var item in AdditionalProperties)
{
writer.WritePropertyName(item.Key);
writer.WriteStringValue(item.Value);
}
foreach (var item in AdditionalDoubleProperties)
{
writer.WritePropertyName(item.Key);
writer.WriteNumberValue(item.Value);
}
if (((options.Format != "W") && (_additionalBinaryDataProperties != null)))
{
foreach (var item in _additionalBinaryDataProperties)
{
writer.WritePropertyName(item.Key);
#if NET6_0_OR_GREATER
writer.WriteRawValue(item.Value);
#else
using (global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(item.Value))
{
global::System.Text.Json.JsonSerializer.Serialize(writer, document.RootElement);
}
#endif
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@
"commandName": "Executable",
"executablePath": "$(SolutionDir)/../dist/generator/Microsoft.Generator.CSharp.exe"
},
"http-type-property-additional-properties": {
"commandLineArgs": "$(SolutionDir)/TestProjects/CadlRanch/http/type/property/additional-properties -p StubLibraryPlugin",
"commandName": "Executable",
"executablePath": "$(SolutionDir)/../dist/generator/Microsoft.Generator.CSharp.exe"
},
"http-type-property-nullable": {
"commandLineArgs": "$(SolutionDir)/TestProjects/CadlRanch/http/type/property/nullable -p StubLibraryPlugin",
"commandName": "Executable",
Expand Down
Loading

0 comments on commit 8bbc09f

Please sign in to comment.