| @@ -1,8 +1,12 @@ | |||
| namespace Discord | |||
| using Discord.Serialization; | |||
| namespace Discord | |||
| { | |||
| public enum PermissionTarget | |||
| { | |||
| [ModelEnum("role")] | |||
| Role, | |||
| [ModelEnum("user")] | |||
| User | |||
| } | |||
| } | |||
| @@ -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, | |||
| } | |||
| @@ -0,0 +1,220 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| namespace Discord.Serialization | |||
| { | |||
| internal class BufferDictionary<TValue> | |||
| { | |||
| 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<byte> 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<ReadOnlyBuffer<byte>, 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> x, ReadOnlyBuffer<byte> y) => x.Span.SequenceEqual(y.Span); | |||
| private bool KeyEquals(ReadOnlyBuffer<byte> x, ReadOnlySpan<byte> y) => x.Span.SequenceEqual(y); | |||
| private int GetKeyHashCode(ReadOnlyBuffer<byte> obj) => GetKeyHashCode(obj.Span); | |||
| private int GetKeyHashCode(ReadOnlySpan<byte> 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; | |||
| } | |||
| } | |||
| @@ -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<T> For<T>() where T : struct => EnumMap<T>.Instance; | |||
| } | |||
| public class EnumMap<T> | |||
| where T : struct | |||
| { | |||
| public static readonly EnumMap<T> Instance = new EnumMap<T>(); | |||
| private readonly BufferDictionary<T> _keyToValue; | |||
| private readonly Dictionary<T, string> _valueToKey; | |||
| private readonly Dictionary<T, ReadOnlyBuffer<byte>> _valueToUtf8Key; | |||
| public EnumMap() | |||
| { | |||
| var typeInfo = typeof(T).GetTypeInfo(); | |||
| if (!typeInfo.IsEnum) | |||
| throw new InvalidOperationException($"{typeInfo.Name} is not an Enum"); | |||
| _keyToValue = new BufferDictionary<T>(); | |||
| _valueToKey = new Dictionary<T, string>(); | |||
| _valueToUtf8Key = new Dictionary<T, ReadOnlyBuffer<byte>>(); | |||
| foreach (T val in Enum.GetValues(typeof(T)).OfType<T>()) | |||
| { | |||
| var fieldInfo = typeInfo.GetDeclaredField(Enum.GetName(typeof(T), val)); | |||
| var attr = fieldInfo.GetCustomAttribute<ModelEnumAttribute>(); | |||
| if (attr != null) | |||
| { | |||
| var key = new ReadOnlyBuffer<byte>(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<byte> 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<byte> 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<byte> GetUtf8Key(T value) | |||
| { | |||
| if (_valueToUtf8Key.TryGetValue(value, out var key)) | |||
| return key; | |||
| throw new SerializationException($"Unknown enum value: {value}"); | |||
| } | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,27 @@ | |||
| using System.Text.Json; | |||
| namespace Discord.Serialization.Json.Converters | |||
| { | |||
| internal class EnumPropertyConverter<T> : IJsonPropertyConverter<T> | |||
| where T : struct | |||
| { | |||
| private static readonly EnumMap<T> _map = EnumMap.For<T>(); | |||
| 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); | |||
| } | |||
| } | |||
| } | |||
| @@ -1,12 +0,0 @@ | |||
| using System.Text.Json; | |||
| namespace Discord.Serialization.Json.Converters | |||
| { | |||
| internal class EnumPropertyConverter<T> : IJsonPropertyConverter<T> | |||
| { | |||
| 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(); | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| @@ -7,211 +7,18 @@ namespace Discord.Serialization | |||
| public class ModelMap<TModel> | |||
| 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<byte> 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<PropertyMap> _dictionary; | |||
| public PropertyMap[] Properties { get; } | |||
| public ModelMap(List<PropertyMap> 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<byte> 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<byte> 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<byte> 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<PropertyMap>(properties.ToDictionary(x => x.Utf8Key)); | |||
| } | |||
| public bool TryGetProperty(ReadOnlyBuffer<byte> 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<byte> 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<byte> x, ReadOnlyBuffer<byte> y) => x.Span.SequenceEqual(y.Span); | |||
| private bool KeyEquals(ReadOnlyBuffer<byte> x, ReadOnlySpan<byte> y) => x.Span.SequenceEqual(y); | |||
| private int GetKeyHashCode(ReadOnlyBuffer<byte> obj) => GetKeyHashCode(obj.Span); | |||
| private int GetKeyHashCode(ReadOnlySpan<byte> 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); | |||
| } | |||
| } | |||
| @@ -2,6 +2,7 @@ | |||
| namespace Discord.Serialization | |||
| { | |||
| [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] | |||
| public class ModelPropertyAttribute : Attribute | |||
| { | |||
| public string Key { get; } | |||