From bb46e2b0c7c993aa11b29239492bc3a20fbf4bae Mon Sep 17 00:00:00 2001 From: Erik Zhang Date: Fri, 22 Jul 2022 16:11:21 +0800 Subject: [PATCH] Add Json Serialization --- src/Neo.Json/JNumber.cs | 4 +- src/Neo.Json/JString.cs | 2 + src/Neo.Json/JToken.cs | 2 + src/Neo.Json/Neo.Json.csproj | 1 + src/Neo.Json/Serialization/ArraySerializer.cs | 33 +++++++ .../Serialization/BooleanSerializer.cs | 18 ++++ .../Serialization/ByteArraySerializer.cs | 22 +++++ .../Serialization/CompositeSerializer.cs | 73 +++++++++++++++ src/Neo.Json/Serialization/EnumSerializer.cs | 18 ++++ src/Neo.Json/Serialization/Int64Serializer.cs | 23 +++++ .../Serialization/IntegerSerializer.cs | 23 +++++ src/Neo.Json/Serialization/Serializer.cs | 90 +++++++++++++++++++ .../Serialization/SerializerAttribute.cs | 16 ++++ .../Serialization/StringSerializer.cs | 22 +++++ .../Serialization/UInt64Serializer.cs | 23 +++++ 15 files changed, 368 insertions(+), 2 deletions(-) create mode 100644 src/Neo.Json/Serialization/ArraySerializer.cs create mode 100644 src/Neo.Json/Serialization/BooleanSerializer.cs create mode 100644 src/Neo.Json/Serialization/ByteArraySerializer.cs create mode 100644 src/Neo.Json/Serialization/CompositeSerializer.cs create mode 100644 src/Neo.Json/Serialization/EnumSerializer.cs create mode 100644 src/Neo.Json/Serialization/Int64Serializer.cs create mode 100644 src/Neo.Json/Serialization/IntegerSerializer.cs create mode 100644 src/Neo.Json/Serialization/Serializer.cs create mode 100644 src/Neo.Json/Serialization/SerializerAttribute.cs create mode 100644 src/Neo.Json/Serialization/StringSerializer.cs create mode 100644 src/Neo.Json/Serialization/UInt64Serializer.cs diff --git a/src/Neo.Json/JNumber.cs b/src/Neo.Json/JNumber.cs index 7e58b72567..5acf982956 100644 --- a/src/Neo.Json/JNumber.cs +++ b/src/Neo.Json/JNumber.cs @@ -21,12 +21,12 @@ public class JNumber : JToken /// /// Represents the largest safe integer in JSON. /// - public static readonly long MAX_SAFE_INTEGER = (long)Math.Pow(2, 53) - 1; + public const long MAX_SAFE_INTEGER = (1L << 53) - 1; /// /// Represents the smallest safe integer in JSON. /// - public static readonly long MIN_SAFE_INTEGER = -MAX_SAFE_INTEGER; + public const long MIN_SAFE_INTEGER = -MAX_SAFE_INTEGER; /// /// Gets the value of the JSON token. diff --git a/src/Neo.Json/JString.cs b/src/Neo.Json/JString.cs index 714ff85928..f7919e5b54 100644 --- a/src/Neo.Json/JString.cs +++ b/src/Neo.Json/JString.cs @@ -8,6 +8,7 @@ // Redistribution and use in source and binary forms with or without // modifications are permitted. +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.Json; @@ -88,6 +89,7 @@ public static implicit operator JString(Enum value) return new JString(value.ToString()); } + [return: NotNullIfNotNull("value")] public static implicit operator JString?(string? value) { return value == null ? null : new JString(value); diff --git a/src/Neo.Json/JToken.cs b/src/Neo.Json/JToken.cs index 63ab9f4752..b8578350e9 100644 --- a/src/Neo.Json/JToken.cs +++ b/src/Neo.Json/JToken.cs @@ -8,6 +8,7 @@ // Redistribution and use in source and binary forms with or without // modifications are permitted. +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using static Neo.Json.Utility; @@ -297,6 +298,7 @@ public static implicit operator JToken(double value) return (JNumber)value; } + [return: NotNullIfNotNull("value")] public static implicit operator JToken?(string? value) { return (JString?)value; diff --git a/src/Neo.Json/Neo.Json.csproj b/src/Neo.Json/Neo.Json.csproj index 94943d4402..285d3f7cf4 100644 --- a/src/Neo.Json/Neo.Json.csproj +++ b/src/Neo.Json/Neo.Json.csproj @@ -3,6 +3,7 @@ enable enable + preview NEO;JSON diff --git a/src/Neo.Json/Serialization/ArraySerializer.cs b/src/Neo.Json/Serialization/ArraySerializer.cs new file mode 100644 index 0000000000..b90f44c280 --- /dev/null +++ b/src/Neo.Json/Serialization/ArraySerializer.cs @@ -0,0 +1,33 @@ +// Copyright (C) 2015-2022 The Neo Project. +// +// The Neo.Json is free software distributed under the MIT software license, +// see the accompanying file LICENSE in the main directory of the +// project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Diagnostics.CodeAnalysis; + +namespace Neo.Json.Serialization; + +class ArraySerializer : Serializer +{ + private static readonly Serializer elementSerializer = GetSerializer(); + + [return: NotNullIfNotNull("obj")] + protected internal override JArray? Serialize(T?[]? obj) + { + if (obj is null) return null; + return obj.Select(elementSerializer.Serialize).ToArray(); + } + + [return: NotNullIfNotNull("json")] + protected internal override T?[]? Deserialize(JToken? json) => json switch + { + JArray array => array.Select(elementSerializer.Deserialize).ToArray(), + null => null, + _ => throw new FormatException() + }; +} diff --git a/src/Neo.Json/Serialization/BooleanSerializer.cs b/src/Neo.Json/Serialization/BooleanSerializer.cs new file mode 100644 index 0000000000..67b139b935 --- /dev/null +++ b/src/Neo.Json/Serialization/BooleanSerializer.cs @@ -0,0 +1,18 @@ +// Copyright (C) 2015-2022 The Neo Project. +// +// The Neo.Json is free software distributed under the MIT software license, +// see the accompanying file LICENSE in the main directory of the +// project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.Json.Serialization; + +class BooleanSerializer : Serializer +{ + protected internal override JBoolean Serialize(bool obj) => obj; + + protected internal override bool Deserialize(JToken? json) => json!.GetBoolean(); +} diff --git a/src/Neo.Json/Serialization/ByteArraySerializer.cs b/src/Neo.Json/Serialization/ByteArraySerializer.cs new file mode 100644 index 0000000000..d0c471a8bb --- /dev/null +++ b/src/Neo.Json/Serialization/ByteArraySerializer.cs @@ -0,0 +1,22 @@ +// Copyright (C) 2015-2022 The Neo Project. +// +// The Neo.Json is free software distributed under the MIT software license, +// see the accompanying file LICENSE in the main directory of the +// project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Diagnostics.CodeAnalysis; + +namespace Neo.Json.Serialization; + +class ByteArraySerializer : Serializer +{ + [return: NotNullIfNotNull("obj")] + protected internal override JString? Serialize(byte[]? obj) => obj is null ? null : Convert.ToBase64String(obj); + + [return: NotNullIfNotNull("json")] + protected internal override byte[]? Deserialize(JToken? json) => json is null ? null : Convert.FromBase64String(json.GetString()); +} diff --git a/src/Neo.Json/Serialization/CompositeSerializer.cs b/src/Neo.Json/Serialization/CompositeSerializer.cs new file mode 100644 index 0000000000..8683e77f01 --- /dev/null +++ b/src/Neo.Json/Serialization/CompositeSerializer.cs @@ -0,0 +1,73 @@ +// Copyright (C) 2015-2022 The Neo Project. +// +// The Neo.Json is free software distributed under the MIT software license, +// see the accompanying file LICENSE in the main directory of the +// project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.Serialization; + +namespace Neo.Json.Serialization; + +class CompositeSerializer : Serializer +{ + private static readonly (PropertyInfo, Serializer, Func, Action)[] properties; + + static CompositeSerializer() + { + properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(p => p.GetMethod is not null && p.SetMethod is not null) + .Select(p => + { + Serializer serializer = GetSerializer(p.PropertyType); + Type sType = serializer.GetType(); + MethodInfo serializeMethod = sType.GetMethod(nameof(Serialize), BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + MethodInfo deserializeMethod = sType.GetMethod(nameof(Deserialize), BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + var instanceExpr = Expression.Parameter(typeof(T)); + var serializerExpr = Expression.Parameter(typeof(Serializer)); + var callExpr = Expression.Call(instanceExpr, p.GetMethod!); + var convertExpr = Expression.Convert(serializerExpr, sType); + callExpr = Expression.Call(convertExpr, serializeMethod, callExpr); + var getterExpr = Expression.Lambda>(callExpr, instanceExpr, serializerExpr); + var valueExpr = Expression.Parameter(typeof(JToken)); + callExpr = Expression.Call(convertExpr, deserializeMethod, valueExpr); + callExpr = Expression.Call(instanceExpr, p.SetMethod!, callExpr); + var setterExpr = Expression.Lambda>(callExpr, instanceExpr, serializerExpr, valueExpr); + return (p, serializer, getterExpr.Compile(), setterExpr.Compile()); + }) + .ToArray(); + } + + [return: NotNullIfNotNull("obj")] + protected internal override JObject? Serialize(T? obj) + { + if (obj is null) return null; + JObject result = new(); + foreach (var (property, serializer, getter, _) in properties) + result[property.Name] = getter(obj, serializer); + return result; + } + + [return: NotNullIfNotNull("json")] + protected internal override T? Deserialize(JToken? json) + { + switch (json) + { + case JObject obj: + T result = (T)FormatterServices.GetUninitializedObject(typeof(T)); + foreach (var (property, serializer, _, setter) in properties) + setter(result, serializer, obj[property.Name]); + return result; + case null: + return default; + default: + throw new FormatException(); + } + } +} diff --git a/src/Neo.Json/Serialization/EnumSerializer.cs b/src/Neo.Json/Serialization/EnumSerializer.cs new file mode 100644 index 0000000000..1754fa3a02 --- /dev/null +++ b/src/Neo.Json/Serialization/EnumSerializer.cs @@ -0,0 +1,18 @@ +// Copyright (C) 2015-2022 The Neo Project. +// +// The Neo.Json is free software distributed under the MIT software license, +// see the accompanying file LICENSE in the main directory of the +// project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.Json.Serialization; + +class EnumSerializer : Serializer where T : unmanaged, Enum +{ + protected internal override JString Serialize(T obj) => obj.ToString(); + + protected internal override T Deserialize(JToken? json) => json!.GetEnum(); +} diff --git a/src/Neo.Json/Serialization/Int64Serializer.cs b/src/Neo.Json/Serialization/Int64Serializer.cs new file mode 100644 index 0000000000..9eee863bf8 --- /dev/null +++ b/src/Neo.Json/Serialization/Int64Serializer.cs @@ -0,0 +1,23 @@ +// Copyright (C) 2015-2022 The Neo Project. +// +// The Neo.Json is free software distributed under the MIT software license, +// see the accompanying file LICENSE in the main directory of the +// project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.Json.Serialization; + +class Int64Serializer : Serializer +{ + protected internal override JToken Serialize(long obj) => obj >= JNumber.MIN_SAFE_INTEGER && obj <= JNumber.MAX_SAFE_INTEGER ? obj : obj.ToString(); + + protected internal override long Deserialize(JToken? json) => json switch + { + JNumber n => (long)n.Value, + JString s => long.Parse(s.Value), + _ => throw new FormatException() + }; +} diff --git a/src/Neo.Json/Serialization/IntegerSerializer.cs b/src/Neo.Json/Serialization/IntegerSerializer.cs new file mode 100644 index 0000000000..f3dd458b89 --- /dev/null +++ b/src/Neo.Json/Serialization/IntegerSerializer.cs @@ -0,0 +1,23 @@ +// Copyright (C) 2015-2022 The Neo Project. +// +// The Neo.Json is free software distributed under the MIT software license, +// see the accompanying file LICENSE in the main directory of the +// project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.Json.Serialization; + +class IntegerSerializer : Serializer where T : unmanaged +{ + protected internal override JToken Serialize(T obj) => (JToken)Convert.ToDouble(obj); + + protected internal override T Deserialize(JToken? json) => json switch + { + JNumber n => (T)Convert.ChangeType(n.Value, typeof(T)), + JString s => (T)Convert.ChangeType(long.Parse(s.Value), typeof(T)), + _ => throw new FormatException() + }; +} diff --git a/src/Neo.Json/Serialization/Serializer.cs b/src/Neo.Json/Serialization/Serializer.cs new file mode 100644 index 0000000000..9ac44c147c --- /dev/null +++ b/src/Neo.Json/Serialization/Serializer.cs @@ -0,0 +1,90 @@ +// Copyright (C) 2015-2022 The Neo Project. +// +// The Neo.Json is free software distributed under the MIT software license, +// see the accompanying file LICENSE in the main directory of the +// project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Diagnostics.CodeAnalysis; + +namespace Neo.Json.Serialization; + +public abstract class Serializer +{ + private static readonly Dictionary serializers = new() + { + [typeof(bool)] = new BooleanSerializer(), + [typeof(sbyte)] = new IntegerSerializer(), + [typeof(byte)] = new IntegerSerializer(), + [typeof(short)] = new IntegerSerializer(), + [typeof(ushort)] = new IntegerSerializer(), + [typeof(int)] = new IntegerSerializer(), + [typeof(uint)] = new IntegerSerializer(), + [typeof(long)] = new Int64Serializer(), + [typeof(ulong)] = new UInt64Serializer(), + [typeof(string)] = new StringSerializer(), + [typeof(byte[])] = new ByteArraySerializer() + }; + + protected static Serializer GetSerializer(Type type) + { + if (!serializers.TryGetValue(type, out var serializer)) + { + Type? serializerAttribute = type.CustomAttributes + .Select(p => p.AttributeType) + .FirstOrDefault(p => p.IsGenericType && p.GetGenericTypeDefinition() == typeof(SerializerAttribute<>)); + if (serializerAttribute is not null) + serializer = (Serializer)Activator.CreateInstance(serializerAttribute.GenericTypeArguments[0])!; + else if (type.IsEnum) + serializer = (Serializer)Activator.CreateInstance(typeof(EnumSerializer<>).MakeGenericType(type))!; + else if (type.IsArray) + serializer = (Serializer)Activator.CreateInstance(typeof(ArraySerializer<>).MakeGenericType(type.GetElementType()!))!; + else + serializer = (Serializer)Activator.CreateInstance(typeof(CompositeSerializer<>).MakeGenericType(type))!; + serializers.Add(type, serializer); + } + return serializer; + } + + protected static Serializer GetSerializer() + { + return (Serializer)GetSerializer(typeof(T)); + } + + [return: NotNullIfNotNull("obj")] + public static JToken? Serialize(object? obj) + { + if (obj is null) return JToken.Null; + Serializer serializer = GetSerializer(obj.GetType()); + return serializer.SerializeObject(obj); + } + + public static JToken Serialize(T obj) where T : struct + { + Serializer serializer = GetSerializer(); + return serializer.Serialize(obj); + } + + [return: NotNullIfNotNull("json")] + public static T? Deserialize(JToken? json) + { + Serializer serializer = GetSerializer(); + return serializer.Deserialize(json); + } + + private protected abstract JToken SerializeObject(object obj); +} + +public abstract class Serializer : Serializer +{ + private protected sealed override JToken SerializeObject(object obj) => Serialize((T)obj); + + [return: NotNullIfNotNull("obj")] + protected internal abstract JToken? Serialize(T? obj); + + [return: NotNullIfNotNull("json")] + protected internal abstract T? Deserialize(JToken? json); +} diff --git a/src/Neo.Json/Serialization/SerializerAttribute.cs b/src/Neo.Json/Serialization/SerializerAttribute.cs new file mode 100644 index 0000000000..bc4c214a46 --- /dev/null +++ b/src/Neo.Json/Serialization/SerializerAttribute.cs @@ -0,0 +1,16 @@ +// Copyright (C) 2015-2022 The Neo Project. +// +// The Neo.Json is free software distributed under the MIT software license, +// see the accompanying file LICENSE in the main directory of the +// project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.Json.Serialization; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, Inherited = false)] +public class SerializerAttribute : Attribute where T : Serializer +{ +} diff --git a/src/Neo.Json/Serialization/StringSerializer.cs b/src/Neo.Json/Serialization/StringSerializer.cs new file mode 100644 index 0000000000..45db2d8a71 --- /dev/null +++ b/src/Neo.Json/Serialization/StringSerializer.cs @@ -0,0 +1,22 @@ +// Copyright (C) 2015-2022 The Neo Project. +// +// The Neo.Json is free software distributed under the MIT software license, +// see the accompanying file LICENSE in the main directory of the +// project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Diagnostics.CodeAnalysis; + +namespace Neo.Json.Serialization; + +class StringSerializer : Serializer +{ + [return: NotNullIfNotNull("json")] + protected internal override JString? Serialize(string? obj) => obj; + + [return: NotNullIfNotNull("json")] + protected internal override string? Deserialize(JToken? json) => json?.GetString(); +} diff --git a/src/Neo.Json/Serialization/UInt64Serializer.cs b/src/Neo.Json/Serialization/UInt64Serializer.cs new file mode 100644 index 0000000000..f0e197c26d --- /dev/null +++ b/src/Neo.Json/Serialization/UInt64Serializer.cs @@ -0,0 +1,23 @@ +// Copyright (C) 2015-2022 The Neo Project. +// +// The Neo.Json is free software distributed under the MIT software license, +// see the accompanying file LICENSE in the main directory of the +// project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.Json.Serialization; + +class UInt64Serializer : Serializer +{ + protected internal override JToken Serialize(ulong obj) => obj <= JNumber.MAX_SAFE_INTEGER ? obj : obj.ToString(); + + protected internal override ulong Deserialize(JToken? json) => json switch + { + JNumber n => (ulong)n.Value, + JString s => ulong.Parse(s.Value), + _ => throw new FormatException() + }; +}