diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index bfc2293ee..833ec7d6e 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -27,8 +27,8 @@ namespace Discord.API static DiscordRestApiClient() { SerializationFormat.Json.AddConverter(); - SerializationFormat.Json.AddConverter(info => info.GetCustomAttribute() != null); - SerializationFormat.Json.AddConverter(info => info.GetCustomAttribute() != null); + SerializationFormat.Json.AddConverter((type, prop) => prop?.GetCustomAttribute() != null); + SerializationFormat.Json.AddConverter((type, prop) => prop?.GetCustomAttribute() != null); SerializationFormat.Json.AddGenericConverter(typeof(EntityOrId<>), typeof(EntityOrIdPropertyConverter<>)); SerializationFormat.Json.AddGenericConverter(typeof(Optional<>), typeof(OptionalPropertyConverter<>)); } diff --git a/src/Discord.Net.Serialization/ConverterCollection.cs b/src/Discord.Net.Serialization/ConverterCollection.cs index 71c416589..d6e32ecc4 100644 --- a/src/Discord.Net.Serialization/ConverterCollection.cs +++ b/src/Discord.Net.Serialization/ConverterCollection.cs @@ -10,85 +10,116 @@ namespace Discord.Serialization private class ConverterTypeCollection { public Type DefaultConverterType; - public List<(Func Condition, Type ConverterType)> Conditionals = new List<(Func, Type)>(); + public List<(Func Condition, Type ConverterType)> Conditionals = new List<(Func, Type)>(); } private static readonly MethodInfo _getConverterMethod = typeof(ConverterCollection).GetTypeInfo().GetDeclaredMethod(nameof(Get)); - private readonly ConcurrentDictionary _maps = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _types = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _genericTypes = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + private readonly Dictionary _types = new Dictionary(); + private readonly Dictionary _mappedGenericTypes = new Dictionary(); + private readonly ConverterTypeCollection _genericTypes = new ConverterTypeCollection(); internal ConverterCollection() { } public void Add() { - var converters = _types.GetOrAdd(typeof(TType), _ => new ConverterTypeCollection()); + if (!_types.TryGetValue(typeof(TType), out var converters)) + _types.Add(typeof(TType), converters = new ConverterTypeCollection()); converters.DefaultConverterType = typeof(TConverter); } - public void Add(Func condition) + public void Add(Func condition) { - var converters = _types.GetOrAdd(typeof(TType), _ => new ConverterTypeCollection()); + if (!_types.TryGetValue(typeof(TType), out var converters)) + _types.Add(typeof(TType), converters = new ConverterTypeCollection()); converters.Conditionals.Add((condition, typeof(TConverter))); } + public void AddGeneric(Type openConverterType) + { + if (openConverterType.IsConstructedGenericType) throw new InvalidOperationException($"{nameof(openConverterType)} must be an open generic"); + _genericTypes.DefaultConverterType = openConverterType; + } + public void AddGeneric(Type openConverterType, Func condition) + { + if (openConverterType.IsConstructedGenericType) throw new InvalidOperationException($"{nameof(openConverterType)} must be an open generic"); + _genericTypes.Conditionals.Add((condition, openConverterType)); + } public void AddGeneric(Type openType, Type openConverterType) { if (openType.IsConstructedGenericType) throw new InvalidOperationException($"{nameof(openType)} must be an open generic"); if (openConverterType.IsConstructedGenericType) throw new InvalidOperationException($"{nameof(openConverterType)} must be an open generic"); - var converters = _genericTypes.GetOrAdd(openType, _ => new ConverterTypeCollection()); + if (!_mappedGenericTypes.TryGetValue(openType, out var converters)) + _mappedGenericTypes.Add(openType, converters = new ConverterTypeCollection()); converters.DefaultConverterType = openConverterType; } - public void AddGeneric(Type openType, Type openConverterType, Func condition) + public void AddGeneric(Type openType, Type openConverterType, Func condition) { if (openType.IsConstructedGenericType) throw new InvalidOperationException($"{nameof(openType)} must be an open generic"); if (openConverterType.IsConstructedGenericType) throw new InvalidOperationException($"{nameof(openConverterType)} must be an open generic"); - var converters = _genericTypes.GetOrAdd(openType, _ => new ConverterTypeCollection()); + if (!_mappedGenericTypes.TryGetValue(openType, out var converters)) + _mappedGenericTypes.Add(openType, converters = new ConverterTypeCollection()); converters.Conditionals.Add((condition, openConverterType)); } - public object Get(PropertyInfo propInfo) + public object Get(PropertyInfo propInfo = null) { - var typeInfo = typeof(TType).GetTypeInfo(); - - //Generic converters - if (typeInfo.IsGenericType) + return _cache.GetOrAdd(typeof(TType), _ => { - var converterType = FindConverterType(typeInfo.GetGenericTypeDefinition(), _genericTypes, propInfo); - if (converterType != null) + TypeInfo typeInfo = typeof(TType).GetTypeInfo(); + + //Mapped generic converters (List -> CollectionPropertyConverter) + if (typeInfo.IsGenericType) { - var innerType = typeInfo.GenericTypeArguments[0]; - converterType = converterType.MakeGenericType(innerType); - object innerConverter = GetInnerConverter(innerType, propInfo); - return Activator.CreateInstance(converterType, innerConverter); + var converterType = FindConverterType(typeInfo.GetGenericTypeDefinition(), _mappedGenericTypes, typeInfo, propInfo); + if (converterType != null) + { + var innerType = typeInfo.GenericTypeArguments[0]; + converterType = converterType.MakeGenericType(innerType); + object innerConverter = GetInnerConverter(innerType, propInfo); + return Activator.CreateInstance(converterType, innerConverter); + } } - } - //Normal converters - { - var converterType = FindConverterType(typeof(TType), _types, propInfo); - if (converterType != null) - return Activator.CreateInstance(converterType); - } + //Normal converters (bool -> BooleanPropertyConverter) + { + var converterType = FindConverterType(typeof(TType), _types, typeInfo, propInfo); + if (converterType != null) + return Activator.CreateInstance(converterType); + } + + //Generic converters (Model -> ObjectPropertyConverter) + { + var converterType = FindConverterType(_genericTypes, typeInfo, propInfo); + if (converterType != null) + { + converterType = converterType.MakeGenericType(typeof(TType)); + return Activator.CreateInstance(converterType); + } + } - throw new InvalidOperationException($"Unsupported model type: {typeof(TType).Name}"); + throw new InvalidOperationException($"Unsupported model type: {typeof(TType).Name}"); + }); } private object GetInnerConverter(Type type, PropertyInfo propInfo) => _getConverterMethod.MakeGenericMethod(type).Invoke(this, new object[] { propInfo }); - private Type FindConverterType(Type type, ConcurrentDictionary collection, PropertyInfo propInfo) + private Type FindConverterType(Type type, Dictionary collection, TypeInfo typeInfo, PropertyInfo propInfo) { if (collection.TryGetValue(type, out var converters)) + return FindConverterType(converters, typeInfo, propInfo); + return null; + } + private Type FindConverterType(ConverterTypeCollection converters, TypeInfo typeInfo, PropertyInfo propInfo) + { + for (int i = 0; i < converters.Conditionals.Count; i++) { - for (int i = 0; i < converters.Conditionals.Count; i++) - { - if (converters.Conditionals[i].Condition(propInfo)) - return converters.Conditionals[i].ConverterType; - } - if (converters.DefaultConverterType != null) - return converters.DefaultConverterType; + if (converters.Conditionals[i].Condition(typeInfo, propInfo)) + return converters.Conditionals[i].ConverterType; } + if (converters.DefaultConverterType != null) + return converters.DefaultConverterType; return null; } } diff --git a/src/Discord.Net.Serialization/Json/Converters/Object.cs b/src/Discord.Net.Serialization/Json/Converters/Object.cs new file mode 100644 index 000000000..b810702e9 --- /dev/null +++ b/src/Discord.Net.Serialization/Json/Converters/Object.cs @@ -0,0 +1,45 @@ +using System.Text.Json; + +namespace Discord.Serialization.Json.Converters +{ + internal class ObjectPropertyConverter : IJsonPropertyConverter + where T : class, new() + { + private static readonly ModelMap _map = SerializationFormat.Json.MapModel(); + + public T Read(PropertyMap map, JsonReader reader, bool isTopLevel) + { + var model = new T(); + + if ((isTopLevel && !reader.Read()) || reader.TokenType != JsonTokenType.StartObject) + throw new SerializationException("Bad input, expected StartObject"); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + return model; + if (reader.TokenType != JsonTokenType.PropertyName) + throw new SerializationException("Bad input, expected PropertyName"); + + string key = reader.ParseString(); + if (_map.PropertiesByKey.TryGetValue(key, out var property)) + (property as IJsonPropertyMap).Read(model, reader); + else + reader.Skip(); //Unknown property, skip + + if (!reader.Read()) + throw new SerializationException("Bad input, expected Value"); + } + throw new SerializationException("Bad input, expected EndObject"); + } + public void Write(PropertyMap map, JsonWriter writer, T value, bool isTopLevel) + { + if (isTopLevel) + writer.WriteObjectStart(map.Key); + else + writer.WriteObjectStart(); + for (int i = 0; i < _map.Properties.Length; i++) + (_map.Properties[i] as IJsonPropertyMap).Write(value, writer); + writer.WriteObjectEnd(); + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Serialization/Json/JsonFormat.cs b/src/Discord.Net.Serialization/Json/JsonFormat.cs index 8b55f150d..3048ca97e 100644 --- a/src/Discord.Net.Serialization/Json/JsonFormat.cs +++ b/src/Discord.Net.Serialization/Json/JsonFormat.cs @@ -1,4 +1,5 @@ -using System; +using Discord.Serialization.Json.Converters; +using System; using System.Collections.Generic; using System.Reflection; using System.Text; @@ -37,20 +38,26 @@ namespace Discord.Serialization.Json AddGenericConverter(typeof(List<>), typeof(Converters.ListPropertyConverter<>)); AddGenericConverter(typeof(Nullable<>), typeof(Converters.NullablePropertyConverter<>)); + AddGenericConverter(typeof(Converters.ObjectPropertyConverter<>), (type, prop) => type.IsClass); + //AddEnumConverter(); } public void AddConverter() where TConverter : class, IJsonPropertyConverter => _converters.Add(); - public void AddConverter(Func condition) + public void AddConverter(Func condition) where TConverter : class, IJsonPropertyConverter => _converters.Add(condition); + public void AddGenericConverter(Type converter) + => _converters.AddGeneric(converter); + public void AddGenericConverter(Type converter, Func condition) + => _converters.AddGeneric(converter, condition); public void AddGenericConverter(Type value, Type converter) => _converters.AddGeneric(value, converter); - public void AddGenericConverter(Type value, Type converter, Func condition) - => _converters.AddGeneric(value, converter); + public void AddGenericConverter(Type value, Type converter, Func condition) + => _converters.AddGeneric(value, converter, condition); protected override PropertyMap CreatePropertyMap(PropertyInfo propInfo) { @@ -61,39 +68,17 @@ namespace Discord.Serialization.Json protected internal override TModel Read(Serializer serializer, ReadOnlyBuffer data) { var reader = new JsonReader(data.Span, SymbolTable.InvariantUtf8); - var map = MapModel(); - var model = new TModel(); - - if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject) - throw new InvalidOperationException("Bad input, expected StartObject"); - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.EndObject) - return model; - if (reader.TokenType != JsonTokenType.PropertyName) - throw new InvalidOperationException("Bad input, expected PropertyName"); - - string key = reader.ParseString(); - if (map.PropertiesByKey.TryGetValue(key, out var property)) - (property as IJsonPropertyMap).Read(model, reader); - else - reader.Skip(); //Unknown property, skip - - if (!reader.Read()) - throw new InvalidOperationException("Bad input, expected Value"); - } - throw new InvalidOperationException("Bad input, expected EndObject"); + if (!reader.Read()) + return null; + var converter = _converters.Get() as IJsonPropertyConverter; + return converter.Read(null, reader, false); } protected internal override void Write(Serializer serializer, ArrayFormatter stream, TModel model) { var writer = new JsonWriter(stream); - var map = MapModel(); - - writer.WriteObjectStart(); - for (int i = 0; i < map.Properties.Length; i++) - (map.Properties[i] as IJsonPropertyMap).Write(model, writer); - writer.WriteObjectEnd(); + var converter = _converters.Get() as IJsonPropertyConverter; + converter.Write(null, writer, model, false); } } } diff --git a/src/Discord.Net.Serialization/SerializationFormat.cs b/src/Discord.Net.Serialization/SerializationFormat.cs index f53b73153..afdd62de5 100644 --- a/src/Discord.Net.Serialization/SerializationFormat.cs +++ b/src/Discord.Net.Serialization/SerializationFormat.cs @@ -19,7 +19,7 @@ namespace Discord.Serialization protected readonly ConcurrentDictionary _maps = new ConcurrentDictionary(); protected readonly ConverterCollection _converters = new ConverterCollection(); - protected ModelMap MapModel() + protected internal ModelMap MapModel() where TModel : class, new() { return _maps.GetOrAdd(typeof(TModel), _ =>