From 894efcca0ac2fcc85318d0c5f022f5a1b63a77dc Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 8 Aug 2017 22:40:24 -0300 Subject: [PATCH] Added enum serialization --- .../Entities/Guilds/PermissionTarget.cs | 6 +- src/Discord.Net.Core/Entities/Users/UserStatus.cs | 8 +- src/Discord.Net.Serialization/BufferDictionary.cs | 220 +++++++++++++++++++++ src/Discord.Net.Serialization/EnumMap.cs | 76 +++++++ .../EnumValueAttribute.cs | 24 +++ .../Json/Converters/Enum.cs | 27 +++ .../Json/Converters/Primitives.Enum.cs | 12 -- src/Discord.Net.Serialization/Json/JsonFormat.cs | 2 +- src/Discord.Net.Serialization/ModelMap.cs | 201 +------------------ .../ModelPropertyAttribute.cs | 1 + 10 files changed, 365 insertions(+), 212 deletions(-) create mode 100644 src/Discord.Net.Serialization/BufferDictionary.cs create mode 100644 src/Discord.Net.Serialization/EnumMap.cs create mode 100644 src/Discord.Net.Serialization/EnumValueAttribute.cs create mode 100644 src/Discord.Net.Serialization/Json/Converters/Enum.cs delete mode 100644 src/Discord.Net.Serialization/Json/Converters/Primitives.Enum.cs diff --git a/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs b/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs index 96595fb69..f6479a0ae 100644 --- a/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs +++ b/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs @@ -1,8 +1,12 @@ -namespace Discord +using Discord.Serialization; + +namespace Discord { public enum PermissionTarget { + [ModelEnum("role")] Role, + [ModelEnum("user")] User } } diff --git a/src/Discord.Net.Core/Entities/Users/UserStatus.cs b/src/Discord.Net.Core/Entities/Users/UserStatus.cs index 74a52a0fa..7d02fe68c 100644 --- a/src/Discord.Net.Core/Entities/Users/UserStatus.cs +++ b/src/Discord.Net.Core/Entities/Users/UserStatus.cs @@ -1,11 +1,17 @@ -namespace Discord +using Discord.Serialization; + +namespace Discord { public enum UserStatus { Offline, + [ModelEnum("online")] Online, + [ModelEnum("idle")] Idle, + [ModelEnum("idle", EnumValueType.WriteOnly)] AFK, + [ModelEnum("dnd")] DoNotDisturb, Invisible, } diff --git a/src/Discord.Net.Serialization/BufferDictionary.cs b/src/Discord.Net.Serialization/BufferDictionary.cs new file mode 100644 index 000000000..4b7c35fab --- /dev/null +++ b/src/Discord.Net.Serialization/BufferDictionary.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Discord.Serialization +{ + internal class BufferDictionary + { + private struct PropertyEntry + { + public int hashCode; // Lower 31 bits of hash code, -1 if unused + public int next; // Index of next entry, -1 if last + public ReadOnlyBuffer key; // Key of entry + public TValue value; // Value of entry + } + + private int[] _buckets; + private PropertyEntry[] _entries; + private int _count; + private int _freeList; + private int _freeCount; + + public BufferDictionary() + { + int size = HashHelpers.GetPrime(0); + _buckets = new int[size]; + for (int i = 0; i < _buckets.Length; i++) _buckets[i] = -1; + _entries = new PropertyEntry[size]; + _freeList = -1; + } + public BufferDictionary(IReadOnlyDictionary, TValue> values) + { + int size = HashHelpers.GetPrime(values.Count); + _buckets = new int[size]; + for (int i = 0; i < _buckets.Length; i++) _buckets[i] = -1; + _entries = new PropertyEntry[size]; + _freeList = -1; + + foreach (var value in values) + Add(value.Key, value.Value); + } + + public void Add(ReadOnlyBuffer key, TValue value) + { + int hashCode = GetKeyHashCode(key) & 0x7FFFFFFF; + int targetBucket = hashCode % _buckets.Length; + + for (int i = _buckets[targetBucket]; i >= 0; i = _entries[i].next) + { + if (_entries[i].hashCode == hashCode && KeyEquals(_entries[i].key, key)) + throw new ArgumentException("Duplicate key", nameof(key)); + } + int index; + if (_freeCount > 0) + { + index = _freeList; + _freeList = _entries[index].next; + _freeCount--; + } + else + { + if (_count == _entries.Length) + { + Resize(); + targetBucket = hashCode % _buckets.Length; + } + index = _count; + _count++; + } + + _entries[index].hashCode = hashCode; + _entries[index].next = _buckets[targetBucket]; + _entries[index].key = key; + _entries[index].value = value; + _buckets[targetBucket] = index; + } + private void Resize() + { + int newSize = HashHelpers.ExpandPrime(_count); + + var newBuckets = new int[newSize]; + for (int i = 0; i < newBuckets.Length; i++) + newBuckets[i] = -1; + + var newEntries = new PropertyEntry[newSize]; + Array.Copy(_entries, 0, newEntries, 0, _count); + + for (int i = 0; i < _count; i++) + { + if (newEntries[i].hashCode >= 0) + { + int bucket = newEntries[i].hashCode % newSize; + newEntries[i].next = newBuckets[bucket]; + newBuckets[bucket] = i; + } + } + _buckets = newBuckets; + _entries = newEntries; + } + + private int FindEntry(ReadOnlyBuffer key) + { + if (_buckets != null) + { + int hashCode = GetKeyHashCode(key) & 0x7FFFFFFF; + for (int i = _buckets[hashCode % _buckets.Length]; i >= 0; i = _entries[i].next) + { + if (_entries[i].hashCode == hashCode && KeyEquals(_entries[i].key, key)) + return i; + } + } + return -1; + } + private int FindEntry(ReadOnlySpan key) + { + if (_buckets != null) + { + int hashCode = GetKeyHashCode(key) & 0x7FFFFFFF; + for (int i = _buckets[hashCode % _buckets.Length]; i >= 0; i = _entries[i].next) + { + if (_entries[i].hashCode == hashCode && KeyEquals(_entries[i].key, key)) + return i; + } + } + return -1; + } + + public bool TryGetValue(ReadOnlyBuffer key, out TValue value) + { + int i = FindEntry(key); + if (i >= 0) + { + value = _entries[i].value; + return true; + } + value = default; + return false; + } + public bool TryGetValue(ReadOnlySpan key, out TValue value) + { + int i = FindEntry(key); + if (i >= 0) + { + value = _entries[i].value; + return true; + } + value = default; + return false; + } + + private bool KeyEquals(ReadOnlyBuffer x, ReadOnlyBuffer y) => x.Span.SequenceEqual(y.Span); + private bool KeyEquals(ReadOnlyBuffer x, ReadOnlySpan y) => x.Span.SequenceEqual(y); + + private int GetKeyHashCode(ReadOnlyBuffer obj) => GetKeyHashCode(obj.Span); + private int GetKeyHashCode(ReadOnlySpan obj) + { + //From Utf8String + //TODO: Replace when they do + unchecked + { + if (obj.Length <= 4) + { + int hash = obj.Length; + for (int i = 0; i < obj.Length; i++) + { + hash <<= 8; + hash ^= obj[i]; + } + return hash; + } + else + { + int hash = obj.Length; + hash ^= obj[0]; + hash <<= 8; + hash ^= obj[1]; + hash <<= 8; + hash ^= obj[obj.Length - 2]; + hash <<= 8; + hash ^= obj[obj.Length - 1]; + return hash; + } + } + } + } + + internal static class HashHelpers + { + public static readonly int[] primes = { + 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, + 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, + 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437, + 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, + 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369, 8639249, 10367101, + 12440537, 14928671, 17914409, 21497293, 25796759, 30956117, 37147349, 44576837, 53492207, 64190669, + 77028803, 92434613, 110921543, 133105859, 159727031, 191672443, 230006941, 276008387, 331210079, + 397452101, 476942527, 572331049, 686797261, 824156741, 988988137, 1186785773, 1424142949, 1708971541, + 2050765853, MaxPrimeArrayLength }; + + public static int GetPrime(int min) + { + for (int i = 0; i < primes.Length; i++) + { + int prime = primes[i]; + if (prime >= min) return prime; + } + return min; + } + + public static int ExpandPrime(int oldSize) + { + int newSize = 2 * oldSize; + if ((uint)newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > oldSize) + return MaxPrimeArrayLength; + return GetPrime(newSize); + } + + public const int MaxPrimeArrayLength = 0x7FEFFFFD; + } +} diff --git a/src/Discord.Net.Serialization/EnumMap.cs b/src/Discord.Net.Serialization/EnumMap.cs new file mode 100644 index 000000000..80aaef942 --- /dev/null +++ b/src/Discord.Net.Serialization/EnumMap.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Utf8; + +namespace Discord.Serialization +{ + internal static class EnumMap + { + public static EnumMap For() where T : struct => EnumMap.Instance; + } + + public class EnumMap + where T : struct + { + public static readonly EnumMap Instance = new EnumMap(); + + private readonly BufferDictionary _keyToValue; + private readonly Dictionary _valueToKey; + private readonly Dictionary> _valueToUtf8Key; + + public EnumMap() + { + var typeInfo = typeof(T).GetTypeInfo(); + if (!typeInfo.IsEnum) + throw new InvalidOperationException($"{typeInfo.Name} is not an Enum"); + + _keyToValue = new BufferDictionary(); + _valueToKey = new Dictionary(); + _valueToUtf8Key = new Dictionary>(); + + foreach (T val in Enum.GetValues(typeof(T)).OfType()) + { + var fieldInfo = typeInfo.GetDeclaredField(Enum.GetName(typeof(T), val)); + var attr = fieldInfo.GetCustomAttribute(); + if (attr != null) + { + var key = new ReadOnlyBuffer(new Utf8String(attr.Key).Bytes.ToArray()); + if (attr.Type != EnumValueType.WriteOnly) + _keyToValue.Add(key, val); + if (attr.Type != EnumValueType.ReadOnly) + { + _valueToUtf8Key.Add(val, key); + _valueToKey.Add(val, attr.Key); + } + } + } + } + + public T GetValue(ReadOnlyBuffer key) + { + if (_keyToValue.TryGetValue(key, out var value)) + return value; + throw new SerializationException($"Unknown enum key: {new Utf8String(key.Span).ToString()}"); + } + public T GetValue(ReadOnlySpan key) + { + if (_keyToValue.TryGetValue(key, out var value)) + return value; + throw new SerializationException($"Unknown enum key: {new Utf8String(key).ToString()}"); + } + public string GetKey(T value) + { + if (_valueToKey.TryGetValue(value, out var key)) + return key; + throw new SerializationException($"Unknown enum value: {value}"); + } + public ReadOnlyBuffer GetUtf8Key(T value) + { + if (_valueToUtf8Key.TryGetValue(value, out var key)) + return key; + throw new SerializationException($"Unknown enum value: {value}"); + } + } +} diff --git a/src/Discord.Net.Serialization/EnumValueAttribute.cs b/src/Discord.Net.Serialization/EnumValueAttribute.cs new file mode 100644 index 000000000..8b5e235f0 --- /dev/null +++ b/src/Discord.Net.Serialization/EnumValueAttribute.cs @@ -0,0 +1,24 @@ +using System; + +namespace Discord.Serialization +{ + public enum EnumValueType + { + ReadWrite, + ReadOnly, + WriteOnly + } + + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class ModelEnumAttribute : Attribute + { + public string Key { get; } + public EnumValueType Type { get; } + + public ModelEnumAttribute(string key, EnumValueType type = EnumValueType.ReadWrite) + { + Key = key; + Type = type; + } + } +} diff --git a/src/Discord.Net.Serialization/Json/Converters/Enum.cs b/src/Discord.Net.Serialization/Json/Converters/Enum.cs new file mode 100644 index 000000000..c320b5018 --- /dev/null +++ b/src/Discord.Net.Serialization/Json/Converters/Enum.cs @@ -0,0 +1,27 @@ +using System.Text.Json; + +namespace Discord.Serialization.Json.Converters +{ + internal class EnumPropertyConverter : IJsonPropertyConverter + where T : struct + { + private static readonly EnumMap _map = EnumMap.For(); + + public T Read(PropertyMap map, ref JsonReader reader, bool isTopLevel) + { + if (isTopLevel) + reader.Read(); + if (reader.ValueType != JsonValueType.String) + throw new SerializationException("Bad input, expected String"); + return _map.GetValue(reader.Value); + } + public void Write(PropertyMap map, ref JsonWriter writer, T value, bool isTopLevel) + { + string key = _map.GetKey(value); + if (isTopLevel) + writer.WriteAttribute(map.Key, key); + else + writer.WriteValue(key); + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Serialization/Json/Converters/Primitives.Enum.cs b/src/Discord.Net.Serialization/Json/Converters/Primitives.Enum.cs deleted file mode 100644 index de87c6b8d..000000000 --- a/src/Discord.Net.Serialization/Json/Converters/Primitives.Enum.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json; - -namespace Discord.Serialization.Json.Converters -{ - internal class EnumPropertyConverter : IJsonPropertyConverter - { - public T Read(PropertyMap map, ref JsonReader reader, bool isTopLevel) - => throw new System.NotImplementedException(); - public void Write(PropertyMap map, ref JsonWriter writer, T value, bool isTopLevel) - => throw new System.NotImplementedException(); - } -} diff --git a/src/Discord.Net.Serialization/Json/JsonFormat.cs b/src/Discord.Net.Serialization/Json/JsonFormat.cs index 3126d0814..074b39c7c 100644 --- a/src/Discord.Net.Serialization/Json/JsonFormat.cs +++ b/src/Discord.Net.Serialization/Json/JsonFormat.cs @@ -38,7 +38,7 @@ namespace Discord.Serialization.Json AddGenericConverter(typeof(List<>), typeof(Converters.ListPropertyConverter<>)); AddGenericConverter(typeof(Nullable<>), typeof(Converters.NullablePropertyConverter<>)); - //AddGenericConverter(typeof(Converters.EnumPropertyConverter<>), (type, prop) => type.IsEnum); //TODO: Impl Enums + AddGenericConverter(typeof(Converters.EnumPropertyConverter<>), (type, prop) => type.IsEnum); AddGenericConverter(typeof(Converters.ObjectPropertyConverter<>), (type, prop) => type.IsClass); } diff --git a/src/Discord.Net.Serialization/ModelMap.cs b/src/Discord.Net.Serialization/ModelMap.cs index 9e05cb891..29657aa22 100644 --- a/src/Discord.Net.Serialization/ModelMap.cs +++ b/src/Discord.Net.Serialization/ModelMap.cs @@ -7,211 +7,18 @@ namespace Discord.Serialization public class ModelMap where TModel : class, new() { - private struct PropertyEntry - { - public int hashCode; // Lower 31 bits of hash code, -1 if unused - public int next; // Index of next entry, -1 if last - public ReadOnlyBuffer key; // Key of entry - public PropertyMap value; // Value of entry - } - - private int[] _buckets; - private PropertyEntry[] _entries; - private int _count; - private int _freeList; - private int _freeCount; - + private BufferDictionary _dictionary; public PropertyMap[] Properties { get; } public ModelMap(List properties) { Properties = properties.ToArray(); - - int size = HashHelpers.GetPrime(Properties.Length); - _buckets = new int[size]; - for (int i = 0; i < _buckets.Length; i++) _buckets[i] = -1; - _entries = new PropertyEntry[size]; - _freeList = -1; - - for (int i = 0; i < properties.Count; i++) - AddProperty(properties[i].Utf8Key, properties[i]); - } - - public void AddProperty(ReadOnlyBuffer key, PropertyMap value) - { - int hashCode = GetKeyHashCode(key) & 0x7FFFFFFF; - int targetBucket = hashCode % _buckets.Length; - - for (int i = _buckets[targetBucket]; i >= 0; i = _entries[i].next) - { - if (_entries[i].hashCode == hashCode && KeyEquals(_entries[i].key, key)) - throw new ArgumentException("Duplicate property", nameof(key)); - } - int index; - if (_freeCount > 0) - { - index = _freeList; - _freeList = _entries[index].next; - _freeCount--; - } - else - { - if (_count == _entries.Length) - { - Resize(); - targetBucket = hashCode % _buckets.Length; - } - index = _count; - _count++; - } - - _entries[index].hashCode = hashCode; - _entries[index].next = _buckets[targetBucket]; - _entries[index].key = key; - _entries[index].value = value; - _buckets[targetBucket] = index; - } - private void Resize() - { - int newSize = HashHelpers.ExpandPrime(_count); - - var newBuckets = new int[newSize]; - for (int i = 0; i < newBuckets.Length; i++) - newBuckets[i] = -1; - - var newEntries = new PropertyEntry[newSize]; - Array.Copy(_entries, 0, newEntries, 0, _count); - - for (int i = 0; i < _count; i++) - { - if (newEntries[i].hashCode >= 0) - { - int bucket = newEntries[i].hashCode % newSize; - newEntries[i].next = newBuckets[bucket]; - newBuckets[bucket] = i; - } - } - _buckets = newBuckets; - _entries = newEntries; - } - - private int FindEntry(ReadOnlyBuffer key) - { - if (_buckets != null) - { - int hashCode = GetKeyHashCode(key) & 0x7FFFFFFF; - for (int i = _buckets[hashCode % _buckets.Length]; i >= 0; i = _entries[i].next) - { - if (_entries[i].hashCode == hashCode && KeyEquals(_entries[i].key, key)) - return i; - } - } - return -1; - } - private int FindEntry(ReadOnlySpan key) - { - if (_buckets != null) - { - int hashCode = GetKeyHashCode(key) & 0x7FFFFFFF; - for (int i = _buckets[hashCode % _buckets.Length]; i >= 0; i = _entries[i].next) - { - if (_entries[i].hashCode == hashCode && KeyEquals(_entries[i].key, key)) - return i; - } - } - return -1; + _dictionary = new BufferDictionary(properties.ToDictionary(x => x.Utf8Key)); } public bool TryGetProperty(ReadOnlyBuffer key, out PropertyMap value) - { - int i = FindEntry(key); - if (i >= 0) - { - value = _entries[i].value; - return true; - } - value = default; - return false; - } + => _dictionary.TryGetValue(key, out value); public bool TryGetProperty(ReadOnlySpan key, out PropertyMap value) - { - int i = FindEntry(key); - if (i >= 0) - { - value = _entries[i].value; - return true; - } - value = default; - return false; - } - - private bool KeyEquals(ReadOnlyBuffer x, ReadOnlyBuffer y) => x.Span.SequenceEqual(y.Span); - private bool KeyEquals(ReadOnlyBuffer x, ReadOnlySpan y) => x.Span.SequenceEqual(y); - - private int GetKeyHashCode(ReadOnlyBuffer obj) => GetKeyHashCode(obj.Span); - private int GetKeyHashCode(ReadOnlySpan obj) - { - //From Utf8String - //TODO: Replace when they do - unchecked - { - if (obj.Length <= 4) - { - int hash = obj.Length; - for (int i = 0; i < obj.Length; i++) - { - hash <<= 8; - hash ^= obj[i]; - } - return hash; - } - else - { - int hash = obj.Length; - hash ^= obj[0]; - hash <<= 8; - hash ^= obj[1]; - hash <<= 8; - hash ^= obj[obj.Length - 2]; - hash <<= 8; - hash ^= obj[obj.Length - 1]; - return hash; - } - } - } - } - - internal static class HashHelpers - { - public static readonly int[] primes = { - 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, - 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, - 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437, - 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, - 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369, 8639249, 10367101, - 12440537, 14928671, 17914409, 21497293, 25796759, 30956117, 37147349, 44576837, 53492207, 64190669, - 77028803, 92434613, 110921543, 133105859, 159727031, 191672443, 230006941, 276008387, 331210079, - 397452101, 476942527, 572331049, 686797261, 824156741, 988988137, 1186785773, 1424142949, 1708971541, - 2050765853, MaxPrimeArrayLength }; - - public static int GetPrime(int min) - { - for (int i = 0; i < primes.Length; i++) - { - int prime = primes[i]; - if (prime >= min) return prime; - } - return min; - } - - public static int ExpandPrime(int oldSize) - { - int newSize = 2 * oldSize; - if ((uint)newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > oldSize) - return MaxPrimeArrayLength; - return GetPrime(newSize); - } - - public const int MaxPrimeArrayLength = 0x7FEFFFFD; + => _dictionary.TryGetValue(key, out value); } } \ No newline at end of file diff --git a/src/Discord.Net.Serialization/ModelPropertyAttribute.cs b/src/Discord.Net.Serialization/ModelPropertyAttribute.cs index c1ceeca73..338c7a857 100644 --- a/src/Discord.Net.Serialization/ModelPropertyAttribute.cs +++ b/src/Discord.Net.Serialization/ModelPropertyAttribute.cs @@ -2,6 +2,7 @@ namespace Discord.Serialization { + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class ModelPropertyAttribute : Attribute { public string Key { get; }