Skip to content

Commit

Permalink
Move ToSerial and ToEnum to SerializationProviders (#3795)
Browse files Browse the repository at this point in the history
Fixes: [#3651](#3651)

---------

Co-authored-by: m-nash <[email protected]>
Co-authored-by: JoshLove-msft <[email protected]>
  • Loading branch information
3 people committed Jul 30, 2024
1 parent 5a8b2cc commit 1d43455
Show file tree
Hide file tree
Showing 39 changed files with 371 additions and 267 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Diagnostics;
using System.IO;
using Microsoft.Generator.CSharp.Input;
using Microsoft.Generator.CSharp.Primitives;
using Microsoft.Generator.CSharp.Providers;

namespace Microsoft.Generator.CSharp.ClientModel.Providers
{
/// <summary>
/// This defines a class with extension methods for enums to convert an enum to its underlying value, or from its underlying value to an instance of the enum
/// </summary>
internal partial class ExtensibleEnumSerializationProvider : TypeProvider
{
private readonly InputEnumType _enumType;
private TypeProvider _enumProvider;

protected override string GetNamespace() => _enumProvider.Type.Namespace;

public ExtensibleEnumSerializationProvider(InputEnumType enumType)
{
Debug.Assert(enumType.IsExtensible);
_enumType = enumType;
_enumProvider = ClientModelPlugin.Instance.TypeFactory.CreateEnum(_enumType);
}

protected override string BuildRelativeFilePath()
{
return Path.Combine("src", "Generated", "Models", $"{_enumProvider.Name}.Serialization.cs");
}

protected override string BuildName() => _enumProvider.Name;

protected override TypeSignatureModifiers GetDeclarationModifiers() => _enumProvider.DeclarationModifiers;

protected override MethodProvider[] BuildMethods()
{
// for string-based extensible enums, we are using `ToString` as its serialization
// for non-string-based extensible enums, we need a method to serialize them
if (!_enumProvider.EnumUnderlyingType.Equals(typeof(string)))
{
var toSerialSignature = new MethodSignature(
Name: $"ToSerial{_enumProvider.EnumUnderlyingType.Name}",
Modifiers: MethodSignatureModifiers.Internal,
ReturnType: _enumProvider.EnumUnderlyingType,
Parameters: Array.Empty<ParameterProvider>(),
Description: null,
ReturnDescription: null);

// writes the method:
// internal float ToSerialSingle() => _value; // when ValueType is float
// internal int ToSerialInt32() => _value; // when ValueType is int
// etc
var valueField = new FieldProvider(FieldModifiers.Private | FieldModifiers.ReadOnly, _enumProvider.EnumUnderlyingType, "_value");
return [new MethodProvider(toSerialSignature, valueField, this)];
}
else
{
return Array.Empty<MethodProvider>();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,40 @@
using System.IO;
using System.Linq;
using Microsoft.Generator.CSharp.Expressions;
using Microsoft.Generator.CSharp.Input;
using Microsoft.Generator.CSharp.Primitives;
using Microsoft.Generator.CSharp.Providers;
using Microsoft.Generator.CSharp.Snippets;
using Microsoft.Generator.CSharp.Statements;
using static Microsoft.Generator.CSharp.Snippets.Snippet;

namespace Microsoft.Generator.CSharp.Providers
namespace Microsoft.Generator.CSharp.ClientModel.Providers
{
/// <summary>
/// This defines a class with extension methods for enums to convert an enum to its underlying value, or from its underlying value to an instance of the enum
/// </summary>
internal class FixedEnumSerializationProvider : TypeProvider
{
private readonly EnumProvider _enumType;
private readonly InputEnumType _enumType;
private TypeProvider _enumProvider;

public FixedEnumSerializationProvider(EnumProvider enumType)
public FixedEnumSerializationProvider(InputEnumType enumType)
{
Debug.Assert(!enumType.IsExtensible);

_enumType = enumType;
_enumProvider = ClientModelPlugin.Instance.TypeFactory.CreateEnum(_enumType);
}

protected override string GetNamespace() => _enumType.Type.Namespace;

protected override TypeSignatureModifiers GetDeclarationModifiers() => TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static | TypeSignatureModifiers.Partial;
protected override string GetNamespace() => _enumProvider.Type.Namespace;
protected override TypeSignatureModifiers GetDeclarationModifiers()
=> TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static | TypeSignatureModifiers.Partial | TypeSignatureModifiers.Class;

protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", "Models", $"{Name}.cs");
protected override string BuildRelativeFilePath()
{
return Path.Combine("src", "Generated", "Models", $"{_enumProvider.Name}.Serialization.cs");
}

protected override string BuildName() => $"{_enumType.Name}Extensions";
protected override string BuildName() => $"{_enumProvider.Name}Extensions";

/// <summary>
/// Returns if this enum type needs an extension method for serialization
Expand All @@ -43,7 +49,8 @@ public FixedEnumSerializationProvider(EnumProvider enumType)
private bool NeedsSerializationMethod()
{
// fixed enum with int based types, we do not write a method for serialization because it was embedded in the definition
if (_enumType is { IsExtensible: false, IsIntValueType: true })
bool isIntValueType = _enumProvider.EnumUnderlyingType.Equals(typeof(int)) || _enumProvider.EnumUnderlyingType.Equals(typeof(long));
if (!_enumType.IsExtensible && isIntValueType)
return false;

// otherwise we need a serialization method with the name of `ToSerial{UnderlyingTypeName}`
Expand All @@ -56,32 +63,32 @@ protected override MethodProvider[] BuildMethods()
// serialization method (in some cases we do not need serialization)
if (NeedsSerializationMethod())
{
var serializationValueParameter = new ParameterProvider("value", $"The value to serialize.", _enumType.Type);
var serializationValueParameter = new ParameterProvider("value", $"The value to serialize.", _enumProvider.Type);
var serializationSignature = new MethodSignature(
Name: $"ToSerial{_enumType.ValueType.Name}",
Name: $"ToSerial{_enumProvider.EnumUnderlyingType.Name}",
Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Static | MethodSignatureModifiers.Extension,
ReturnType: _enumType.ValueType,
ReturnType: _enumProvider.EnumUnderlyingType,
Parameters: [serializationValueParameter],
Description: null, ReturnDescription: null);

// the fields of an enum type are the values of the enum type
var knownCases = new SwitchCaseExpression[_enumType.Members.Count];
var knownCases = new SwitchCaseExpression[_enumProvider.EnumValues.Count];
for (int i = 0; i < knownCases.Length; i++)
{
var enumValue = _enumType.Members[i];
knownCases[i] = new SwitchCaseExpression(new MemberExpression(_enumType.Type, enumValue.Field.Name), Literal(enumValue.Value));
var enumValue = _enumProvider.EnumValues[i];
knownCases[i] = new SwitchCaseExpression(new MemberExpression(_enumProvider.Type, enumValue.Field.Name), Literal(enumValue.Value));
}
var defaultCase = SwitchCaseExpression.Default(ThrowExpression(New.ArgumentOutOfRangeException(_enumType, serializationValueParameter)));
var defaultCase = SwitchCaseExpression.Default(ThrowExpression(New.ArgumentOutOfRangeException(_enumProvider, serializationValueParameter)));
var serializationBody = new SwitchExpression(serializationValueParameter, [.. knownCases, defaultCase]);
methods.Add(new(serializationSignature, serializationBody, this));
}

// deserialization method (we always need a deserialization)
var deserializationValueParameter = new ParameterProvider("value", $"The value to deserialize.", _enumType.ValueType);
var deserializationValueParameter = new ParameterProvider("value", $"The value to deserialize.", _enumProvider.EnumUnderlyingType);
var deserializationSignature = new MethodSignature(
Name: $"To{_enumType.Type.Name}",
Name: $"To{_enumProvider.Name}",
Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Static | MethodSignatureModifiers.Extension,
ReturnType: _enumType.Type,
ReturnType: _enumProvider.Type,
Parameters: [deserializationValueParameter],
Description: null, ReturnDescription: null);

Expand All @@ -92,12 +99,12 @@ protected override MethodProvider[] BuildMethods()
// in general, this loop builds up if statements for each value, it looks like:
// if (<condition>) { return EnumType.TheValue; }
// the condition could be different depending on the type of the underlying value type of the enum
for (int i = 0; i < _enumType.Fields.Count; i++)
for (int i = 0; i < _enumProvider.Fields.Count; i++)
{
var enumField = _enumType.Fields[i];
var enumValue = _enumType.Members[i];
var enumField = _enumProvider.Fields[i];
var enumValue = _enumProvider.EnumValues[i];
ScopedApi<bool> condition;
if (_enumType.IsStringValueType)
if (_enumProvider.EnumUnderlyingType.Equals(typeof(string)))
{
// when the values are strings, we compare them case-insensitively
// this is either
Expand All @@ -106,7 +113,7 @@ protected override MethodProvider[] BuildMethods()
// string.Equals(value, "<the value>", StringComparison.InvariantCultureIgnoreCase)
condition = (enumValue.Value is string strValue && strValue.All(char.IsAscii)
? stringComparer.Invoke(nameof(IEqualityComparer<string>.Equals), value, Literal(strValue))
: Static(_enumType.ValueType).Invoke(nameof(Equals), [value, Literal(enumValue.Value), FrameworkEnumValue(StringComparison.InvariantCultureIgnoreCase)]))
: Static(_enumProvider.EnumUnderlyingType).Invoke(nameof(Equals), [value, Literal(enumValue.Value), FrameworkEnumValue(StringComparison.InvariantCultureIgnoreCase)]))
.As<bool>();
}
else
Expand All @@ -116,12 +123,12 @@ protected override MethodProvider[] BuildMethods()
}
deserializationBody.Add(new IfStatement(condition)
{
Return(new MemberExpression(_enumType.Type, enumField.Name))
Return(new MemberExpression(_enumProvider.Type, enumField.Name))
});
}

// add a fallback throw statement to ensure every path of this method returns a value
deserializationBody.Add(Throw(New.ArgumentOutOfRangeException(_enumType, deserializationValueParameter)));
deserializationBody.Add(Throw(New.ArgumentOutOfRangeException(_enumProvider, deserializationValueParameter)));

methods.Add(new(deserializationSignature, deserializationBody, this));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Generator.CSharp.ClientModel.Providers;
using Microsoft.Generator.CSharp.Input;
using Microsoft.Generator.CSharp.Primitives;
Expand Down Expand Up @@ -38,6 +40,14 @@ protected override IReadOnlyList<TypeProvider> CreateSerializationsCore(InputTyp
{
case InputModelType inputModel when inputModel.Usage.HasFlag(InputModelTypeUsage.Json):
return [new MrwSerializationTypeDefinition(inputModel)];
case InputEnumType inputEnumType when inputEnumType.IsExtensible:
if (ClientModelPlugin.Instance.TypeFactory.CreateCSharpType(inputEnumType).UnderlyingEnumType.Equals(typeof(string)))
{
return [];
}
return [new ExtensibleEnumSerializationProvider(inputEnumType)];
case InputEnumType inputEnumType:
return [new FixedEnumSerializationProvider(inputEnumType)];
default:
return base.CreateSerializationsCore(inputType);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,4 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// 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.Expressions;
using Microsoft.Generator.CSharp.Input;
using Microsoft.Generator.CSharp.Providers;
using Microsoft.Generator.CSharp.Statements;
using NUnit.Framework;

namespace Microsoft.Generator.CSharp.ClientModel.Tests.Providers
{
public class EnumProviderSerializationTests
{
[OneTimeSetUp]
public void Setup()
{
MockHelpers.LoadMockPlugin();
}

public static object[] ValidateTestCases =
{
new object[] {"One", 1, "Two", 2}
};

private TypeProvider? CreateEnumSerializationProvider(string stringA, int intA, string stringB, int intB)
{
IReadOnlyList<InputEnumTypeValue> values = new List<InputEnumTypeValue>
{
new InputEnumTypeValue(stringA, intA, null),
new InputEnumTypeValue(stringB, intB, null)
};
var input = new InputEnumType("mockInputEnum", "mockNamespace", "public", null, "The mock enum", InputModelTypeUsage.Input | InputModelTypeUsage.Output, new InputPrimitiveType(InputPrimitiveTypeKind.Int32), values, false);
TypeProvider enumType = ClientModelPlugin.Instance.TypeFactory.CreateEnum(input);
return enumType.SerializationProviders.FirstOrDefault();
}

[TestCaseSource(nameof(ValidateTestCases))]
public void ValidateToSerial(string stringA, int intA, string stringB, int intB)
{
var serialization = CreateEnumSerializationProvider(stringA, intA, stringB, intB);
MethodProvider? method = serialization!.Methods.Where(m => m.Signature.Name.Contains("ToSerial")).FirstOrDefault();
// Cast method.BodyExpression to SwitchCaseExpression
Assert.IsNull(method);
}

[TestCaseSource(nameof(ValidateTestCases))]
public void ValidateToEnum(string stringA, int intA, string stringB, int intB)
{
var serialization = CreateEnumSerializationProvider(stringA, intA, stringB, intB);
MethodProvider? method = serialization!.Methods.Where(m => m.Signature.Name.Contains("Enum")).FirstOrDefault();
// Cast method.BodyExpression to SwitchCaseExpression
if (method!.BodyStatements is MethodBodyStatements methodBodyStatements)
{
// Verify that the switch case expression has the correct number of cases (values + 1 for throw)
Assert.AreEqual(3, methodBodyStatements.Statements.Count());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ internal CSharpType(
implementation.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public) && arguments.All(t => t.IsPublic),
implementation.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Struct),
baseType,
implementation is EnumProvider enumProvider ? enumProvider.ValueType.FrameworkType : null)
implementation.IsEnum? implementation.EnumUnderlyingType.FrameworkType : null)
{
}

Expand Down
Loading

0 comments on commit 1d43455

Please sign in to comment.