Skip to content

Commit

Permalink
Move model serialization ctor to model provider (#4143)
Browse files Browse the repository at this point in the history
fixes: #3871
  • Loading branch information
jorgerangel-msft authored Aug 12, 2024
1 parent 75e773c commit 1e3ccc2
Show file tree
Hide file tree
Showing 29 changed files with 444 additions and 439 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,13 @@ namespace Microsoft.Generator.CSharp.ClientModel.Providers
/// </summary>
internal class MrwSerializationTypeDefinition : TypeProvider
{
private const string PrivateAdditionalPropertiesPropertyDescription = "Keeps track of any properties unknown to the library.";
private const string PrivateAdditionalPropertiesPropertyName = "_serializedAdditionalRawData";
private const string JsonModelWriteCoreMethodName = "JsonModelWriteCore";
private const string JsonModelCreateCoreMethodName = "JsonModelCreateCore";
private const string PersistableModelWriteCoreMethodName = "PersistableModelWriteCore";
private const string PersistableModelCreateCoreMethodName = "PersistableModelCreateCore";
private const string WriteAction = "writing";
private const string ReadAction = "reading";
private const string AdditionalRawDataVarName = "serializedAdditionalRawData";
private readonly ParameterProvider _utf8JsonWriterParameter = new("writer", $"The JSON writer.", typeof(Utf8JsonWriter));
private readonly ParameterProvider _utf8JsonReaderParameter = new("reader", $"The JSON reader.", typeof(Utf8JsonReader), isRef: true);
private readonly ParameterProvider _serializationOptionsParameter =
Expand All @@ -46,22 +44,19 @@ internal class MrwSerializationTypeDefinition : TypeProvider
private readonly ScopedApi<ModelReaderWriterOptions> _mrwOptionsParameterSnippet;
private readonly ScopedApi<JsonElement> _jsonElementParameterSnippet;
private readonly ScopedApi<bool> _isNotEqualToWireConditionSnippet;
private readonly CSharpType _privateAdditionalRawDataPropertyType = typeof(IDictionary<string, BinaryData>);
private readonly CSharpType _jsonModelTInterface;
private readonly CSharpType? _jsonModelObjectInterface;
private readonly CSharpType _persistableModelTInterface;
private readonly CSharpType? _persistableModelObjectInterface;
private TypeProvider _model;
private readonly ModelProvider _model;
private readonly InputModelType _inputModel;
private readonly FieldProvider? _rawDataField;
private readonly bool _isStruct;
private ConstructorProvider? _serializationConstructor;
// Flag to determine if the model should override the serialization methods
private readonly bool _shouldOverrideMethods;
// TODO -- we should not be needing this if we resolve https://github.com/microsoft/typespec/issues/3796
private readonly MrwSerializationTypeDefinition? _baseSerializationProvider;

public MrwSerializationTypeDefinition(InputModelType inputModel, TypeProvider modelProvider)
public MrwSerializationTypeDefinition(InputModelType inputModel, ModelProvider modelProvider)
{
_model = modelProvider;
_inputModel = inputModel;
Expand All @@ -71,11 +66,7 @@ public MrwSerializationTypeDefinition(InputModelType inputModel, TypeProvider mo
_jsonModelObjectInterface = _isStruct ? (CSharpType)typeof(IJsonModel<object>) : null;
_persistableModelTInterface = new CSharpType(typeof(IPersistableModel<>), _model.Type);
_persistableModelObjectInterface = _isStruct ? (CSharpType)typeof(IPersistableModel<object>) : null;

if (inputModel.BaseModel is not null)
_baseSerializationProvider = ClientModelPlugin.Instance.TypeFactory.CreateSerializations(inputModel.BaseModel, modelProvider).OfType<MrwSerializationTypeDefinition>().FirstOrDefault();

_rawDataField = BuildRawDataField();
_rawDataField = _model.Fields.FirstOrDefault(f => f.Name == PrivateAdditionalPropertiesPropertyName);
_shouldOverrideMethods = _model.Type.BaseType != null && _model.Type.BaseType is { IsFrameworkType: false };
_utf8JsonWriterSnippet = _utf8JsonWriterParameter.As<Utf8JsonWriter>();
_mrwOptionsParameterSnippet = _serializationOptionsParameter.As<ModelReaderWriterOptions>();
Expand All @@ -86,25 +77,15 @@ public MrwSerializationTypeDefinition(InputModelType inputModel, TypeProvider mo
protected override string GetNamespace() => _model.Type.Namespace;

protected override TypeSignatureModifiers GetDeclarationModifiers() => _model.DeclarationModifiers;
private ConstructorProvider SerializationConstructor => _serializationConstructor ??= BuildSerializationConstructor();
private ConstructorProvider SerializationConstructor => _serializationConstructor ??= _model.FullConstructor;

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

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

/// <summary>
/// Builds the fields for the model by adding the raw data field for serialization.
/// </summary>
/// <returns>The list of <see cref="FieldProvider"/> for the model.</returns>
protected override FieldProvider[] BuildFields()
{
return _rawDataField != null ? [_rawDataField] : Array.Empty<FieldProvider>();
}

protected override ConstructorProvider[] BuildConstructors()
{
List<ConstructorProvider> constructors = new();
bool serializationCtorParamsMatch = false;
bool ctorWithNoParamsExist = false;

foreach (var ctor in _model.Constructors)
Expand All @@ -116,21 +97,6 @@ protected override ConstructorProvider[] BuildConstructors()
{
ctorWithNoParamsExist = true;
}

if (!serializationCtorParamsMatch)
{
// Check if the model constructor parameters match the serialization constructor parameters
if (initializationCtorParams.SequenceEqual(SerializationConstructor.Signature.Parameters))
{
serializationCtorParamsMatch = true;
}
}
}

// Add the serialization constructor if it doesn't match any of the existing constructors
if (!serializationCtorParamsMatch)
{
constructors.Add(SerializationConstructor);
}

// Add an empty constructor if the model doesn't have one
Expand All @@ -139,39 +105,7 @@ protected override ConstructorProvider[] BuildConstructors()
constructors.Add(BuildEmptyConstructor());
}

return constructors.ToArray();
}

/// <summary>
/// Builds the raw data field for the model to be used for serialization.
/// </summary>
/// <returns>The constructed <see cref="FieldProvider"/> if the model should generate the field.</returns>
private FieldProvider? BuildRawDataField()
{
if (_isStruct)
{
return null;
}

// check if there is a raw data field on my base, if so, we do not have to have one here
if (_baseSerializationProvider?._rawDataField != null)
{
return null;
}

var modifiers = FieldModifiers.Private;
if (!_model.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Sealed))
{
modifiers |= FieldModifiers.Protected;
}

var rawDataField = new FieldProvider(
modifiers: modifiers,
type: _privateAdditionalRawDataPropertyType,
description: FormattableStringHelpers.FromString(PrivateAdditionalPropertiesPropertyDescription),
name: PrivateAdditionalPropertiesPropertyName);

return rawDataField;
return [.. constructors];
}

/// <summary>
Expand Down Expand Up @@ -527,28 +461,6 @@ internal MethodProvider BuildPersistableModelGetFormatFromOptionsObjectDeclarati
);
}

/// <summary>
/// Builds the serialization constructor for the model.
/// </summary>
/// <returns>The constructed serialization constructor.</returns>
internal ConstructorProvider BuildSerializationConstructor()
{
var (serializationCtorParameters, serializationCtorInitializer) = BuildSerializationConstructorParameters();

return new ConstructorProvider(
signature: new ConstructorSignature(
Type,
$"Initializes a new instance of {Type:C}",
MethodSignatureModifiers.Internal,
serializationCtorParameters,
Initializer: serializationCtorInitializer),
bodyStatements: new MethodBodyStatement[]
{
GetPropertyInitializers()
},
this);
}

private MethodBodyStatement[] BuildJsonModelWriteMethodBody(MethodProvider jsonModelWriteCoreMethod)
{
var coreMethodSignature = jsonModelWriteCoreMethod.Signature;
Expand Down Expand Up @@ -699,7 +611,6 @@ private ValueExpression[] GetSerializationCtorParameterValues()
{
var parameters = SerializationConstructor.Signature.Parameters;
ValueExpression[] serializationCtorParameters = new ValueExpression[parameters.Count];
var serializationCtorParameterValues = new Dictionary<string, ValueExpression>(parameters.Count);

// Map property variable names to their corresponding parameter values
for (int i = 0; i < parameters.Count; i++)
Expand Down Expand Up @@ -771,7 +682,7 @@ private List<MethodBodyStatement> BuildDeserializePropertiesStatements(ScopedApi
// deserialize the raw data properties
if (_rawDataField != null)
{
var elementType = _privateAdditionalRawDataPropertyType.Arguments[1].FrameworkType;
var elementType = _rawDataField.Type.Arguments[1].FrameworkType;
var rawDataDeserializationValue = GetValueTypeDeserializationExpression(elementType, jsonProperty.Value(), SerializationFormat.Default);
propertyDeserializationStatements.Add(new IfStatement(_isNotEqualToWireConditionSnippet)
{
Expand Down Expand Up @@ -959,44 +870,6 @@ private static MethodBodyStatement NullCheckCollectionItemIfRequired(
? new IfElseStatement(arrayItemVar.ValueKindEqualsNull(), assignNull, deserializeValue)
: deserializeValue;

/// <summary>
/// Builds the parameters for the serialization constructor by iterating through the input model properties.
/// It then adds raw data field to the constructor if it doesn't already exist in the list of constructed parameters.
/// </summary>
/// <returns>The list of parameters for the serialization parameter.</returns>
private (IReadOnlyList<ParameterProvider> Parameters, ConstructorInitializer? Initializer) BuildSerializationConstructorParameters()
{
var baseConstructor = _baseSerializationProvider?.SerializationConstructor.Signature;
var baseParameters = baseConstructor?.Parameters ?? [];
var parameterCapacity = baseParameters.Count + _inputModel.Properties.Count;
var parameterNames = baseParameters.Select(p => p.Name).ToHashSet();
var constructorParameters = new List<ParameterProvider>(parameterCapacity);

// add the base parameters
constructorParameters.AddRange(baseParameters);

// construct the initializer using the parameters from base signature
var constructorInitializer = new ConstructorInitializer(true, baseParameters);

foreach (var property in _model.Properties)
{
// skip those non-spec properties
if (property.WireInfo == null)
{
continue;
}
constructorParameters.Add(property.AsParameter);
}

// Append the raw data field if it doesn't already exist in the constructor parameters
if (_rawDataField != null)
{
constructorParameters.Add(_rawDataField.AsParameter);
}

return (constructorParameters, constructorInitializer);
}

private ConstructorProvider BuildEmptyConstructor()
{
var accessibility = _isStruct ? MethodSignatureModifiers.Public : MethodSignatureModifiers.Internal;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ protected override IReadOnlyList<TypeProvider> CreateSerializationsCore(InputTyp
switch (inputType)
{
case InputModelType inputModel when inputModel.Usage.HasFlag(InputModelTypeUsage.Json):
return [new MrwSerializationTypeDefinition(inputModel, typeProvider)];
if (typeProvider is ModelProvider modelProvider)
{
return [new MrwSerializationTypeDefinition(inputModel, modelProvider)];
}
return [];
case InputEnumType { IsExtensible: true } inputEnumType:
if (ClientModelPlugin.Instance.TypeFactory.CreateCSharpType(inputEnumType)?.UnderlyingEnumType.Equals(typeof(string)) == true)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ public class JsonModelCoreTests
public JsonModelCoreTests()
{
MockHelpers.LoadMockPlugin(createSerializationsCore: (inputType, typeProvider)
=> inputType is InputModelType modeltype ? [new MockMrwProvider(modeltype, typeProvider)] : []);
=> inputType is InputModelType modeltype ? [new MockMrwProvider(modeltype, (typeProvider as ModelProvider)!)] : []);
}

private class MockMrwProvider : MrwSerializationTypeDefinition
{
public MockMrwProvider(InputModelType inputModel, TypeProvider typeProvider)
: base(inputModel, typeProvider)
public MockMrwProvider(InputModelType inputModel, ModelProvider modelProvider)
: base(inputModel, modelProvider)
{
}

Expand Down

This file was deleted.

Loading

0 comments on commit 1e3ccc2

Please sign in to comment.