Skip to content

Commit

Permalink
Support customizing prop wire name (#4591)
Browse files Browse the repository at this point in the history
This PR adds support for customizing a model property's serialization
name via the `CodeGenSerialization` attribute.

supports: #4264
  • Loading branch information
jorgerangel-msft authored Oct 4, 2024
1 parent 56c3d62 commit b2dc525
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,49 @@ public async Task CanCustomizeSerializationMethodForRenamedProperty()
var file = writer.Write();
Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
}

// Validates that a properties serialization name can be changed using custom code.
[Test]
public async Task CanChangePropertySerializedName()
{
var props = new[]
{
InputFactory.Property("Name", InputPrimitiveType.String),
InputFactory.Property("Color", InputPrimitiveType.String),
InputFactory.Property("Flavor", 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(modelProvider);
Assert.IsNotNull(serializationProvider);

var properties = modelProvider.Properties;
Assert.AreEqual(2, properties.Count);
Assert.AreEqual("Name", properties[0].Name);
Assert.AreEqual("customName", properties[0].WireInfo?.SerializedName);
Assert.AreEqual("Flavor", properties[1].Name);
Assert.AreEqual("flavor", properties[1].WireInfo?.SerializedName);


var customCodeView = modelProvider.CustomCodeView;
Assert.IsNotNull(customCodeView);
var customProperties = customCodeView!.Properties;
Assert.AreEqual(1, customProperties.Count);
Assert.AreEqual("CustomColor", customProperties[0].Name);
Assert.AreEqual("customColor2", customProperties[0].WireInfo?.SerializedName);

// validate the serialization provider uses the custom property name
var writer = new TypeProviderWriter(serializationProvider);
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,180 @@
// <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(Name))
{
writer.WritePropertyName("customName"u8);
writer.WriteStringValue(Name);
}
if (global::Sample.Optional.IsDefined(Flavor))
{
writer.WritePropertyName("flavor"u8);
writer.WriteStringValue(Flavor);
}
if (global::Sample.Optional.IsDefined(CustomColor))
{
writer.WritePropertyName("customColor2"u8);
writer.WriteStringValue(CustomColor);
}
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;
}
string name = default;
string flavor = default;
string customColor = 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("customName"u8))
{
if ((prop.Value.ValueKind == global::System.Text.Json.JsonValueKind.Null))
{
name = null;
continue;
}
name = prop.Value.GetString();
continue;
}
if (prop.NameEquals("flavor"u8))
{
if ((prop.Value.ValueKind == global::System.Text.Json.JsonValueKind.Null))
{
flavor = null;
continue;
}
flavor = prop.Value.GetString();
continue;
}
if (prop.NameEquals("customColor2"u8))
{
if ((prop.Value.ValueKind == global::System.Text.Json.JsonValueKind.Null))
{
customColor = null;
continue;
}
customColor = prop.Value.GetString();
continue;
}
if ((options.Format != "W"))
{
additionalBinaryDataProperties.Add(prop.Name, global::System.BinaryData.FromString(prop.Value.GetRawText()));
}
}
return new global::Sample.Models.MockInputModel(name, flavor, customColor, 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,14 @@
#nullable disable

using Microsoft.Generator.CSharp.Customization;

namespace Sample.Models
{
[CodeGenSerialization(nameof(Name), "customName")]
[CodeGenSerialization(nameof(CustomColor), "customColor2")]
public partial class MockInputModel
{
[CodeGenMember("Color")]
public string CustomColor { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ namespace Microsoft.Generator.CSharp.Customization
public class CodeGenSerializationAttribute : Attribute
{
/// <summary>
/// Gets or sets the property name which these hooks should apply to
/// Gets or sets the property name which these hooks should apply to.
/// </summary>
public string? PropertyName { get; set; }
/// <summary>
/// Gets or sets the serialization path of the property in the JSON
/// Gets or sets the serialization name of the property.
/// </summary>
public string[]? SerializationPath { get; }
public string? PropertySerializationName { get; }
/// <summary>
/// Gets or sets the method name to use when serializing the property value (property name excluded)
/// The signature of the serialization hook method must be or compatible with when invoking:
Expand Down Expand Up @@ -45,13 +45,7 @@ public CodeGenSerializationAttribute(string propertyName)
public CodeGenSerializationAttribute(string propertyName, string serializationName)
{
PropertyName = propertyName;
SerializationPath = new[] { serializationName };
}

public CodeGenSerializationAttribute(string propertyName, string[] serializationPath)
{
PropertyName = propertyName;
SerializationPath = serializationPath;
PropertySerializationName = serializationName;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Microsoft.Generator.CSharp.Primitives
public class WireInformation
{
public SerializationFormat SerializationFormat { get; }
public string SerializedName { get; }
public string SerializedName { get; internal set; }

public WireInformation(SerializationFormat serializationFormat, string serializedName)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,36 @@ private bool ShouldGenerate(PropertyProvider property, IDictionary<string, Prope
}
}

string? serializedName = null;
if (property.WireInfo != null)
{
bool containsRenamedProperty = renamedProperties.TryGetValue(property.Name, out PropertyProvider? renamedProp);
foreach (var attribute in GetCodeGenSerializationAttributes())
{
if (CodeGenAttributes.TryGetCodeGenSerializationAttributeValue(
attribute,
out var propertyName,
out string? serializationName,
out _,
out _,
out _) && serializationName != null)
{
if (propertyName == property.Name
|| (containsRenamedProperty && renamedProp != null && propertyName == renamedProp.Name))
{
serializedName = serializationName;
break;
}
}
}

// replace original property serialization name.
if (serializedName != null)
{
property.WireInfo.SerializedName = serializedName;
}
}

if (renamedProperties.TryGetValue(property.Name, out PropertyProvider? customProp) ||
customProperties.TryGetValue(property.Name, out customProp))
{
Expand Down Expand Up @@ -489,5 +519,7 @@ private static FileLinePositionSpan GetFileLinePosition(SyntaxReference? syntaxR

private IEnumerable<AttributeData> GetMemberSuppressionAttributes()
=> CustomCodeView?.GetAttributes()?.Where(a => a.AttributeClass?.Name == CodeGenAttributes.CodeGenSuppressAttributeName) ?? [];
private IEnumerable<AttributeData> GetCodeGenSerializationAttributes()
=> CustomCodeView?.GetAttributes()?.Where(a => a.AttributeClass?.Name == CodeGenAttributes.CodeGenSerializationAttributeName) ?? [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ public static bool TryGetCodeGenMemberAttributeValue(AttributeData attributeData
return name != null;
}

public static bool TryGetCodeGenSerializationAttributeValue(AttributeData attributeData, [MaybeNullWhen(false)] out string propertyName, out IReadOnlyList<string>? serializationNames, out string? serializationHook, out string? deserializationHook, out string? bicepSerializationHook)
public static bool TryGetCodeGenSerializationAttributeValue(AttributeData attributeData, [MaybeNullWhen(false)] out string propertyName, out string? serializationName, out string? serializationHook, out string? deserializationHook, out string? bicepSerializationHook)
{
propertyName = null;
serializationNames = null;
serializationName = null;
serializationHook = null;
deserializationHook = null;
bicepSerializationHook = null;
Expand All @@ -53,20 +53,19 @@ public static bool TryGetCodeGenSerializationAttributeValue(AttributeData attrib
if (ctorArgs.Length > 1)
{
var namesArg = ctorArgs[1];
serializationNames = namesArg.Kind switch
serializationName = namesArg.Kind switch
{
TypedConstantKind.Array => ToStringArray(namesArg.Values),
_ when namesArg.IsNull => null,
_ => new string[] { namesArg.Value?.ToString()! }
_ => namesArg.Value?.ToString()!
};
}

foreach (var (key, namedArgument) in attributeData.NamedArguments)
{
switch (key)
{
case nameof(CodeGenSerializationAttribute.SerializationPath):
serializationNames = ToStringArray(namedArgument.Values);
case nameof(CodeGenSerializationAttribute.PropertySerializationName):
serializationName = namedArgument.Value as string;
break;
case nameof(CodeGenSerializationAttribute.SerializationValueHook):
serializationHook = namedArgument.Value as string;
Expand All @@ -77,7 +76,7 @@ public static bool TryGetCodeGenSerializationAttributeValue(AttributeData attrib
}
}

return propertyName != null && (serializationNames != null || serializationHook != null || deserializationHook != null || bicepSerializationHook != null);
return propertyName != null && (serializationName != null || serializationHook != null || deserializationHook != null || bicepSerializationHook != null);
}

public static bool TryGetCodeGenModelAttributeValue(AttributeData attributeData, out string[]? usage, out string[]? formats)
Expand Down

0 comments on commit b2dc525

Please sign in to comment.