Added support for Nettownsoft's JSON.NET

This commit is contained in:
Carl de Billy 2018-02-13 00:32:45 -05:00
Родитель 9b9217c158
Коммит aa00c38ab1
8 изменённых файлов: 130 добавлений и 19 удалений

Просмотреть файл

@ -33,8 +33,10 @@
1. The class needs to be partial - because generated code will augment it.
1. Restrictions:
* **No default constructor allowed**
(this won't work with [Newtownsoft's JSON.NET](https://www.newtonsoft.com/json)
- that's intentional: You need to deserialize the builder instead)
(will with [Newtownsoft's JSON.NET](https://www.newtonsoft.com/json),
when detected, it will generate custom JSON Converter. You can disable
this globally by setting this attribute:
`[assembly: ImmutableGenerationOptions(GenerateNewtownsoftJsonNetConverters=false)]`)
* **No property setters allowed** (even `private` ones):
properties should be _read only_, even for the class itself.
* **No fields allowed** (except static fields, but that would be weird).

Просмотреть файл

@ -20,6 +20,7 @@ using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
[assembly: Uno.ImmutableGenerationOptions(TreatArrayAsImmutable = true, GenerateEqualityByDefault = true)]
@ -147,6 +148,48 @@ namespace Uno.CodeGen.Tests
Option<MyImmutableEntity> option = builder;
option.MatchNone().Should().BeTrue();
}
[TestMethod]
public void Immutable_When_Deserializing_A_Using_JsonNet()
{
const string json = "{IsSomething:false, T:null, Entity:{MyField1:1, MyField2:2}}";
var a = JsonConvert.DeserializeObject<A>(json);
a.Should().NotBeNull();
a.IsSomething.Should().BeFalse();
a.T.Should().BeNull();
a.Entity.Should().NotBeNull();
a.Entity­.MyField1.Should().Be(1);
a.Entity­.MyField2.Should().Be(2);
}
[TestMethod]
public void Immutable_When_Deserializing_ABuilder_Using_JsonNet()
{
const string json = "{IsSomething:false, T:null, Entity:{MyField1:1, MyField2:2}}";
var a = JsonConvert.DeserializeObject<A.Builder>(json).ToImmutable();
a.Should().NotBeNull();
a.IsSomething.Should().BeFalse();
a.T.Should().BeNull();
a.Entity.Should().NotBeNull();
a.Entity­.MyField1.Should().Be(1);
a.Entity­.MyField2.Should().Be(2);
}
[TestMethod]
public void Immutable_When_Serializing_A_Using_JsonNet()
{
var json = JsonConvert.SerializeObject(A.Default.WithEntity(x => null).ToImmutable());
json.Should().BeEquivalentTo("{\"T\":null,\"Entity\":null,\"IsSomething\":true}");
}
[TestMethod]
public void Immutable_When_Serializing_ABuilder_Using_JsonNet()
{
var json = JsonConvert.SerializeObject(A.Default.WithEntity(x => null));
json.Should().BeEquivalentTo("{\"T\":null,\"Entity\":null,\"IsSomething\":true}");
}
}
[GeneratedImmutable]

Просмотреть файл

@ -20,6 +20,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
<PackageReference Include="MSTest.TestAdapter" Version="1.2.0" />
<PackageReference Include="MSTest.TestFramework" Version="1.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.4.1" />
<PackageReference Include="Uno.Core" Version="1.20.0-dev.4" />
<PackageReference Include="Uno.SourceGenerationTasks" Version="1.20.0-dev.17" />

Просмотреть файл

@ -107,7 +107,7 @@ namespace Uno
{
var builder = new IndentedStringBuilder();
var (symbolName, genericArguments, symbolNameWithGenerics, symbolNameForXml, symbolNameDefinition, resultFileName) = typeSymbol.GetSymbolNames();
var (symbolName, genericArguments, symbolNameWithGenerics, symbolNameForXml, symbolNameDefinition, resultFileName, _) = typeSymbol.GetSymbolNames();
var (equalityMembers, hashMembers, keyEqualityMembers) = GetEqualityMembers(typeSymbol);
var baseTypeInfo = GetBaseTypeInfo(typeSymbol);
var generateKeyEquals = baseTypeInfo.baseImplementsKeyEquals || baseTypeInfo.baseImplementsKeyEqualsT || keyEqualityMembers.Any();

Просмотреть файл

@ -29,7 +29,7 @@ namespace Uno.Helpers
var symbolName = typeSymbol.Name;
if (typeSymbol.TypeArguments.Length == 0) // not a generic type
{
return new SymbolNames(typeSymbol, symbolName, "", symbolName, symbolName, symbolName, symbolName);
return new SymbolNames(typeSymbol, symbolName, "", symbolName, symbolName, symbolName, symbolName, "");
}
var argumentNames = typeSymbol.GetTypeArgumentNames();
@ -49,7 +49,13 @@ namespace Uno.Helpers
// symbolNameWithGenerics: MyType_T1_T2
var symbolFilename = $"{symbolName}_{string.Join("_", argumentNames)}";
return new SymbolNames(typeSymbol, symbolName, $"<{genericArguments}>", symbolNameWithGenerics, symbolForXml, symbolNameDefinition, symbolFilename);
var genericConstraints = " " + string.Join(" ", typeSymbol
.TypeArguments
.OfType<ITypeParameterSymbol>()
.SelectMany((tps, i) => tps.ConstraintTypes.Select(c => (tps: tps, c:c, i:i)))
.Select(x => $"where {argumentNames[x.i]} : {x.c}"));
return new SymbolNames(typeSymbol, symbolName, $"<{genericArguments}>", symbolNameWithGenerics, symbolForXml, symbolNameDefinition, symbolFilename, genericConstraints);
}
public static string[] GetTypeArgumentNames(this ITypeSymbol typeSymbol)
@ -60,7 +66,15 @@ namespace Uno.Helpers
public class SymbolNames
{
public SymbolNames(INamedTypeSymbol symbol, string symbolName, string genericArguments, string symbolNameWithGenerics, string symbolFoxXml, string symbolNameDefinition, string symbolFilename)
public SymbolNames(
INamedTypeSymbol symbol,
string symbolName,
string genericArguments,
string symbolNameWithGenerics,
string symbolFoxXml,
string symbolNameDefinition,
string symbolFilename,
string genericConstraints)
{
Symbol = symbol;
SymbolName = symbolName;
@ -69,6 +83,7 @@ namespace Uno.Helpers
SymbolFoxXml = symbolFoxXml;
SymbolNameDefinition = symbolNameDefinition;
SymbolFilename = symbolFilename;
GenericConstraints = genericConstraints;
}
public INamedTypeSymbol Symbol { get; }
@ -78,6 +93,7 @@ namespace Uno.Helpers
public string SymbolFoxXml { get; }
public string SymbolNameDefinition { get; }
public string SymbolFilename { get; }
public string GenericConstraints { get; }
public void Deconstruct(
out string symbolName,
@ -85,7 +101,8 @@ namespace Uno.Helpers
out string symbolNameWithGenerics,
out string symbolFoxXml,
out string symbolNameDefinition,
out string symbolFilename)
out string symbolFilename,
out string genericConstraints)
{
symbolName = SymbolName;
genericArguments = GenericArguments;
@ -93,6 +110,7 @@ namespace Uno.Helpers
symbolFoxXml = SymbolFoxXml;
symbolNameDefinition = SymbolNameDefinition;
symbolFilename = SymbolFilename;
genericConstraints = GenericConstraints;
}
}
}

Просмотреть файл

@ -108,7 +108,7 @@ namespace Uno.Helpers
return true; // tuples are immutables
}
switch (definitionType.BaseType.ToString())
switch (definitionType.BaseType?.ToString())
{
case "System.Enum":
return true;

Просмотреть файл

@ -43,8 +43,9 @@ namespace Uno
private INamedTypeSymbol _immutableAttributeCopyIgnoreAttributeSymbol;
private INamedTypeSymbol _immutableGenerationOptionsAttributeSymbol;
private (bool generateOptionCode, bool treatArrayAsImmutable, bool generateEqualityByDefault, bool generateJsonNet) _generationOptions;
private bool _generateOptionCode = true;
private (bool generateOptionCode, bool treatArrayAsImmutable, bool generateEqualityByDefault) _generationOptions;
private bool _generateJsonNet = true;
private Regex[] _copyIgnoreAttributeRegexes;
@ -73,6 +74,8 @@ namespace Uno
_generateOptionCode = _generationOptions.generateOptionCode && context.Compilation.GetTypeByMetadataName("Uno.Option") != null;
_generateJsonNet = _generationOptions.generateOptionCode && context.Compilation.GetTypeByMetadataName("Newtonsoft.Json.JsonConvert") != null;
foreach ((var type, var moduleAttribute) in generationData)
{
var baseTypeInfo = GetTypeInfo(context, type, immutableEntitiesToGenerate);
@ -90,11 +93,12 @@ namespace Uno
.Select(a => new Regex(a.ConstructorArguments[0].Value.ToString()));
}
private (bool generateOptionCode, bool treatArrayAsImmutable, bool generateEqualityByDefault) ExtractGenerationOptions(IAssemblySymbol assembly)
private (bool generateOptionCode, bool treatArrayAsImmutable, bool generateEqualityByDefault, bool generateJsonNet) ExtractGenerationOptions(IAssemblySymbol assembly)
{
var generateOptionCode = true;
var treatArrayAsImmutable = false;
var generateEqualityByDefault = true;
var generateJsonNet = true;
var attribute = assembly
.GetAttributes()
@ -115,11 +119,14 @@ namespace Uno
case nameof(ImmutableGenerationOptionsAttribute.GenerateEqualityByDefault):
generateEqualityByDefault = (bool)argument.Value.Value;
break;
case nameof(ImmutableGenerationOptionsAttribute.GenerateNewtownsoftJsonNetConverters):
generateJsonNet = (bool)argument.Value.Value;
break;
}
}
}
return (generateOptionCode, treatArrayAsImmutable, generateEqualityByDefault);
return (generateOptionCode, treatArrayAsImmutable, generateEqualityByDefault, generateJsonNet);
}
private bool GetShouldGenerateEquality(AttributeData attribute)
@ -176,6 +183,7 @@ namespace Uno
var defaultMemberName = "Default";
var generateOption = _generateOptionCode && !typeSymbol.IsAbstract;
var generateJsonNet = _generateJsonNet && !typeSymbol.IsAbstract;
var classCopyIgnoreRegexes = ExtractCopyIgnoreAttributes(typeSymbol).ToArray();
@ -206,7 +214,7 @@ namespace Uno
var builder = new IndentedStringBuilder();
var symbolNames = typeSymbol.GetSymbolNames();
var (symbolName, genericArguments, symbolNameWithGenerics, symbolNameForXml, symbolNameDefinition, resultFileName) = symbolNames;
var (symbolName, genericArguments, symbolNameWithGenerics, symbolNameForXml, symbolNameDefinition, resultFileName, genericConstraints) = symbolNames;
ValidateType(builder, typeSymbol, baseTypeInfo, symbolNames, typeProperties);
@ -249,6 +257,11 @@ namespace Uno
builder.AppendLineInvariant("[global::Uno.GeneratedEquality] // Set [GeneratedImmutable(GeneratedEquality = false)] if you don't want this attribute.");
}
if (generateJsonNet)
{
builder.AppendLineInvariant($"[global::Newtonsoft.Json.JsonConverter(typeof({symbolName}BuilderJsonConverterTo{symbolNameDefinition}))]");
}
builder.AppendLineInvariant($"[global::Uno.ImmutableBuilder(typeof({symbolNameDefinition}.Builder))] // Other generators can use this to find the builder.");
var abstractClause = typeSymbol.IsAbstract ? "abstract " : "";
@ -550,8 +563,6 @@ public static implicit operator global::Uno.Option<{symbolNameWithGenerics}>(Bui
builder.AppendLineInvariant($"// => You have this error? it's because you defined a default constructor on your class!");
builder.AppendLineInvariant($"//");
builder.AppendLineInvariant($"// New instances should use the builder instead.");
builder.AppendLineInvariant($"// We know, it's not compatible with Newtownsoft's JSON.net: It's by-design.");
builder.AppendLineInvariant($"// (you need to deserialize the builder, not the immutable type itself)");
builder.AppendLineInvariant($"//");
builder.AppendLineInvariant($"// Send your complaints or inquiried to \"architecture [-@-] nventive [.] com\".");
builder.AppendLineInvariant($"//");
@ -630,7 +641,7 @@ public static implicit operator {symbolNameWithGenerics}(Builder builder)
builder.AppendLineInvariant($"/// {symbolNameForXml} modified = original.With{prop.Name}(new{prop.Name}Value); // create a new modified immutable instance");
builder.AppendLineInvariant("/// </example>");
builder.AppendLineInvariant("[global::System.Diagnostics.Contracts.Pure]");
using (builder.BlockInvariant($"public static {builderName} With{prop.Name}{genericArguments}(this {symbolNameWithGenerics} entity, {prop.Type} value)"))
using (builder.BlockInvariant($"public static {builderName} With{prop.Name}{genericArguments}(this {symbolNameWithGenerics} entity, {prop.Type} value){genericConstraints}"))
{
builder.AppendLineInvariant($"return new {builderName}(entity).With{prop.Name}(value);");
}
@ -648,7 +659,7 @@ public static implicit operator {symbolNameWithGenerics}(Builder builder)
builder.AppendLineInvariant($"/// {symbolNameForXml} modified = original.With{prop.Name}(previous{prop.Name}Value => new {prop.Type}(...)); // create a new modified immutable instance");
builder.AppendLineInvariant("/// </example>");
builder.AppendLineInvariant("[global::System.Diagnostics.Contracts.Pure]");
using (builder.BlockInvariant($"public static {builderName} With{prop.Name}{genericArguments}(this {symbolNameWithGenerics} entity, Func<{prop.Type}, {prop.Type}> valueSelector)"))
using (builder.BlockInvariant($"public static {builderName} With{prop.Name}{genericArguments}(this {symbolNameWithGenerics} entity, Func<{prop.Type}, {prop.Type}> valueSelector){genericConstraints}"))
{
builder.AppendLineInvariant($"return new {builderName}(entity).With{prop.Name}(valueSelector);");
}
@ -671,7 +682,7 @@ public static implicit operator {symbolNameWithGenerics}(Builder builder)
builder.AppendLineInvariant($"/// Option&lt;{symbolNameForXml}&gt; modified = original.With{prop.Name}(new{prop.Name}Value); // result type is Option.Some");
builder.AppendLineInvariant("/// </example>");
builder.AppendLineInvariant("[global::System.Diagnostics.Contracts.Pure]");
using (builder.BlockInvariant($"public static {builderName} With{prop.Name}{genericArguments}(this global::Uno.Option<{symbolNameWithGenerics}> optionEntity, {prop.Type} value)"))
using (builder.BlockInvariant($"public static {builderName} With{prop.Name}{genericArguments}(this global::Uno.Option<{symbolNameWithGenerics}> optionEntity, {prop.Type} value){genericConstraints}"))
{
builder.AppendLineInvariant($"return {builderName}.FromOption(optionEntity).With{prop.Name}(value);");
}
@ -692,7 +703,7 @@ public static implicit operator {symbolNameWithGenerics}(Builder builder)
builder.AppendLineInvariant($"/// {symbolNameForXml} modified = original.With{prop.Name}(previous{prop.Name}Value => new {prop.Type}(...)); // create a new modified immutable instance");
builder.AppendLineInvariant("/// </example>");
builder.AppendLineInvariant("[global::System.Diagnostics.Contracts.Pure]");
using (builder.BlockInvariant($"public static {builderName} With{prop.Name}{genericArguments}(this global::Uno.Option<{symbolNameWithGenerics}> optionEntity, Func<{prop.Type}, {prop.Type}> valueSelector)"))
using (builder.BlockInvariant($"public static {builderName} With{prop.Name}{genericArguments}(this global::Uno.Option<{symbolNameWithGenerics}> optionEntity, Func<{prop.Type}, {prop.Type}> valueSelector){genericConstraints}"))
{
builder.AppendLineInvariant($"return {builderName}.FromOption(optionEntity).With{prop.Name}(valueSelector);");
}
@ -703,6 +714,34 @@ public static implicit operator {symbolNameWithGenerics}(Builder builder)
builder.AppendLineInvariant($"#endregion // .WithXXX() methods on {symbolNameWithGenerics}");
}
builder.AppendLine();
}
if (generateJsonNet)
{
builder.AppendLine(
$@"public sealed class {symbolName}BuilderJsonConverterTo{symbolName}{genericArguments} : global::Newtonsoft.Json.JsonConverter{genericConstraints}
{{
public override void WriteJson(global::Newtonsoft.Json.JsonWriter writer, object value, global::Newtonsoft.Json.JsonSerializer serializer)
{{
var v = ({symbolNameWithGenerics}.Builder)({symbolNameWithGenerics})value;
serializer.Serialize(writer, v);
}}
public override object ReadJson(global::Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, global::Newtonsoft.Json.JsonSerializer serializer)
{{
var o = serializer.Deserialize<{symbolNameWithGenerics}.Builder>(reader);
return ({symbolNameWithGenerics})o;
}}
public override bool CanConvert(Type objectType)
{{
return objectType == typeof({symbolNameWithGenerics}) || objectType == typeof({symbolNameWithGenerics}.Builder);
}}
}}");
builder.AppendLine();
}
}

Просмотреть файл

@ -46,8 +46,16 @@ namespace Uno
/// </summary>
/// <remarks>
/// Default is true. Can be overridden by type on the attribute declaration
/// `[GenerateImmutable(GenerateEquality=XXX)]`
/// `[GenerateImmutable(GenerateEquality=false)]`
/// </remarks>
public bool GenerateEqualityByDefault { get; set; } = true;
/// <summary>
/// If you want to generate Newtownsoft's JSON.NET converters by default.
/// </summary>
/// <remarks>
/// Default is true. No effect if package `Newtownsoft.Json` is not referenced.
/// </remarks>
public bool GenerateNewtownsoftJsonNetConverters { get; set; } = true;
}
}