Merge pull request #15 from carldebilly/dev/cdb/newtownsoft-json-net
Added support for Newtownsoft's JSON.NET
This commit is contained in:
Коммит
41bf380a76
|
@ -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<{symbolNameForXml}> 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;
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче