Skip to content

Commit

Permalink
Move serialization constructors and raw field (#3414)
Browse files Browse the repository at this point in the history
This PR migrates the construction of the existing serialization
constructors for a model to the MrwSerializationTypeProvider class. With
this change, all serialization related code is generated with a models
serialization class file. It also adds the support for generating an
additional constructor to deserialize into a model with an additional
raw data parameter.
  • Loading branch information
jorgerangel-msft committed Jun 4, 2024
1 parent 5da8fe7 commit f853b72
Show file tree
Hide file tree
Showing 30 changed files with 587 additions and 457 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using Microsoft.Generator.CSharp.Input;
using Microsoft.Generator.CSharp.ClientModel.Snippets;
using Microsoft.Generator.CSharp.Providers;
using Microsoft.Generator.CSharp.Snippets;
Expand All @@ -29,10 +30,11 @@ public class ClientModelPlugin : CodeModelPlugin
/// Returns the serialization type providers for the given model type provider.
/// </summary>
/// <param name="provider">The model type provider.</param>
public override IReadOnlyList<TypeProvider> GetSerializationTypeProviders(ModelProvider provider)
/// <param name="inputModel">The input model.</param>
public override IReadOnlyList<TypeProvider> GetSerializationTypeProviders(ModelProvider provider, InputModelType inputModel)
{
// Add MRW serialization type provider
return [new MrwSerializationTypeProvider(provider)];
return [new MrwSerializationTypeProvider(provider, inputModel)];
}

[ImportingConstructor]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
<ProjectReference Include="..\..\Microsoft.Generator.CSharp\src\Microsoft.Generator.CSharp.csproj" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Generator.CSharp\src\Shared\**\*.cs" LinkBase="Shared" />
</ItemGroup>

<!-- Copy output to package dist path for local execution and -->
<Target Name="CopyForNpmPackage" AfterTargets="Build">
<Message Text="Copying output to dist path" Importance="high" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@

using System;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Microsoft.Generator.CSharp.ClientModel.Snippets;
using Microsoft.Generator.CSharp.Expressions;
using Microsoft.Generator.CSharp.Input;
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.ClientModel
{
Expand All @@ -17,26 +21,34 @@ namespace Microsoft.Generator.CSharp.ClientModel
/// </summary>
internal sealed class MrwSerializationTypeProvider : TypeProvider
{
private readonly ParameterProvider SerializationOptionsParameter =
new("options", $"The client options.", typeof(ModelReaderWriterOptions));
private readonly ParameterProvider _serializationOptionsParameter =
new("options", $"The client options for reading and writing models.", typeof(ModelReaderWriterOptions));
private const string _privateAdditionalPropertiesPropertyDescription = "Keeps track of any properties unknown to the library.";
private const string _privateAdditionalPropertiesPropertyName = "_serializedAdditionalRawData";
private static readonly CSharpType _privateAdditionalPropertiesPropertyType = typeof(IDictionary<string, BinaryData>);
private readonly CSharpType _iJsonModelTInterface;
private readonly CSharpType? _iJsonModelObjectInterface;
private readonly CSharpType _iPersistableModelTInterface;
private readonly CSharpType? _iPersistableModelObjectInterface;
private readonly ModelProvider _model;
private ModelProvider _model;
private InputModelType _inputModel;
private readonly FieldProvider? _rawDataField;
private readonly bool _isStruct;

public MrwSerializationTypeProvider(ModelProvider model)
public MrwSerializationTypeProvider(ModelProvider model, InputModelType inputModel)
{
_model = model;
_inputModel = inputModel;
_isStruct = model.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Struct);
Name = model.Name;
Namespace = model.Namespace;
// Initialize the serialization interfaces
_iJsonModelTInterface = new CSharpType(typeof(IJsonModel<>), _model.Type);
_iJsonModelTInterface = new CSharpType(typeof(IJsonModel<>), model.Type);
_iJsonModelObjectInterface = _isStruct ? (CSharpType)typeof(IJsonModel<object>) : null;
_iPersistableModelTInterface = new CSharpType(typeof(IPersistableModel<>), _model.Type);
_iPersistableModelTInterface = new CSharpType(typeof(IPersistableModel<>), model.Type);
_iPersistableModelObjectInterface = _isStruct ? (CSharpType)typeof(IPersistableModel<object>) : null;
_rawDataField = BuildRawDataField();

Name = model.Name;
Namespace = model.Namespace;
}

protected override TypeSignatureModifiers GetDeclarationModifiers() => _model.DeclarationModifiers;
Expand All @@ -45,7 +57,78 @@ public MrwSerializationTypeProvider(ModelProvider model)
public override string Namespace { get; }

/// <summary>
/// Builds the serialization methods for the model. If the serialization supports JSON, it will build the JSON serialization methods.
/// 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 MethodProvider[] BuildConstructors()
{
List<MethodProvider> constructors = new List<MethodProvider>();
bool serializationCtorParamsMatch = false;
bool ctorWithNoParamsExist = false;
MethodProvider serializationConstructor = BuildSerializationConstructor();

foreach (var ctor in _model.Constructors)
{
var initializationCtorParams = ctor.Signature.Parameters;

// Check if the model constructor has no parameters
if (!ctorWithNoParamsExist && !initializationCtorParams.Any())
{
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
if (!ctorWithNoParamsExist)
{
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;
}

var FieldProvider = new FieldProvider(
modifiers: FieldModifiers.Private,
type: _privateAdditionalPropertiesPropertyType,
name: _privateAdditionalPropertiesPropertyName);

return FieldProvider;
}

/// <summary>
/// Builds the serialization methods for the model.
/// </summary>
/// <returns>A list of serialization and deserialization methods for the model.</returns>
protected override MethodProvider[] BuildMethods()
Expand Down Expand Up @@ -91,7 +174,7 @@ internal MethodProvider BuildJsonModelWriteMethod()
// void IJsonModel<T>.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options)
return new MethodProvider
(
new MethodSignature(nameof(IJsonModel<object>.Write), null, null, MethodSignatureModifiers.None, null, null, new[] { utf8JsonWriterParameter, SerializationOptionsParameter }, ExplicitInterface: _iJsonModelTInterface),
new MethodSignature(nameof(IJsonModel<object>.Write), null, null, MethodSignatureModifiers.None, null, null, new[] { utf8JsonWriterParameter, _serializationOptionsParameter }, ExplicitInterface: _iJsonModelTInterface),
// TO-DO: Add body for json properties' serialization https://github.com/microsoft/typespec/issues/3330
Snippet.EmptyStatement
);
Expand All @@ -107,7 +190,7 @@ internal MethodProvider BuildJsonModelCreateMethod()
var typeOfT = GetModelArgumentType(_iJsonModelTInterface);
return new MethodProvider
(
new MethodSignature(nameof(IJsonModel<object>.Create), null, null, MethodSignatureModifiers.None, typeOfT, null, new[] { utf8JsonReaderParameter, SerializationOptionsParameter }, ExplicitInterface: _iJsonModelTInterface),
new MethodSignature(nameof(IJsonModel<object>.Create), null, null, MethodSignatureModifiers.None, typeOfT, null, new[] { utf8JsonReaderParameter, _serializationOptionsParameter }, ExplicitInterface: _iJsonModelTInterface),
// TO-DO: Call the base model ctor for now until the model properties are serialized https://github.com/microsoft/typespec/issues/3330
Snippet.Return(new NewInstanceExpression(typeOfT, Array.Empty<ValueExpression>()))
);
Expand All @@ -122,7 +205,7 @@ internal MethodProvider BuildIModelWriteMethod()
var returnType = typeof(BinaryData);
return new MethodProvider
(
new MethodSignature(nameof(IPersistableModel<object>.Write), null, null, MethodSignatureModifiers.None, returnType, null, new[] { SerializationOptionsParameter }, ExplicitInterface: _iPersistableModelTInterface),
new MethodSignature(nameof(IPersistableModel<object>.Write), null, null, MethodSignatureModifiers.None, returnType, null, new[] { _serializationOptionsParameter }, ExplicitInterface: _iPersistableModelTInterface),
// TO-DO: Call the base model ctor for now until the model properties are serialized https://github.com/microsoft/typespec/issues/3330
Snippet.Return(new NewInstanceExpression(returnType, [Snippet.Literal(_iPersistableModelTInterface.Name)]))
);
Expand All @@ -138,7 +221,7 @@ internal MethodProvider BuildIModelCreateMethod()
var typeOfT = GetModelArgumentType(_iPersistableModelTInterface);
return new MethodProvider
(
new MethodSignature(nameof(IPersistableModel<object>.Create), null, null, MethodSignatureModifiers.None, typeOfT, null, new[] { dataParameter, SerializationOptionsParameter }, ExplicitInterface: _iPersistableModelTInterface),
new MethodSignature(nameof(IPersistableModel<object>.Create), null, null, MethodSignatureModifiers.None, typeOfT, null, new[] { dataParameter, _serializationOptionsParameter }, ExplicitInterface: _iPersistableModelTInterface),
// TO-DO: Call the base model ctor for now until the model properties are serialized https://github.com/microsoft/typespec/issues/3330
Snippet.Return(new NewInstanceExpression(typeOfT, Array.Empty<ValueExpression>()))
);
Expand All @@ -153,11 +236,99 @@ internal MethodProvider BuildIModelGetFormatFromOptionsMethod()
// ModelReaderWriterFormat IPersistableModel<T>.GetFormatFromOptions(ModelReaderWriterOptions options)
return new MethodProvider
(
new MethodSignature(nameof(IPersistableModel<object>.GetFormatFromOptions), null, null, MethodSignatureModifiers.None, typeof(string), null, new[] { SerializationOptionsParameter }, ExplicitInterface: _iPersistableModelTInterface),
new MethodSignature(nameof(IPersistableModel<object>.GetFormatFromOptions), null, null, MethodSignatureModifiers.None, typeof(string), null, new[] { _serializationOptionsParameter }, ExplicitInterface: _iPersistableModelTInterface),
jsonWireFormat
);
}

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

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

private MethodBodyStatement GetPropertyInitializers(IReadOnlyList<ParameterProvider> parameters)
{
List<MethodBodyStatement> methodBodyStatements = new();

foreach (var param in parameters)
{
if (param.Name == _rawDataField?.Name.ToVariableName())
{
methodBodyStatements.Add(Assign(new MemberExpression(null, _rawDataField.Name), new ParameterReferenceSnippet(param)));
continue;
}

ValueExpression initializationValue = new ParameterReferenceSnippet(param);
var initializationStatement = Assign(new MemberExpression(null, param.Name.FirstCharToUpperCase()), initializationValue);
if (initializationStatement != null)
{
methodBodyStatements.Add(initializationStatement);
}
}

return methodBodyStatements;
}

/// <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 List<ParameterProvider> BuildSerializationConstructorParameters()
{
List<ParameterProvider> constructorParameters = new List<ParameterProvider>();
bool shouldAddRawDataField = _rawDataField != null;

foreach (var property in _inputModel.Properties)
{
var parameter = new ParameterProvider(property)
{
Validation = ParameterValidationType.None,
};
constructorParameters.Add(parameter);

if (shouldAddRawDataField && string.Equals(parameter.Name, _rawDataField?.Name, StringComparison.OrdinalIgnoreCase))
{
shouldAddRawDataField = false;
}
}

// Append the raw data field if it doesn't already exist in the constructor parameters
if (shouldAddRawDataField && _rawDataField != null)
{
constructorParameters.Add(new ParameterProvider(
_rawDataField.Name.ToVariableName(),
FormattableStringHelpers.FromString(_privateAdditionalPropertiesPropertyDescription),
_rawDataField.Type));
}

return constructorParameters;
}

private MethodProvider BuildEmptyConstructor()
{
var accessibility = _isStruct ? MethodSignatureModifiers.Public : MethodSignatureModifiers.Internal;
return new MethodProvider(
signature: new ConstructorSignature(Type, $"Initializes a new instance of {Type:C} for deserialization.", null, accessibility, Array.Empty<ParameterProvider>()),
bodyStatements: new MethodBodyStatement());
}

/// <summary>
/// Attempts to get the model argument type from the model interface.
/// </summary>
Expand Down

This file was deleted.

Loading

0 comments on commit f853b72

Please sign in to comment.