Skip to content

Commit

Permalink
Support serializing fields (#4746)
Browse files Browse the repository at this point in the history
Fixes #4638

Also fixes serialization for literal extensible enums (enums with one
default literal value).
  • Loading branch information
JoshLove-msft authored Oct 16, 2024
1 parent 1267f51 commit 518ac2b
Show file tree
Hide file tree
Showing 21 changed files with 1,015 additions and 201 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -733,24 +733,29 @@ private List<MethodBodyStatement> BuildDeserializePropertiesStatements(ScopedApi
for (int i = 0; i < parameters.Count; i++)
{
var parameter = parameters[i];
if (parameter.Property is { } property)
if (parameter.Property != null || parameter.Field != null)
{
// handle additional properties
if (property != _additionalBinaryDataProperty && property.IsAdditionalProperties)
if (parameter.Property != null && parameter.Property != _additionalBinaryDataProperty && parameter.Property.IsAdditionalProperties)
{
AddAdditionalPropertiesValueKindStatements(additionalPropsValueKindBodyStatements, property, jsonProperty);
AddAdditionalPropertiesValueKindStatements(additionalPropsValueKindBodyStatements, parameter.Property, jsonProperty);
continue;
}

var wireInfo = parameter.Property?.WireInfo ?? parameter.Field?.WireInfo;

// By default, we should only deserialize properties with wire info. Those properties without wire info indicate they are not spec properties.
if (property.WireInfo is not { } wireInfo)
if (wireInfo == null)
{
continue;
}
var propertySerializationName = wireInfo.SerializedName;
var propertyName = parameter.Property?.Name ?? parameter.Field?.Name;
var propertyType = parameter.Property?.Type ?? parameter.Field?.Type;
var propertyExpression = parameter.Property?.AsVariableExpression ?? parameter.Field?.AsVariableExpression;
var checkIfJsonPropEqualsName = new IfStatement(jsonProperty.NameEquals(propertySerializationName))
{
DeserializeProperty(property, jsonProperty, serializationAttributes)
DeserializeProperty(propertyName!, propertyType!, wireInfo, propertyExpression!, jsonProperty, serializationAttributes)
};
propertyDeserializationStatements.Add(checkIfJsonPropEqualsName);
}
Expand Down Expand Up @@ -1051,15 +1056,18 @@ private static SwitchStatement CreateDeserializeAdditionalPropsValueKindCheck(
}

private MethodBodyStatement[] DeserializeProperty(
PropertyProvider property,
string propertyName,
CSharpType propertyType,
PropertyWireInformation wireInfo,
VariableExpression variableExpression,
ScopedApi<JsonProperty> jsonProperty,
IEnumerable<AttributeData> serializationAttributes)
{
var serializationFormat = property.WireInfo?.SerializationFormat ?? SerializationFormat.Default;
var propertyVarReference = property.AsVariableExpression;
var serializationFormat = wireInfo.SerializationFormat;
var propertyVarReference = variableExpression;
var deserializationStatements = new MethodBodyStatement[2]
{
DeserializeValue(property.Type, jsonProperty.Value(), serializationFormat, out ValueExpression value),
DeserializeValue(propertyType, jsonProperty.Value(), serializationFormat, out ValueExpression value),
propertyVarReference.Assign(value).Terminate()
};

Expand All @@ -1071,7 +1079,7 @@ private MethodBodyStatement[] DeserializeProperty(
out _,
out _,
out var deserializationHook,
out _) && name == property.Name && deserializationHook != null)
out _) && name == propertyName && deserializationHook != null)
{
deserializationStatements =
[Static().Invoke(
Expand All @@ -1084,7 +1092,7 @@ private MethodBodyStatement[] DeserializeProperty(

return
[
DeserializationPropertyNullCheckStatement(property, jsonProperty, propertyVarReference),
DeserializationPropertyNullCheckStatement(propertyType, wireInfo, jsonProperty, propertyVarReference),
deserializationStatements,
Continue
];
Expand All @@ -1097,14 +1105,15 @@ private MethodBodyStatement[] DeserializeProperty(
/// return a null check for the json property.
/// </summary>
private static MethodBodyStatement DeserializationPropertyNullCheckStatement(
PropertyProvider property,
CSharpType propertyType,
PropertyWireInformation wireInfo,
ScopedApi<JsonProperty> jsonProperty,
VariableExpression propertyVarRef)
{
// Produces: if (prop.Value.ValueKind == System.Text.Json.JsonValueKind.Null)
var checkEmptyProperty = jsonProperty.Value().ValueKindEqualsNull();
CSharpType serializedType = property.Type;
var propertyIsRequired = property.WireInfo?.IsRequired ?? false;
CSharpType serializedType = propertyType;
var propertyIsRequired = wireInfo.IsRequired;

if (serializedType.IsNullable || !propertyIsRequired)
{
Expand Down Expand Up @@ -1311,89 +1320,110 @@ private static MethodBodyStatement ThrowValidationFailException(ValueExpression
/// </summary>
private MethodBodyStatement[] CreateWritePropertiesStatements()
{
var properties = _model.CanonicalView.Properties;
List<MethodBodyStatement> propertyStatements = new();
foreach (var property in properties)
foreach (var property in _model.CanonicalView.Properties)
{
// 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)
if (property.WireInfo == null)
{
continue;
}
var propertySerializationName = wireInfo.SerializedName;
var propertySerializationFormat = wireInfo.SerializationFormat;
var propertyIsReadOnly = wireInfo.IsReadOnly;
var propertyIsRequired = wireInfo.IsRequired;
var propertyIsNullable = wireInfo.IsNullable;

// Generate the serialization statements for the property
var serializationStatement = CreateSerializationStatement(property.Type, property, propertySerializationFormat);

// Check for custom serialization hooks
foreach (var attribute in _model.CustomCodeView?.GetAttributes()
.Where(a => a.AttributeClass?.Name == CodeGenAttributes.CodeGenSerializationAttributeName) ?? [])

propertyStatements.Add(CreateWritePropertyStatement(property.WireInfo, property.Type, property.Name, property));
}

foreach (var field in _model.CanonicalView.Fields)
{
// we should only write those properties with a wire info. Those properties without wireinfo indicate they are not spec properties.
if (field.WireInfo == null)
{
if (CodeGenAttributes.TryGetCodeGenSerializationAttributeValue(
attribute,
out var name,
out _,
out var serializationHook,
out _,
out _) && name == property.Name && serializationHook != null)
{
serializationStatement = This.Invoke(
serializationHook,
_utf8JsonWriterSnippet,
_serializationOptionsParameter)
.Terminate();
}
continue;
}

var writePropertySerializationStatements = new MethodBodyStatement[]
{
_utf8JsonWriterSnippet.WritePropertyName(propertySerializationName),
serializationStatement
};
propertyStatements.Add(CreateWritePropertyStatement(field.WireInfo, field.Type, field.Name, field));
}

// Wrap the serialization statement in a check for whether the property is defined
var wrapInIsDefinedStatement = WrapInIsDefined(property, property, propertyIsRequired, propertyIsReadOnly, propertyIsNullable, writePropertySerializationStatements);
if (propertyIsReadOnly && wrapInIsDefinedStatement is not IfStatement)
return [.. propertyStatements];
}

private MethodBodyStatement CreateWritePropertyStatement(
PropertyWireInformation wireInfo,
CSharpType propertyType,
string propertyName,
MemberExpression propertyExpression)
{
var propertySerializationName = wireInfo.SerializedName;
var propertySerializationFormat = wireInfo.SerializationFormat;
var propertyIsReadOnly = wireInfo.IsReadOnly;
var propertyIsRequired = wireInfo.IsRequired;
var propertyIsNullable = wireInfo.IsNullable;

// Generate the serialization statements for the property
var serializationStatement = CreateSerializationStatement(propertyType, propertyExpression, propertySerializationFormat);

// Check for custom serialization hooks
foreach (var attribute in _model.CustomCodeView?.GetAttributes()
.Where(a => a.AttributeClass?.Name == CodeGenAttributes.CodeGenSerializationAttributeName) ?? [])
{
if (CodeGenAttributes.TryGetCodeGenSerializationAttributeValue(
attribute,
out var name,
out _,
out var serializationHook,
out _,
out _) && name == propertyName && serializationHook != null)
{
wrapInIsDefinedStatement = new IfStatement(_isNotEqualToWireConditionSnippet)
{
wrapInIsDefinedStatement
};
serializationStatement = This.Invoke(
serializationHook,
_utf8JsonWriterSnippet,
_serializationOptionsParameter)
.Terminate();
}
propertyStatements.Add(wrapInIsDefinedStatement);
}

return [.. propertyStatements];
var writePropertySerializationStatements = new MethodBodyStatement[]
{
_utf8JsonWriterSnippet.WritePropertyName(propertySerializationName),
serializationStatement
};

// Wrap the serialization statement in a check for whether the property is defined
var wrapInIsDefinedStatement = WrapInIsDefined(
propertyExpression,
propertyType,
wireInfo,
propertyIsRequired,
propertyIsReadOnly,
propertyIsNullable,
writePropertySerializationStatements);
if (propertyIsReadOnly && wrapInIsDefinedStatement is not IfStatement)
{
wrapInIsDefinedStatement = new IfStatement(_isNotEqualToWireConditionSnippet)
{
wrapInIsDefinedStatement
};
}

return wrapInIsDefinedStatement;
}

/// <summary>
/// Wraps the serialization statement in a condition check to ensure only initialized and required properties are serialized.
/// </summary>
/// <param name="propertyProvider">The model property.</param>
/// <param name="propertyMemberExpression">The expression representing the property to serialize.</param>
/// <param name="writePropertySerializationStatement">The serialization statement to conditionally execute.</param>
/// <returns>A method body statement that includes condition checks before serialization.</returns>
private MethodBodyStatement WrapInIsDefined(
PropertyProvider propertyProvider,
MemberExpression propertyMemberExpression,
MemberExpression propertyExpression,
CSharpType propertyType,
PropertyWireInformation wireInfo,
bool propertyIsRequired,
bool propertyIsReadOnly,
bool propertyIsNullable,
MethodBodyStatement writePropertySerializationStatement)
{
var propertyType = propertyProvider.Type;

// Create the first conditional statement to check if the property is defined
if (propertyIsNullable)
{
writePropertySerializationStatement = CheckPropertyIsInitialized(
propertyProvider,
propertyType,
wireInfo,
propertyIsRequired,
propertyMemberExpression,
propertyExpression,
writePropertySerializationStatement);
}

Expand All @@ -1404,18 +1434,16 @@ private MethodBodyStatement WrapInIsDefined(
}

// Conditionally serialize based on whether the property is a collection or a single value
return CreateConditionalSerializationStatement(propertyType, propertyMemberExpression, propertyIsReadOnly, writePropertySerializationStatement);
return CreateConditionalSerializationStatement(propertyType, propertyExpression, propertyIsReadOnly, writePropertySerializationStatement);
}

private IfElseStatement CheckPropertyIsInitialized(
PropertyProvider propertyProvider,
CSharpType propertyType,
PropertyWireInformation wireInfo,
bool isPropRequired,
MemberExpression propertyMemberExpression,
MethodBodyStatement writePropertySerializationStatement)
{
var propertyType = propertyProvider.Type;
var propertySerialization = propertyProvider.WireInfo;
var propertyName = propertySerialization?.SerializedName ?? propertyProvider.Name;
ScopedApi<bool> propertyIsInitialized;

if (propertyType.IsCollection && !propertyType.IsReadOnlyMemory && isPropRequired)
Expand All @@ -1431,7 +1459,7 @@ private IfElseStatement CheckPropertyIsInitialized(
return new IfElseStatement(
propertyIsInitialized,
writePropertySerializationStatement,
_utf8JsonWriterSnippet.WriteNull(propertyName.ToVariableName()));
_utf8JsonWriterSnippet.WriteNull(wireInfo.SerializedName.ToVariableName()));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,26 @@ public async Task CanSerializeCustomPropertyFromBase()
Assert.IsNotNull(fullCtor);
Assert.IsTrue(fullCtor!.Signature.Parameters.Any(p => p.Name == "prop1"));
}

[Test]
public async Task CanCustomizePropertyUsingField()
{
var props = new[]
{
InputFactory.Property("Prop1", 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);

Assert.AreEqual(0, modelProvider.Properties.Count);
var writer = new TypeProviderWriter(modelProvider);
var file = writer.Write();
Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,38 @@ public async Task CanCustomizeExtensibleEnum(InputPrimitiveType enumType)
Assert.AreEqual(Helpers.GetExpectedFromFile(enumType.Name), file.Content);
}

private static IEnumerable<TestCaseData> ExtensibleEnumCasesFromLiteral =>
[
new TestCaseData(InputPrimitiveType.String, "foo"),
new TestCaseData(InputPrimitiveType.Int32, 1),
];

[TestCaseSource(nameof(ExtensibleEnumCasesFromLiteral))]
public async Task CanCustomizeLiteralExtensibleEnum(InputPrimitiveType enumType, object value)
{
var props = new[]
{
InputFactory.Property("Prop1", InputFactory.Literal.Enum(
InputFactory.Enum("EnumType", enumType, isExtensible: true),
value: value))
};

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

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);

var writer = new TypeProviderWriter(serializationProvider);
var file = writer.Write();
Assert.AreEqual(Helpers.GetExpectedFromFile(parameters), file.Content);
}

[Test]
public async Task CanReplaceSerializationMethod()
{
Expand Down Expand Up @@ -299,5 +331,33 @@ public async Task CanReplaceDeserializationMethod()
var file = writer.Write();
Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
}

[TestCase(true)]
[TestCase(false)]
public async Task CanCustomizePropertyUsingField(bool redefineProperty)
{
var props = new[]
{
InputFactory.Property("Prop1", InputPrimitiveType.String),
};

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

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, modelProvider.Properties.Count);

var writer = new TypeProviderWriter(serializationProvider);
var file = writer.Write();
Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);

var fullCtor = modelProvider.Constructors.Last();
Assert.IsTrue(fullCtor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Internal));
Assert.AreEqual(2, fullCtor.Signature.Parameters.Count);
}
}
}
Loading

0 comments on commit 518ac2b

Please sign in to comment.