Skip to content

Commit

Permalink
Support serialization of custom property (#4532)
Browse files Browse the repository at this point in the history
Fixes #4266
  • Loading branch information
JoshLove-msft authored Sep 25, 2024
1 parent 3d80941 commit 6fbd3c3
Show file tree
Hide file tree
Showing 12 changed files with 322 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1241,11 +1241,10 @@ private static MethodBodyStatement ThrowValidationFailException(ValueExpression
/// </summary>
private MethodBodyStatement[] CreateWritePropertiesStatements()
{
var propertyCount = _model.Properties.Count;
List<MethodBodyStatement> propertyStatements = new(propertyCount);
for (int i = 0; i < propertyCount; i++)
var properties = _model.Properties.Concat(_model.CustomCodeView?.Properties.Where(p => p.WireInfo != null) ?? []);
List<MethodBodyStatement> propertyStatements = new();
foreach (var property in properties)
{
var property = _model.Properties[i];
// we should only write those properties with a wire info. Those properties without wireinfo indicate they are not spec properties.
if (property.WireInfo is not { } wireInfo)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Linq;
using System.Threading.Tasks;
using Microsoft.Generator.CSharp.ClientModel.Providers;
using Microsoft.Generator.CSharp.Input;
using Microsoft.Generator.CSharp.Primitives;
using Microsoft.Generator.CSharp.Providers;
using Microsoft.Generator.CSharp.Tests.Common;
using NUnit.Framework;

namespace Microsoft.Generator.CSharp.ClientModel.Tests.Providers.MrwSerializationTypeDefinitions
{
public class ModelCustomizationTests
{
[Test]
public async Task CanChangePropertyName()
{
var props = new[]
{
InputFactory.Property("Prop1", InputFactory.Array(InputPrimitiveType.String))
};

var inputModel = InputFactory.Model("mockInputModel", properties: props, usage: InputModelTypeUsage.Json);
var plugin = await MockHelpers.LoadMockPluginAsync(
inputModels: () => [inputModel],
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var modelProvider = plugin.Object.OutputLibrary.TypeProviders.Single(t => t is ModelProvider);
var serializationProvider = modelProvider.SerializationProviders.Single(t => t is MrwSerializationTypeDefinition);
Assert.IsNotNull(serializationProvider);
Assert.AreEqual(0, serializationProvider!.Fields.Count);

// validate the methods use the custom member name
var writer = new TypeProviderWriter(modelProvider);
var file = writer.Write();
var expected = Helpers.GetExpectedFromFile();
Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Linq;
using System.Threading.Tasks;
using Microsoft.Generator.CSharp.ClientModel.Providers;
using Microsoft.Generator.CSharp.Input;
using Microsoft.Generator.CSharp.Primitives;
using Microsoft.Generator.CSharp.Providers;
using Microsoft.Generator.CSharp.Tests.Common;
using NUnit.Framework;

namespace Microsoft.Generator.CSharp.ClientModel.Tests.Providers.MrwSerializationTypeDefinitions
{
public class SerializationCustomizationTests
{
[Test]
public async Task CanChangePropertyName()
{
var props = new[]
{
InputFactory.Property("Prop1", InputFactory.Array(InputPrimitiveType.String))
};

var inputModel = InputFactory.Model("mockInputModel", properties: props, usage: InputModelTypeUsage.Json);
var plugin = await MockHelpers.LoadMockPluginAsync(
inputModels: () => [inputModel],
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var modelProvider = plugin.Object.OutputLibrary.TypeProviders.Single(t => t is ModelProvider);
var serializationProvider = modelProvider.SerializationProviders.Single(t => t is MrwSerializationTypeDefinition);
Assert.IsNotNull(serializationProvider);
Assert.AreEqual(0, serializationProvider!.Fields.Count);

// validate the methods use the custom member name
var writer = new TypeProviderWriter(serializationProvider);
var file = writer.Write();
Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// <auto-generated/>

#nullable disable

using System;
using System.Collections.Generic;

namespace Sample.Models
{
/// <summary> mockInputModel description. </summary>
public partial class MockInputModel
{
/// <summary> Keeps track of any properties unknown to the library. </summary>
private protected global::System.Collections.Generic.IDictionary<string, global::System.BinaryData> _additionalBinaryDataProperties;

internal MockInputModel()
{
}

internal MockInputModel(global::System.String[] prop2, global::System.Collections.Generic.IDictionary<string, global::System.BinaryData> additionalBinaryDataProperties)
{
Prop2 = prop2;
_additionalBinaryDataProperties = additionalBinaryDataProperties;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#nullable disable

using Microsoft.Generator.CSharp.Customization;

namespace Sample.Models
{
public partial class MockInputModel
{
[CodeGenMember("Prop1")]
public string[] Prop2 { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// <auto-generated/>

#nullable disable

using System;
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Text.Json;
using Sample;

namespace Sample.Models
{
/// <summary></summary>
public partial class MockInputModel : global::System.ClientModel.Primitives.IJsonModel<global::Sample.Models.MockInputModel>
{
void global::System.ClientModel.Primitives.IJsonModel<global::Sample.Models.MockInputModel>.Write(global::System.Text.Json.Utf8JsonWriter writer, global::System.ClientModel.Primitives.ModelReaderWriterOptions options)
{
writer.WriteStartObject();
this.JsonModelWriteCore(writer, options);
writer.WriteEndObject();
}

/// <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.Models.MockInputModel>)this).GetFormatFromOptions(options) : options.Format;
if ((format != "J"))
{
throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support writing '{format}' format.");
}
if (global::Sample.Optional.IsDefined(Prop2))
{
writer.WritePropertyName("prop1"u8);
writer.WriteStartArray();
foreach (var item in Prop2)
{
if ((item == null))
{
writer.WriteNullValue();
continue;
}
writer.WriteStringValue(item);
}
writer.WriteEndArray();
}
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
}
}
}

global::Sample.Models.MockInputModel global::System.ClientModel.Primitives.IJsonModel<global::Sample.Models.MockInputModel>.Create(ref global::System.Text.Json.Utf8JsonReader reader, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => ((global::Sample.Models.MockInputModel)this.JsonModelCreateCore(ref reader, options));

/// <param name="reader"> The JSON reader. </param>
/// <param name="options"> The client options for reading and writing models. </param>
protected virtual global::Sample.Models.MockInputModel JsonModelCreateCore(ref global::System.Text.Json.Utf8JsonReader reader, global::System.ClientModel.Primitives.ModelReaderWriterOptions options)
{
string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel<global::Sample.Models.MockInputModel>)this).GetFormatFromOptions(options) : options.Format;
if ((format != "J"))
{
throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support reading '{format}' format.");
}
using global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.ParseValue(ref reader);
return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, options);
}

internal static global::Sample.Models.MockInputModel DeserializeMockInputModel(global::System.Text.Json.JsonElement element, global::System.ClientModel.Primitives.ModelReaderWriterOptions options)
{
if ((element.ValueKind == global::System.Text.Json.JsonValueKind.Null))
{
return null;
}
global::System.String[] prop2 = default;
global::System.Collections.Generic.IDictionary<string, global::System.BinaryData> additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary<string, global::System.BinaryData>();
foreach (var prop in element.EnumerateObject())
{
if (prop.NameEquals("prop1"u8))
{
if ((prop.Value.ValueKind == global::System.Text.Json.JsonValueKind.Null))
{
continue;
}
global::System.Collections.Generic.List<string> array = new global::System.Collections.Generic.List<string>();
foreach (var item in prop.Value.EnumerateArray())
{
if ((item.ValueKind == global::System.Text.Json.JsonValueKind.Null))
{
array.Add(null);
}
else
{
array.Add(item.GetString());
}
}
prop2 = array;
continue;
}
if ((options.Format != "W"))
{
additionalBinaryDataProperties.Add(prop.Name, global::System.BinaryData.FromString(prop.Value.GetRawText()));
}
}
return new global::Sample.Models.MockInputModel(prop2, additionalBinaryDataProperties);
}

global::System.BinaryData global::System.ClientModel.Primitives.IPersistableModel<global::Sample.Models.MockInputModel>.Write(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => this.PersistableModelWriteCore(options);

/// <param name="options"> The client options for reading and writing models. </param>
protected virtual global::System.BinaryData PersistableModelWriteCore(global::System.ClientModel.Primitives.ModelReaderWriterOptions options)
{
string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel<global::Sample.Models.MockInputModel>)this).GetFormatFromOptions(options) : options.Format;
switch (format)
{
case "J":
return global::System.ClientModel.Primitives.ModelReaderWriter.Write(this, options);
default:
throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support writing '{options.Format}' format.");
}
}

global::Sample.Models.MockInputModel global::System.ClientModel.Primitives.IPersistableModel<global::Sample.Models.MockInputModel>.Create(global::System.BinaryData data, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => ((global::Sample.Models.MockInputModel)this.PersistableModelCreateCore(data, options));

/// <param name="data"> The data to parse. </param>
/// <param name="options"> The client options for reading and writing models. </param>
protected virtual global::Sample.Models.MockInputModel PersistableModelCreateCore(global::System.BinaryData data, global::System.ClientModel.Primitives.ModelReaderWriterOptions options)
{
string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel<global::Sample.Models.MockInputModel>)this).GetFormatFromOptions(options) : options.Format;
switch (format)
{
case "J":
using (global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(data))
{
return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, options);
}
default:
throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support reading '{options.Format}' format.");
}
}

string global::System.ClientModel.Primitives.IPersistableModel<global::Sample.Models.MockInputModel>.GetFormatFromOptions(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => "J";

/// <param name="mockInputModel"> The <see cref="global::Sample.Models.MockInputModel"/> to serialize into <see cref="global::System.ClientModel.BinaryContent"/>. </param>
public static implicit operator BinaryContent(global::Sample.Models.MockInputModel mockInputModel)
{
return global::System.ClientModel.BinaryContent.Create(mockInputModel, global::Sample.ModelSerializationExtensions.WireOptions);
}

/// <param name="result"> The <see cref="global::System.ClientModel.ClientResult"/> to deserialize the <see cref="global::Sample.Models.MockInputModel"/> from. </param>
public static explicit operator MockInputModel(global::System.ClientModel.ClientResult result)
{
using global::System.ClientModel.Primitives.PipelineResponse response = result.GetRawResponse();
using global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(response.Content);
return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, global::Sample.ModelSerializationExtensions.WireOptions);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#nullable disable

using Microsoft.Generator.CSharp.Customization;

namespace Sample.Models
{
public partial class MockInputModel
{
[CodeGenMember("Prop1")]
public string[] Prop2 { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ internal static class MockHelpers

public static async Task<Mock<ClientModelPlugin>> LoadMockPluginAsync(
Func<IReadOnlyList<InputEnumType>>? inputEnums = null,
Func<IReadOnlyList<InputModelType>>? inputModels = null,
Func<Task<Compilation>>? compilation = null)
{
var mockPlugin = LoadMockPlugin(inputEnums: inputEnums);
var mockPlugin = LoadMockPlugin(inputEnums: inputEnums, inputModels: inputModels);

var compilationResult = compilation == null ? null : await compilation();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ public ModelProvider? BaseModelProvider
internal bool SupportsBinaryDataAdditionalProperties => AdditionalPropertyProperties.Any(p => p.Type.ElementType.Equals(_additionalPropsUnknownType));
public ConstructorProvider FullConstructor => _fullConstructor ??= BuildFullConstructor();

internal IReadOnlyList<PropertyProvider> AllSpecProperties => Properties.Concat(CustomCodeView?.Properties.Where(p => p.WireInfo != null) ?? []).ToList();

protected override string GetNamespace() => CodeModelPlugin.Instance.Configuration.ModelNamespace;

protected override CSharpType? GetBaseType()
Expand Down Expand Up @@ -465,7 +467,7 @@ private ConstructorProvider BuildFullConstructor()
baseParameters.AddRange(BaseModelProvider.FullConstructor.Signature.Parameters);
}

HashSet<PropertyProvider> overriddenProperties = Properties.Where(p => p.BaseProperty is not null).Select(p => p.BaseProperty!).ToHashSet();
HashSet<PropertyProvider> overriddenProperties = AllSpecProperties.Where(p => p.BaseProperty is not null).Select(p => p.BaseProperty!).ToHashSet();

// add the base parameters, if any
foreach (var property in baseProperties)
Expand All @@ -476,7 +478,7 @@ private ConstructorProvider BuildFullConstructor()
// construct the initializer using the parameters from base signature
var constructorInitializer = new ConstructorInitializer(true, [.. baseParameters.Select(p => GetExpressionForCtor(p, overriddenProperties, isPrimaryConstructor))]);

foreach (var property in Properties)
foreach (var property in AllSpecProperties)
{
AddInitializationParameterForCtor(constructorParameters, property, Type.IsStruct, isPrimaryConstructor);
}
Expand Down Expand Up @@ -603,7 +605,7 @@ private MethodBodyStatement GetPropertyInitializers(
List<MethodBodyStatement> methodBodyStatements = new(Properties.Count + 1);
Dictionary<string, ParameterProvider> parameterMap = parameters?.ToDictionary(p => p.Name) ?? [];

foreach (var property in Properties)
foreach (var property in AllSpecProperties)
{
// skip those non-spec properties
if (property.WireInfo == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class PropertyProvider
public PropertyBody Body { get; internal set; }
public CSharpType? ExplicitInterface { get; }
public XmlDocProvider XmlDocs { get; private set; }
public PropertyWireInformation? WireInfo { get; }
public PropertyWireInformation? WireInfo { get; internal set; }
public bool IsDiscriminator { get; }
public bool IsAdditionalProperties { get; init; }

Expand All @@ -44,8 +44,6 @@ public class PropertyProvider

internal IEnumerable<AttributeData>? Attributes { get; init; }

internal PropertyProvider? SpecProperty { get; set; }

// for mocking
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
protected PropertyProvider()
Expand Down
Loading

0 comments on commit 6fbd3c3

Please sign in to comment.