From 00cf546d6d4234e5230c35c43581af1300509626 Mon Sep 17 00:00:00 2001 From: Quahu Date: Mon, 29 Jul 2024 18:58:16 +0200 Subject: [PATCH] Implemented STJ --- src/Disqord.Core/Disqord.Core.csproj | 2 +- .../TransientSlashCommandOptionChoice.cs | 2 +- .../TransientSlashCommandInteractionOption.cs | 2 +- ...ApplicationCommandOptionChoiceJsonModel.cs | 8 +- .../Json/Default/DefaultJsonSerializer.cs | 4 +- .../Json/Default/Entities/ContractResolver.cs | 2 +- .../Json/Default/Nodes/DefaultJsonArray.cs | 62 +++++++- .../Json/Default/Nodes/DefaultJsonNode.cs | 43 ++--- .../Json/Default/Nodes/DefaultJsonObject.cs | 58 ++++++- .../Json/Default/Nodes/DefaultJsonValue.cs | 15 +- .../Serialization/Json/JsonModel.cs | 59 +------ .../Serialization/Json/Nodes/IJsonArray.cs | 4 +- .../Serialization/Json/Nodes/IJsonNode.cs | 11 +- .../Serialization/Json/Nodes/IJsonObject.cs | 2 +- .../Serialization/Json/Nodes/IJsonValue.cs | 5 +- .../STJ/Entities/Converters/EnumConverter.cs | 67 ++++++++ .../Entities/Converters/JsonNodeConverter.cs | 16 +- .../Entities/Converters/NullableConverter.cs | 37 +++++ .../Entities/Converters/OptionalConverter.cs | 22 ++- .../Entities/Converters/SnowflakeConverter.cs | 21 ++- .../SnowflakeDictionaryConverter`1.cs | 64 ++++++++ .../Entities/Converters/StreamConverter.cs | 83 +--------- .../Json/STJ/Entities/JsonTypeInfoResolver.cs | 138 ++++++++++++---- .../Serialization/Json/STJ/JsonUtilities.cs | 33 ++++ .../Json/STJ/Nodes/SystemJsonArray.cs | 150 +++++++++++++----- .../Json/STJ/Nodes/SystemJsonNode.cs | 65 ++++++-- .../Json/STJ/Nodes/SystemJsonObject.cs | 90 +++++++++-- .../Json/STJ/Nodes/SystemJsonValue.cs | 25 +-- .../Json/STJ/SystemJsonSerializer.cs | 21 +-- .../Default/Sharding/DefaultShard.cs | 4 +- .../Dispatcher/DefaultGatewayDispatcher.cs | 3 +- .../Dispatches/Voice/VOICE_STATE_UPDATE.cs | 4 +- src/Disqord.Rest.Api/RestApiException.cs | 2 +- src/Disqord.TestBot/Program.cs | 11 ++ src/Disqord.targets | 2 +- 35 files changed, 821 insertions(+), 316 deletions(-) create mode 100644 src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/EnumConverter.cs create mode 100644 src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/NullableConverter.cs create mode 100644 src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/SnowflakeDictionaryConverter`1.cs create mode 100644 src/Disqord.Core/Serialization/Json/STJ/JsonUtilities.cs diff --git a/src/Disqord.Core/Disqord.Core.csproj b/src/Disqord.Core/Disqord.Core.csproj index 998bbc2e9..7fb3f1bac 100644 --- a/src/Disqord.Core/Disqord.Core.csproj +++ b/src/Disqord.Core/Disqord.Core.csproj @@ -13,6 +13,6 @@ - + diff --git a/src/Disqord.Core/Entities/Transient/ApplicationCommands/Slash/TransientSlashCommandOptionChoice.cs b/src/Disqord.Core/Entities/Transient/ApplicationCommands/Slash/TransientSlashCommandOptionChoice.cs index 209d12b2a..b9d616692 100644 --- a/src/Disqord.Core/Entities/Transient/ApplicationCommands/Slash/TransientSlashCommandOptionChoice.cs +++ b/src/Disqord.Core/Entities/Transient/ApplicationCommands/Slash/TransientSlashCommandOptionChoice.cs @@ -25,7 +25,7 @@ public IReadOnlyDictionary NameLocalizations } /// - public object Value => Model.Value.Value!; + public object Value => Model.Value.GetValue()!; public TransientSlashCommandOptionChoice(IClient client, ApplicationCommandOptionChoiceJsonModel model) : base(client, model) diff --git a/src/Disqord.Core/Entities/Transient/Interactions/Commands/Slash/TransientSlashCommandInteractionOption.cs b/src/Disqord.Core/Entities/Transient/Interactions/Commands/Slash/TransientSlashCommandInteractionOption.cs index 874d58bed..786fa1285 100644 --- a/src/Disqord.Core/Entities/Transient/Interactions/Commands/Slash/TransientSlashCommandInteractionOption.cs +++ b/src/Disqord.Core/Entities/Transient/Interactions/Commands/Slash/TransientSlashCommandInteractionOption.cs @@ -15,7 +15,7 @@ public class TransientSlashCommandInteractionOption : TransientClientEntity Model.Type; /// - public object? Value => Model.Value.GetValueOrDefault()?.Value; + public object? Value => Model.Value.GetValueOrDefault()?.GetValue(); /// public IReadOnlyDictionary Options diff --git a/src/Disqord.Core/Models/ApplicationCommands/ApplicationCommandOptionChoiceJsonModel.cs b/src/Disqord.Core/Models/ApplicationCommands/ApplicationCommandOptionChoiceJsonModel.cs index 1e5158238..cd871eb34 100644 --- a/src/Disqord.Core/Models/ApplicationCommands/ApplicationCommandOptionChoiceJsonModel.cs +++ b/src/Disqord.Core/Models/ApplicationCommands/ApplicationCommandOptionChoiceJsonModel.cs @@ -23,9 +23,11 @@ protected override void OnValidate() Guard.HasSizeBetweenOrEqualTo(Name, Discord.Limits.ApplicationCommand.Option.Choice.MinNameLength, Discord.Limits.ApplicationCommand.Option.Choice.MaxNameLength); Guard.IsNotNull(Value); - Guard.IsNotNull(Value.Value); - var value = Guard.IsAssignableToType(Value.Value, nameof(Value)); + var objectValue = Value.GetValue(); + Guard.IsNotNull(objectValue); + + var value = Guard.IsAssignableToType(objectValue, nameof(Value)); switch (value.GetTypeCode()) { case TypeCode.SByte: @@ -62,4 +64,4 @@ protected override void OnValidate() } } } -} \ No newline at end of file +} diff --git a/src/Disqord.Core/Serialization/Json/Default/DefaultJsonSerializer.cs b/src/Disqord.Core/Serialization/Json/Default/DefaultJsonSerializer.cs index 62fa98d79..c90ce81a9 100644 --- a/src/Disqord.Core/Serialization/Json/Default/DefaultJsonSerializer.cs +++ b/src/Disqord.Core/Serialization/Json/Default/DefaultJsonSerializer.cs @@ -80,7 +80,9 @@ public virtual void Serialize(Stream stream, object obj, IJsonSerializerOptions? public virtual IJsonNode GetJsonNode(object? obj) { if (obj == null) + { return DefaultJsonNode.Create(JValue.CreateNull(), UnderlyingSerializer); + } return DefaultJsonNode.Create(JToken.FromObject(obj, UnderlyingSerializer), UnderlyingSerializer); } @@ -145,4 +147,4 @@ public static JsonTextWriter Conditional(IJsonSerializerOptions? options, TextWr return new JsonTextWriter(writer); } } -} \ No newline at end of file +} diff --git a/src/Disqord.Core/Serialization/Json/Default/Entities/ContractResolver.cs b/src/Disqord.Core/Serialization/Json/Default/Entities/ContractResolver.cs index 0fc7f3082..ee0fb9337 100644 --- a/src/Disqord.Core/Serialization/Json/Default/Entities/ContractResolver.cs +++ b/src/Disqord.Core/Serialization/Json/Default/Entities/ContractResolver.cs @@ -114,7 +114,7 @@ protected override JsonObjectContract CreateObjectContract(Type objectType) { null => null, JToken jToken => DefaultJsonNode.Create(jToken, _serializer.UnderlyingSerializer), - _ => DefaultJsonNode.Create(JToken.FromObject(value, _serializer.UnderlyingSerializer), _serializer.UnderlyingSerializer) + _ => DefaultJsonNode.Create(value, _serializer.UnderlyingSerializer) }; model.ExtensionData.Add(key, node); diff --git a/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonArray.cs b/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonArray.cs index 4c1d03c10..452038e93 100644 --- a/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonArray.cs +++ b/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonArray.cs @@ -18,12 +18,70 @@ public class DefaultJsonArray : DefaultJsonNode, IJsonArray public int Count => Token.Count; /// - public IJsonNode? this[int index] => Create(Token[index], Serializer); + public IJsonNode? this[int index] + { + get => Create(Token[index], Serializer); + set => Token[index] = GetJToken(value)!; + } + + bool ICollection.IsReadOnly => false; public DefaultJsonArray(JArray token, JsonSerializer serializer) : base(token, serializer) { } + /// + public void Add(IJsonNode? item) + { + Token.Add(GetJToken(item)!); + } + + /// + public void Clear() + { + Token.Clear(); + } + + /// + public bool Contains(IJsonNode? item) + { + return Token.Contains(GetJToken(item)!); + } + + /// + public void CopyTo(IJsonNode?[] array, int arrayIndex) + { + var count = Count; + for (var i = 0; i < count; i++) + { + array[arrayIndex + i] = this[i]; + } + } + + /// + public bool Remove(IJsonNode? item) + { + return Token.Remove(GetJToken(item)!); + } + + /// + public int IndexOf(IJsonNode? item) + { + return Token.IndexOf(GetJToken(item)!); + } + + /// + public void Insert(int index, IJsonNode? item) + { + Token.Insert(index, GetJToken(item)!); + } + + /// + public void RemoveAt(int index) + { + Token.RemoveAt(index); + } + /// public IEnumerator GetEnumerator() { @@ -72,4 +130,4 @@ public void Reset() public void Dispose() { } } -} \ No newline at end of file +} diff --git a/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonNode.cs b/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonNode.cs index f8d2bbb8d..4051a1183 100644 --- a/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonNode.cs +++ b/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonNode.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Qommon; namespace Disqord.Serialization.Json.Default; @@ -16,7 +17,10 @@ public class DefaultJsonNode : IJsonNode /// public JToken Token { get; } - private protected readonly JsonSerializer Serializer; + /// + /// Gets the underlying serializer. + /// + public JsonSerializer Serializer { get; } public DefaultJsonNode(JToken token, JsonSerializer serializer) { @@ -37,34 +41,27 @@ public DefaultJsonNode(JToken token, JsonSerializer serializer) /// /// The string representing this node. /// - public string ToString(Formatting formatting) - { - return Token.ToString(formatting); - } - - /// - /// Formats this node into an indented JSON representation. - /// - /// - /// The string representing this node. - /// - public override string ToString() + public string ToJsonString(JsonFormatting formatting) { - return Token.ToString(Formatting.Indented); + return Token.ToString(formatting switch + { + JsonFormatting.Indented => Formatting.Indented, + _ => Formatting.None + }); } /// /// Creates a new from the specified object. /// - /// The object to create the node for. /// The default JSON serializer. + /// The object to create the node for. /// /// A JSON node representing the object. /// - public static IJsonNode? Create(object? obj, DefaultJsonSerializer serializer) + public static IJsonNode? Create(object? obj, JsonSerializer serializer) { - var token = obj != null ? JToken.FromObject(obj) : JValue.CreateNull(); - return Create(token, serializer.UnderlyingSerializer); + var token = obj != null ? JToken.FromObject(obj, serializer) : JValue.CreateNull(); + return Create(token, serializer); } [return: NotNullIfNotNull("token")] @@ -79,4 +76,12 @@ public override string ToString() _ => throw new InvalidOperationException("Unknown JSON token type.") }; } -} \ No newline at end of file + + [return: NotNullIfNotNull("node")] + internal static JToken? GetJToken(IJsonNode? node) + { + return node != null + ? Guard.IsAssignableToType(node).Token + : null; + } +} diff --git a/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonObject.cs b/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonObject.cs index 7bd0b894f..7811f9911 100644 --- a/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonObject.cs +++ b/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonObject.cs @@ -19,18 +19,64 @@ public class DefaultJsonObject : DefaultJsonNode, IJsonObject public int Count => Token.Count; /// - public IEnumerable Keys => (Token as IDictionary).Keys; + public ICollection Keys => (Token as IDictionary).Keys; /// - public IEnumerable Values => (Token as IDictionary).Values.Select(x => Create(x, Serializer)); + public ICollection Values => (Token as IDictionary).Values.Select(value => Create(value, Serializer)).ToArray(); /// - public IJsonNode? this[string key] => Create(Token[key], Serializer); + public IJsonNode? this[string key] + { + get => Create(Token[key], Serializer); + set => Token[key] = GetJToken(value); + } + + bool ICollection>.IsReadOnly => false; public DefaultJsonObject(JObject token, JsonSerializer serializer) : base(token, serializer) { } + /// + public void Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + /// + public void Clear() + { + Token.RemoveAll(); + } + + /// + public bool Contains(KeyValuePair item) + { + return TryGetValue(item.Key, out var value) && ReferenceEquals(value, item.Value); + } + + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + var index = 0; + foreach (var (key, value) in this) + { + array[arrayIndex + index++] = KeyValuePair.Create(key, value); + } + } + + /// + public bool Remove(KeyValuePair item) + { + return Remove(item.Key); + } + + /// + public void Add(string key, IJsonNode? value) + { + Token.Add(key, GetJToken(value)); + } + /// public bool ContainsKey(string key) { @@ -50,6 +96,12 @@ public bool TryGetValue(string key, out IJsonNode? value) return false; } + /// + public bool Remove(string key) + { + return Token.Remove(key); + } + private sealed class Enumerator : IEnumerator> { public KeyValuePair Current => KeyValuePair.Create(_enumerator.Current.Key, Create(_enumerator.Current.Value, _serializer)); diff --git a/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonValue.cs b/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonValue.cs index c6df28088..fd5d6cae9 100644 --- a/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonValue.cs +++ b/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonValue.cs @@ -15,20 +15,19 @@ public class DefaultJsonValue : DefaultJsonNode, IJsonValue /// public new JValue Token => (base.Token as JValue)!; - /// - public object? Value - { - get => Token.Value; - set => Token.Value = value; - } - public DefaultJsonValue(JValue token, JsonSerializer serializer) : base(token, serializer) { } + /// + public T? GetValue() + { + return Token.Value(); + } + /// public override string ToString() { return Token.ToString(CultureInfo.InvariantCulture); } -} \ No newline at end of file +} diff --git a/src/Disqord.Core/Serialization/Json/JsonModel.cs b/src/Disqord.Core/Serialization/Json/JsonModel.cs index 08cca8ec6..3a37618f2 100644 --- a/src/Disqord.Core/Serialization/Json/JsonModel.cs +++ b/src/Disqord.Core/Serialization/Json/JsonModel.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -105,63 +104,13 @@ public static bool TryGetExtensionDatum(JsonModel jsonModel, string name, out return false; } - // public void Add(string key, IJsonNode value) - // => ExtensionData.Add(key, value); - - // bool IReadOnlyDictionary.ContainsKey(string key) - // { - // return ExtensionDataCache.TryGetValue(this, out var extensionData) && extensionData.ContainsKey(key); - // } - - // bool IDictionary.Remove(string key) - // => _extensionData.Remove(key); - - // bool IReadOnlyDictionary.TryGetValue(string key, out IJsonNode? value) - // { - // if (ExtensionDataCache.TryGetValue(this, out var extensionData) && extensionData.TryGetValue(key, out value)) - // return true; - // - // value = default; - // return false; - // } - - // void ICollection>.Add(KeyValuePair item) - // => Add(item.Key, item.Value); - // - // void ICollection>.Clear() - // => _extensionData?.Clear(); - // - // bool ICollection>.Contains(KeyValuePair item) - // => _extensionData?.ContainsKey(item.Key) ?? false; - // - // void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) - // => _extensionData?.CopyTo(array, arrayIndex); - // - // bool ICollection>.Remove(KeyValuePair item) - // => _extensionData.Remove(item); - // - // bool ICollection>.IsReadOnly => _extensionData.IsReadOnly; - // int IReadOnlyCollection>.Count => ExtensionDataCache.TryGetValue(this, out var extensionData) ? extensionData.Count : 0; - - // ICollection IDictionary.Keys => _extensionData.Keys; - // ICollection IDictionary.Values => _extensionData.Values; - // IEnumerable IReadOnlyDictionary.Keys => ExtensionData.Keys; - // - // IEnumerable IReadOnlyDictionary.Values => ExtensionData.Values; - // T IJsonNode.ToType() { throw new NotSupportedException(); } - // - // IEnumerator> IEnumerable>.GetEnumerator() - // { - // return ExtensionData.GetEnumerator(); - // } - // - // IEnumerator IEnumerable.GetEnumerator() - // { - // return ExtensionData.GetEnumerator(); - // } + string IJsonNode.ToJsonString(JsonFormatting formatting) + { + throw new NotSupportedException(); + } } diff --git a/src/Disqord.Core/Serialization/Json/Nodes/IJsonArray.cs b/src/Disqord.Core/Serialization/Json/Nodes/IJsonArray.cs index a0d2851cf..c9b79cbc1 100644 --- a/src/Disqord.Core/Serialization/Json/Nodes/IJsonArray.cs +++ b/src/Disqord.Core/Serialization/Json/Nodes/IJsonArray.cs @@ -5,5 +5,5 @@ namespace Disqord.Serialization.Json; /// /// Represents a JSON array node, i.e. an array of s. /// -public interface IJsonArray : IJsonNode, IReadOnlyList -{ } \ No newline at end of file +public interface IJsonArray : IJsonNode, IList +{ } diff --git a/src/Disqord.Core/Serialization/Json/Nodes/IJsonNode.cs b/src/Disqord.Core/Serialization/Json/Nodes/IJsonNode.cs index fcd13dd7c..4b6cc90b5 100644 --- a/src/Disqord.Core/Serialization/Json/Nodes/IJsonNode.cs +++ b/src/Disqord.Core/Serialization/Json/Nodes/IJsonNode.cs @@ -13,4 +13,13 @@ public interface IJsonNode /// The converted type. /// T? ToType(); -} \ No newline at end of file + + /// + /// Converts this node into a JSON string using the specified formatting. + /// + /// The JSON formatting. + /// + /// The JSON string representation of this node. + /// + string ToJsonString(JsonFormatting formatting); +} diff --git a/src/Disqord.Core/Serialization/Json/Nodes/IJsonObject.cs b/src/Disqord.Core/Serialization/Json/Nodes/IJsonObject.cs index 919f0e962..6e50a47b8 100644 --- a/src/Disqord.Core/Serialization/Json/Nodes/IJsonObject.cs +++ b/src/Disqord.Core/Serialization/Json/Nodes/IJsonObject.cs @@ -5,5 +5,5 @@ namespace Disqord.Serialization.Json; /// /// Represents a JSON object node, i.e. a dictionary of s keyed by the property names. /// -public interface IJsonObject : IJsonNode, IReadOnlyDictionary +public interface IJsonObject : IJsonNode, IDictionary { } diff --git a/src/Disqord.Core/Serialization/Json/Nodes/IJsonValue.cs b/src/Disqord.Core/Serialization/Json/Nodes/IJsonValue.cs index 8087c7072..eed6d1afc 100644 --- a/src/Disqord.Core/Serialization/Json/Nodes/IJsonValue.cs +++ b/src/Disqord.Core/Serialization/Json/Nodes/IJsonValue.cs @@ -8,5 +8,6 @@ public interface IJsonValue : IJsonNode /// /// Gets the value of this JSON node. /// - object? Value { get; set; } -} \ No newline at end of file + /// The type of the value. + T? GetValue(); +} diff --git a/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/EnumConverter.cs b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/EnumConverter.cs new file mode 100644 index 000000000..ae027eee7 --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/EnumConverter.cs @@ -0,0 +1,67 @@ +using System; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using Qommon; +using BufferType = +#if NET8_0_OR_GREATER + byte +#else + char +#endif + ; +namespace Disqord.Serialization.Json.System; + +public class EnumConverter : JsonConverterFactory +{ + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsEnum; + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return Activator.CreateInstance(typeof(EnumConverterImpl<>).MakeGenericType(typeToConvert)) as JsonConverter; + } + + private class EnumConverterImpl : JsonConverter + where TEnum : struct, Enum + { + public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = 0ul; + if (reader.TokenType == JsonTokenType.String) + { + value = reader.ReadUInt64FromString(); + } + else if (reader.TokenType == JsonTokenType.Number) + { + value = reader.GetUInt64(); + } + else + { + Throw.InvalidOperationException("Invalid enum value."); + } + + return Unsafe.As(ref value); + } + + public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) + { + const double maxSafeInteger = 9007199254740991; + + var ulongValue = ((IConvertible) value).ToUInt64(CultureInfo.InvariantCulture); + if (ulongValue <= maxSafeInteger) + { + writer.WriteNumberValue(ulongValue); + } + else + { + var buffer = (stackalloc BufferType[20]); + ulongValue.TryFormat(buffer, out var countWritten); + writer.WriteStringValue(buffer[..countWritten]); + } + } + } +} diff --git a/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/JsonNodeConverter.cs b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/JsonNodeConverter.cs index 496ce511f..1a808d4a5 100644 --- a/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/JsonNodeConverter.cs +++ b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/JsonNodeConverter.cs @@ -5,19 +5,25 @@ namespace Disqord.Serialization.Json.System; -internal class JsonNodeConverter : JsonConverter +internal class JsonNodeConverter : JsonConverter + where TNode : IJsonNode { - public override IJsonNode? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsAssignableTo(typeof(IJsonNode)) && !typeToConvert.IsAssignableTo(typeof(JsonModel)); + } + + public override TNode? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var node = JsonNode.Parse(ref reader); - return SystemJsonNode.Create(node, options); + return (TNode?) SystemJsonNode.Create(node, options); } - public override void Write(Utf8JsonWriter writer, IJsonNode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TNode value, JsonSerializerOptions options) { if (value is SystemJsonNode systemJsonNode) { - systemJsonNode.Token.WriteTo(writer, options); + systemJsonNode.Node.WriteTo(writer, options); } else { diff --git a/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/NullableConverter.cs b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/NullableConverter.cs new file mode 100644 index 000000000..627687bc7 --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/NullableConverter.cs @@ -0,0 +1,37 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Disqord.Serialization.Json.System; + +public class NullableConverter : JsonConverter + where TValue : struct +{ + private readonly JsonConverter _valueConverter; + + public NullableConverter(JsonConverter valueConverter) + { + _valueConverter = valueConverter; + } + + public override TValue? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + return _valueConverter.Read(ref reader, typeToConvert, options); + } + + public override void Write(Utf8JsonWriter writer, TValue? value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + _valueConverter.Write(writer, value.Value, options); + } +} diff --git a/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/OptionalConverter.cs b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/OptionalConverter.cs index 3394db4af..3d53e2568 100644 --- a/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/OptionalConverter.cs +++ b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/OptionalConverter.cs @@ -21,7 +21,27 @@ public override void Write(Utf8JsonWriter writer, Optional value, JsonS } else { - JsonSerializer.Serialize(writer, optionalValue, options); + JsonSerializer.Serialize(writer, value.Value, options); } } } + +internal class OptionalConverterWithValueConverter : JsonConverter> +{ + private readonly JsonConverter _valueConverter; + + public OptionalConverterWithValueConverter(JsonConverter valueConverter) + { + _valueConverter = valueConverter; + } + + public override Optional Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return _valueConverter.Read(ref reader, typeToConvert, options); + } + + public override void Write(Utf8JsonWriter writer, Optional value, JsonSerializerOptions options) + { + _valueConverter.Write(writer, value.Value, options); + } +} diff --git a/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/SnowflakeConverter.cs b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/SnowflakeConverter.cs index d1f5b64ea..8e04a5fe7 100644 --- a/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/SnowflakeConverter.cs +++ b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/SnowflakeConverter.cs @@ -1,6 +1,13 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; +using BufferType = +#if NET8_0_OR_GREATER + byte +#else + char +#endif + ; namespace Disqord.Serialization.Json.System; @@ -11,14 +18,18 @@ internal class SnowflakeConverter : JsonConverter public override Snowflake Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var value = reader.GetString()!; - return new Snowflake(Snowflake.Parse(value)); + if (reader.TokenType == JsonTokenType.String) + { + return reader.ReadUInt64FromString(); + } + + return reader.GetUInt64(); } public override void Write(Utf8JsonWriter writer, Snowflake value, JsonSerializerOptions options) { - var stringValue = (stackalloc char[20]); - value.TryFormat(stringValue, out var charsWritten); - writer.WriteStringValue(stringValue[..charsWritten]); + var buffer = (stackalloc BufferType[20]); + value.RawValue.TryFormat(buffer, out var countWritten); + writer.WriteStringValue(buffer[..countWritten]); } } diff --git a/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/SnowflakeDictionaryConverter`1.cs b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/SnowflakeDictionaryConverter`1.cs new file mode 100644 index 000000000..064c6c64d --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/SnowflakeDictionaryConverter`1.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using BufferType = +#if NET8_0_OR_GREATER + byte +#else + char +#endif + ; + +namespace Disqord.Serialization.Json.System; + +public class SnowflakeDictionaryConverter : JsonConverter> +{ + public override Dictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + var jsonObject = JsonSerializer.Deserialize(ref reader, options)!; + var count = jsonObject.Count; + var dictionary = new Dictionary(count); + for (var i = 0; i < count; i++) + { + var property = jsonObject.GetAt(i); + dictionary.Add( + Convert.ToUInt64(property.Key, CultureInfo.InvariantCulture), + (property.Value != null + ? property.Value.Deserialize(options) + : default)!); + } + + return dictionary; + } + + public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + var buffer = (stackalloc BufferType[20]); + foreach (var kvp in value) + { + kvp.Key.RawValue.TryFormat(buffer, out var countWritten); + writer.WritePropertyName(buffer[..countWritten]); + + if (kvp.Value == null) + { + writer.WriteNullValue(); + } + else + { + JsonSerializer.Serialize(writer, kvp.Value, options); + } + } + + writer.WriteEndObject(); + } +} diff --git a/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/StreamConverter.cs b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/StreamConverter.cs index 492d950aa..f0dec87fa 100644 --- a/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/StreamConverter.cs +++ b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/StreamConverter.cs @@ -3,23 +3,15 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using Microsoft.Extensions.Logging; +using Qommon; namespace Disqord.Serialization.Json.System; internal class StreamConverter : JsonConverter { - private readonly SystemJsonSerializer _serializer; + // This header works regardless of the actual type of the attachment. public const string Header = "data:image/jpeg;base64,"; - private bool _shownHttpWarning; - private Type? _httpBaseContentType; - - public StreamConverter(SystemJsonSerializer serializer) - { - _serializer = serializer; - } - public override Stream? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw new NotSupportedException(); @@ -27,9 +19,7 @@ public StreamConverter(SystemJsonSerializer serializer) public override void Write(Utf8JsonWriter writer, Stream stream, JsonSerializerOptions options) { - // Shows a warning for System.Net.Http content streams. - // See CheckStreamType for more information. - CheckStreamType(stream); + Guard.CanRead(stream); StringBuilder base64Builder; if (stream.CanSeek) @@ -43,10 +33,12 @@ public override void Write(Utf8JsonWriter writer, Stream stream, JsonSerializerO // Check if the user didn't rewind the stream. if (stream.Position == stream.Length) + { throw new ArgumentException("The stream's position is the same as its length. Did you forget to rewind it?"); + } // Check if the stream is a memory stream and the underlying buffer is retrievable, - // so we can skip the reading as all of the memory is already allocated anyways. + // so we can skip the reading as all the memory is already allocated anyway. if (stream is MemoryStream memoryStream && memoryStream.TryGetBuffer(out var memoryStreamBuffer)) { var base64 = string.Concat(Header, Convert.ToBase64String(memoryStreamBuffer.AsSpan())); @@ -71,72 +63,13 @@ public override void Write(Utf8JsonWriter writer, Stream stream, JsonSerializerO while ((bytesRead = stream.Read(byteSpan)) != 0) { if (!Convert.TryToBase64Chars(byteSpan[..bytesRead], charSpan, out var charsWritten)) + { throw new ArgumentException("The stream could not be converted to base64."); + } base64Builder.Append(charSpan[..charsWritten]); } writer.WriteStringValue(base64Builder.ToString()); } - - // This method just checks if the user passed a System.Net.Http content stream and warns them, if they did. - // The reasoning is that content streams are lazy, meaning code like `var stream = await HttpClient#GetStreamAsync()` - // doesn't actually download the stream completely, as many would expect it to. - // It's inconsistent with other GetXAsync() methods which are all content requests while that one, for some reason, - // is only a headers request. If the user passes that stream for serialization the base64 encoding code above - // won't work due to possible buffer underflowing. If I was to make it work with it - it'd be all synchronous, hence I'd rather just warn the user and have them both - // acknowledge how GetStreamAsync() works and pass me a seekable stream, whether it'd be a MemoryStream or a FileStream as - // it's going to be both more efficient as well as more reliable. - public void CheckStreamType(Stream stream) - { - lock (this) - { - if (_shownHttpWarning /*|| !_serializer.ShowHttpStreamsWarning*/) - return; - - // Check if the type is already cached. - if (_httpBaseContentType == null) - { - // The following loops get all currently loaded assemblies and find System.Net.Http, - // in which they try to find the internal HttpBaseStream class. - var assemblies = AppDomain.CurrentDomain.GetAssemblies(); - for (var i = 0; i < assemblies.Length; i++) - { - var assembly = assemblies[i]; - if (assembly.GetName().Name != "System.Net.Http") - continue; - - var types = assembly.GetTypes(); - for (var j = 0; j < types.Length; j++) - { - var type = types[j]; - if (type.Name != "HttpBaseStream") - continue; - - // Cache the type for future checks. - _httpBaseContentType = type; - } - } - - if (_httpBaseContentType == null) - { - // This means that the assembly hasn't been loaded, so we assume the user simply isn't using System.Net.Http. - // Let's not check anything else. - _shownHttpWarning = true; - return; - } - } - - // Check if the given stream implements HttpBaseStream. - if (!_httpBaseContentType.IsInstanceOfType(stream)) - return; - - _httpBaseContentType = null; - _shownHttpWarning = true; - _serializer.Logger.LogWarning( - "You are passing HTTP streams directly to the API methods which is highly advised against due to buffer data underflowing for incomplete streams. " + - "If you ignore this warning, ensure the streams are fully downloaded or copy them over to MemoryStreams. " + - "This warning will not appear again."); - } - } } diff --git a/src/Disqord.Core/Serialization/Json/STJ/Entities/JsonTypeInfoResolver.cs b/src/Disqord.Core/Serialization/Json/STJ/Entities/JsonTypeInfoResolver.cs index 94a331fad..f0bc045cc 100644 --- a/src/Disqord.Core/Serialization/Json/STJ/Entities/JsonTypeInfoResolver.cs +++ b/src/Disqord.Core/Serialization/Json/STJ/Entities/JsonTypeInfoResolver.cs @@ -1,52 +1,47 @@ using System; using System.Collections.Generic; +using System.IO; using System.Reflection; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; +using Qommon; using Qommon.Serialization; namespace Disqord.Serialization.Json.System; internal class JsonTypeInfoResolver : DefaultJsonTypeInfoResolver { - private static readonly Dictionary _optionalConverters = new(); + private static readonly PropertyInfo _ignoreConditionProperty; + + private static readonly ConditionalWeakTable _optionalConverters = new(); + private static readonly ConditionalWeakTable _snowflakeDictionaryConverters = new(); + private static readonly StreamConverter? _streamConverter = new(); + private static readonly JsonStringEnumConverter? _stringEnumConverter = new(); + private static readonly SnowflakeConverter? _snowflakeConverter = new(); + private static readonly NullableConverter? _nullableSnowflakeConverter = new(_snowflakeConverter); public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) { + // new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) var jsonTypeInfo = base.GetTypeInfo(type, options); - // TODO: AttributeProvider var jsonProperties = jsonTypeInfo.Properties; var jsonPropertyCount = jsonProperties.Count; - var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public); List? jsonPropertiesToRemove = null; for (var i = 0; i < jsonPropertyCount; i++) { var jsonProperty = jsonProperties[i]; - FieldInfo? matchingField = null; - foreach (var property in fields) - { - if (jsonProperty.Name == property.Name) - { - matchingField = property; - break; - } - } - - if (matchingField == null) + var fieldInfo = jsonProperty.AttributeProvider as FieldInfo; + if (fieldInfo == null) { // TODO: IsExtensionData (jsonPropertiesToRemove ??= new()).Add(jsonProperty); - continue; } - // Guard.IsNotNull(matchingField); - - var attributes = matchingField.GetCustomAttributes(); + var attributes = fieldInfo.GetCustomAttributes(); JsonPropertyAttribute? jsonPropertyAttribute = null; foreach (var attribute in attributes) { @@ -73,27 +68,25 @@ public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions option jsonProperty.Name = jsonPropertyAttribute.Name; - // TODO: set options individually, if possible in the future? - // TODO: set the currently internal IgnoreCondition instead of duping options? - typeof(JsonPropertyInfo).GetProperty("Options")!.SetValue(jsonProperty, new JsonSerializerOptions(jsonProperty.Options)); if (typeof(IOptional).IsAssignableFrom(jsonProperty.PropertyType)) { - jsonProperty.Options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault; - - ref var optionalConverter = ref CollectionsMarshal.GetValueRefOrAddDefault(_optionalConverters, jsonProperty.PropertyType, out var exists); - if (!exists) + if (jsonProperty.PropertyType.GenericTypeArguments.Length == 0) { - var optionalConverterType = typeof(OptionalConverter<>).MakeGenericType(jsonProperty.PropertyType.GenericTypeArguments[0]); - optionalConverter = Unsafe.As(Activator.CreateInstance(optionalConverterType)); + Throw.InvalidOperationException($"JSON property type {jsonProperty.PropertyType} is not supported."); } - jsonProperty.CustomConverter = optionalConverter; + _ignoreConditionProperty.SetValue(jsonProperty, JsonIgnoreCondition.WhenWritingDefault); + + jsonProperty.CustomConverter = GetOptionalConverter(jsonProperty.PropertyType); } else { - jsonProperty.Options.DefaultIgnoreCondition = jsonPropertyAttribute.NullValueHandling == NullValueHandling.Ignore - ? JsonIgnoreCondition.WhenWritingNull - : JsonIgnoreCondition.Never; + if (jsonPropertyAttribute.NullValueHandling == NullValueHandling.Ignore) + { + _ignoreConditionProperty.SetValue(jsonProperty, JsonIgnoreCondition.WhenWritingNull); + } + + jsonProperty.CustomConverter = GetConverter(jsonProperty.PropertyType); } } @@ -105,4 +98,85 @@ public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions option return jsonTypeInfo; } + + private static JsonConverter GetOptionalConverter(Type type) + { + var optionalType = type.GenericTypeArguments[0]; + return _optionalConverters.GetValue(optionalType, static type => + { + var valueConverter = GetConverter(type); + if (valueConverter != null) + { + var optionalConverterType = typeof(OptionalConverterWithValueConverter<>).MakeGenericType(type); + return (Activator.CreateInstance(optionalConverterType, valueConverter) as JsonConverter)!; + } + else + { + var optionalConverterType = typeof(OptionalConverter<>).MakeGenericType(type); + return (Activator.CreateInstance(optionalConverterType) as JsonConverter)!; + } + }); + } + + private static JsonConverter? GetConverter(Type type) + { + if (typeof(Stream).IsAssignableFrom(type)) + { + return _streamConverter; + } + + if (typeof(IJsonNode).IsAssignableFrom(type) && !typeof(JsonModel).IsAssignableFrom(type)) + { + return (Activator.CreateInstance(typeof(JsonNodeConverter<>).MakeGenericType(type)) as JsonConverter)!; + } + + if (!type.IsClass) + { + var nullableType = Nullable.GetUnderlyingType(type); + if (nullableType != null) + type = nullableType; + + if (type.IsEnum) + { + var stringEnumAttribute = type.GetCustomAttribute(); + if (stringEnumAttribute != null) + { + return _stringEnumConverter; + } + } + else if (type == typeof(Snowflake)) + { + if (nullableType != null) + { + return _nullableSnowflakeConverter; + } + + return _snowflakeConverter; + } + } + else + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + { + var generics = type.GetGenericArguments(); + if (generics[0] == typeof(Snowflake)) + { + return _snowflakeDictionaryConverters.GetValue(generics[1], type => (Activator.CreateInstance(typeof(SnowflakeDictionaryConverter<>).MakeGenericType(type)) as JsonConverter)!); + } + } + } + + return null; + } + + static JsonTypeInfoResolver() + { + var ignoreConditionProperty = typeof(JsonPropertyInfo).GetProperty("IgnoreCondition", BindingFlags.Instance | BindingFlags.NonPublic); + if (ignoreConditionProperty == null) + { + Throw.InvalidOperationException("The System.Text.Json version is not compatible with this resolver."); + } + + _ignoreConditionProperty = ignoreConditionProperty; + } } diff --git a/src/Disqord.Core/Serialization/Json/STJ/JsonUtilities.cs b/src/Disqord.Core/Serialization/Json/STJ/JsonUtilities.cs new file mode 100644 index 000000000..969d1c3a0 --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/JsonUtilities.cs @@ -0,0 +1,33 @@ +using System.Text.Json; +#if NET8_0_OR_GREATER +using System.Buffers; +#endif + +namespace Disqord.Serialization.Json.System; + +internal static class JsonUtilities +{ + public static ulong ReadUInt64FromString(this ref Utf8JsonReader reader) + { +#if NET8_0_OR_GREATER + if (!reader.HasValueSequence) + { + return ulong.Parse(reader.ValueSpan); + } + + if (reader.ValueSequence.IsSingleSegment) + { + return ulong.Parse(reader.ValueSequence.FirstSpan); + } + + var buffer = (stackalloc byte[20]); + reader.ValueSequence.CopyTo(buffer); + return ulong.Parse(buffer[..(int) reader.ValueSequence.Length]); + +#else + var buffer = (stackalloc char[20]); + reader.CopyString(buffer); + return ulong.Parse(buffer); +#endif + } +} diff --git a/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonArray.cs b/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonArray.cs index 93d05dbe7..58e9eb1b4 100644 --- a/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonArray.cs +++ b/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonArray.cs @@ -2,67 +2,133 @@ using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Nodes; +using Newtonsoft.Json.Linq; -namespace Disqord.Serialization.Json.System +namespace Disqord.Serialization.Json.System; + +/// +/// Represents a default JSON array node. +/// Wraps a . +/// +public class SystemJsonArray : SystemJsonNode, IJsonArray { - internal class SystemJsonArray : SystemJsonNode, IJsonArray + /// + public new JsonArray Node => (base.Node as JsonArray)!; + + /// + public int Count => Node.Count; + + /// + public IJsonNode? this[int index] { - public new JsonArray Token => base.Token.AsArray(); + get => Create(Node[index], Options); + set => Node[index] = GetSystemNode(value); + } - public SystemJsonArray(JsonNode node, JsonSerializerOptions options) - : base(node, options) - { } + bool ICollection.IsReadOnly => false; - public IEnumerator GetEnumerator() - { - return new Enumerator(this); - } + public SystemJsonArray(JsonArray node, JsonSerializerOptions options) + : base(node, options) + { } + + /// + public void Add(IJsonNode? item) + { + Node.Add(GetSystemNode(item)); + } + + /// + public void Clear() + { + Node.Clear(); + } + + /// + public bool Contains(IJsonNode? item) + { + return Node.Contains(GetSystemNode(item)); + } - IEnumerator IEnumerable.GetEnumerator() + /// + public void CopyTo(IJsonNode?[] array, int arrayIndex) + { + var count = Count; + for (var i = 0; i < count; i++) { - return GetEnumerator(); + array[arrayIndex + i] = this[i]; } + } - public int Count => Token.Count; + /// + public bool Remove(IJsonNode? item) + { + return Node.Remove(GetSystemNode(item)); + } - public IJsonNode? this[int index] => Create(Token[index], Options); + /// + public int IndexOf(IJsonNode? item) + { + return Node.IndexOf(GetSystemNode(item)); + } - private sealed class Enumerator : IEnumerator - { - public IJsonNode? Current => Create(_current?.Token, _array.Options); + /// + public void Insert(int index, IJsonNode? item) + { + Node.Insert(index, GetSystemNode(item)); + } - object? IEnumerator.Current => Current; + /// + public void RemoveAt(int index) + { + Node.RemoveAt(index); + } - private readonly SystemJsonArray _array; - private int _index; - private SystemJsonNode? _current; + /// + public IEnumerator GetEnumerator() + { + return new Enumerator(this); + } - internal Enumerator(SystemJsonArray array) - { - _array = array; - } + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } - public bool MoveNext() - { - var index = _index; - if (_index++ < _array.Count) - { - _current = (_array[index] as SystemJsonArray)!; - return true; - } - - _current = null; - return false; - } + private sealed class Enumerator : IEnumerator + { + public IJsonNode? Current => Create(_current?.Node, _array.Options); - public void Reset() + object? IEnumerator.Current => Current; + + private readonly SystemJsonArray _array; + private int _index; + private SystemJsonNode? _current; + + internal Enumerator(SystemJsonArray array) + { + _array = array; + } + + public bool MoveNext() + { + var index = _index; + if (_index++ < _array.Count) { - _index = 0; - _current = null; + _current = (_array[index] as SystemJsonNode)!; + return true; } - public void Dispose() - { } + _current = null; + return false; } + + public void Reset() + { + _index = 0; + _current = null; + } + + public void Dispose() + { } } } diff --git a/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonNode.cs b/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonNode.cs index 4e7e14a76..4ce2a5d54 100644 --- a/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonNode.cs +++ b/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonNode.cs @@ -2,42 +2,79 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Nodes; +using Qommon; namespace Disqord.Serialization.Json.System; +/// +/// Represents a default JSON node. +/// Wraps a . +/// public class SystemJsonNode : IJsonNode { - protected readonly JsonSerializerOptions Options; + /// + /// Gets the underlying . + /// + public JsonNode Node { get; } + + /// + /// Gets the underlying serializer options. + /// + public JsonSerializerOptions Options { get; } public SystemJsonNode(JsonNode node, JsonSerializerOptions options) { - Token = node; + Node = node; Options = options; } - public JsonNode Token { get; set; } - + /// public T? ToType() { - return Token.Deserialize(Options); + return Node.Deserialize(Options); } + /// + public string ToJsonString(JsonFormatting formatting) + { + return Node.ToJsonString(new JsonSerializerOptions(Options) + { + WriteIndented = formatting == JsonFormatting.Indented + }); + } + + /// + /// Creates a new from the specified object. + /// + /// The object to create the node for. + /// The JSON serializer options. + /// + /// A JSON node representing the object. + /// public static IJsonNode? Create(object? obj, JsonSerializerOptions options) { - var element = JsonSerializer.SerializeToNode(obj, options); - return Create(element, options); + var node = JsonSerializer.SerializeToNode(obj, options); + return Create(node, options); } - [return: NotNullIfNotNull("token")] - internal static IJsonNode? Create(JsonNode? token, JsonSerializerOptions options) + [return: NotNullIfNotNull("node")] + internal static IJsonNode? Create(JsonNode? node, JsonSerializerOptions options) { - return token switch + return node switch { null => null, - JsonArray jsonArray => new SystemJsonArray(jsonArray, options), - JsonObject jsonObject => new SystemJsonObject(jsonObject, options), - JsonValue jsonValue => new SystemJsonValue(jsonValue, options), - _ => throw new InvalidOperationException("Unknown JSON token type.") + JsonObject @object => new SystemJsonObject(@object, options), + JsonArray array => new SystemJsonArray(array, options), + JsonValue value => new SystemJsonValue(value, options), + _ => throw new InvalidOperationException("Unknown JSON node type.") }; } + + [return: NotNullIfNotNull("node")] + internal static JsonNode? GetSystemNode(IJsonNode? node) + { + return node != null + ? Guard.IsAssignableToType(node).Node + : null; + } } diff --git a/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonObject.cs b/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonObject.cs index 57b826682..09ae5789a 100644 --- a/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonObject.cs +++ b/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonObject.cs @@ -6,30 +6,89 @@ namespace Disqord.Serialization.Json.System; +/// +/// Represents a default JSON object node. +/// Wraps a . +/// public class SystemJsonObject : SystemJsonNode, IJsonObject { - private readonly JsonSerializerOptions _options; + /// + public new JsonObject Node => (base.Node as JsonObject)!; - public new JsonObject Token => base.Token.AsObject(); + /// + public int Count => Node.Count; - public SystemJsonObject(JsonObject node, JsonSerializerOptions options) - : base(node, options) + /// + public ICollection Keys => (Node as IDictionary).Keys; + + /// + public ICollection Values => (Node as IDictionary).Values.Select(value => Create(value, Options)).ToArray(); + + /// + public IJsonNode? this[string key] + { + get => Create(Node[key], Options); + set => Node[key] = GetSystemNode(value); + } + + bool ICollection>.IsReadOnly => false; + + public SystemJsonObject(JsonObject @object, JsonSerializerOptions options) + : base(@object, options) + { } + + /// + public void Add(KeyValuePair item) { - _options = options; + Add(item.Key, item.Value); } - public int Count => Token.Count; + /// + public void Clear() + { + Node.Clear(); + } + /// + public bool Contains(KeyValuePair item) + { + return TryGetValue(item.Key, out var value) && ReferenceEquals(value, item.Value); + } + + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + var index = 0; + foreach (var (key, value) in this) + { + array[arrayIndex + index++] = KeyValuePair.Create(key, value); + } + } + + /// + public bool Remove(KeyValuePair item) + { + return Remove(item.Key); + } + + /// + public void Add(string key, IJsonNode? value) + { + Node.Add(key, GetSystemNode(value)); + } + + /// public bool ContainsKey(string key) { - return Token.ContainsKey(key); + return Node.ContainsKey(key); } + /// public bool TryGetValue(string key, out IJsonNode? value) { - if (Token.TryGetPropertyValue(key, out var propertyValue)) + if (Node.TryGetPropertyValue(key, out var node)) { - value = Create(propertyValue, _options); + value = Create(node, Options); return true; } @@ -37,11 +96,11 @@ public bool TryGetValue(string key, out IJsonNode? value) return false; } - public IJsonNode? this[string key] => Create(Token[key], _options); - - public IEnumerable Keys => (Token as IDictionary).Keys; - - public IEnumerable Values => (Token as IDictionary).Values.Select(x => Create(x, _options)); + /// + public bool Remove(string key) + { + return Node.Remove(key); + } private sealed class Enumerator : IEnumerator> { @@ -68,9 +127,10 @@ public void Dispose() => _enumerator.Dispose(); } + /// public IEnumerator> GetEnumerator() { - return new Enumerator(Token.GetEnumerator(), _options); + return new Enumerator(Node.GetEnumerator(), Options); } IEnumerator IEnumerable.GetEnumerator() diff --git a/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonValue.cs b/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonValue.cs index 349add638..1d21e962b 100644 --- a/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonValue.cs +++ b/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonValue.cs @@ -4,24 +4,31 @@ namespace Disqord.Serialization.Json.System; -[DebuggerDisplay("{Value}")] +/// +/// Represents a default JSON value node. +/// Wraps a . +/// +[DebuggerDisplay($"{nameof(Value)}")] public class SystemJsonValue : SystemJsonNode, IJsonValue { - public new JsonValue Token => base.Token.AsValue(); + /// + public new JsonValue Node => (base.Node as JsonValue)!; - public SystemJsonValue(JsonValue token, JsonSerializerOptions options) - : base(token, options) + private object? Value => GetValue(); + + public SystemJsonValue(JsonValue value, JsonSerializerOptions options) + : base(value, options) { } - // TODO - public object? Value + /// + public T? GetValue() { - get => Token; - set => base.Token = JsonValue.Create(value)!; + return Node.Deserialize(Options); } + /// public override string ToString() { - return Token.ToString(); + return Node.ToString(); } } diff --git a/src/Disqord.Core/Serialization/Json/STJ/SystemJsonSerializer.cs b/src/Disqord.Core/Serialization/Json/STJ/SystemJsonSerializer.cs index eb1e34c17..52713b9eb 100644 --- a/src/Disqord.Core/Serialization/Json/STJ/SystemJsonSerializer.cs +++ b/src/Disqord.Core/Serialization/Json/STJ/SystemJsonSerializer.cs @@ -10,21 +10,22 @@ public class SystemJsonSerializer : IJsonSerializer { public ILogger Logger { get; } - private readonly JsonSerializerOptions _options; + /// + /// Gets the underlying . + /// + public JsonSerializerOptions UnderlyingOptions { get; } public SystemJsonSerializer(ILogger logger) { Logger = logger; - _options = new JsonSerializerOptions + UnderlyingOptions = new JsonSerializerOptions { - NumberHandling = JsonNumberHandling.AllowReadingFromString, + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, IncludeFields = true, IgnoreReadOnlyFields = true, IgnoreReadOnlyProperties = true, TypeInfoResolver = new JsonTypeInfoResolver(), - - // TODO: proper string enum converter - Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), new JsonNodeConverter(), new SnowflakeConverter(), new StreamConverter(this) } + Converters = { new EnumConverter(), new SnowflakeConverter(), new StreamConverter() } }; } @@ -33,7 +34,7 @@ public SystemJsonSerializer(ILogger logger) { try { - return JsonSerializer.Deserialize(stream, type, _options); + return JsonSerializer.Deserialize(stream, type, UnderlyingOptions); } catch (Exception ex) { @@ -48,13 +49,13 @@ public void Serialize(Stream stream, object obj, IJsonSerializerOptions? options { if (options != null && options.Formatting == JsonFormatting.Indented) { - var serializerOptions = new JsonSerializerOptions(_options); + var serializerOptions = new JsonSerializerOptions(UnderlyingOptions); serializerOptions.WriteIndented = true; JsonSerializer.Serialize(stream, obj, serializerOptions); } else { - JsonSerializer.Serialize(stream, obj, _options); + JsonSerializer.Serialize(stream, obj, UnderlyingOptions); } } catch (Exception ex) @@ -65,6 +66,6 @@ public void Serialize(Stream stream, object obj, IJsonSerializerOptions? options public IJsonNode GetJsonNode(object? obj) { - return SystemJsonNode.Create(obj, _options)!; + return SystemJsonNode.Create(obj, UnderlyingOptions)!; } } diff --git a/src/Disqord.Gateway.Api/Default/Sharding/DefaultShard.cs b/src/Disqord.Gateway.Api/Default/Sharding/DefaultShard.cs index 6961cdd72..d3ce78911 100644 --- a/src/Disqord.Gateway.Api/Default/Sharding/DefaultShard.cs +++ b/src/Disqord.Gateway.Api/Default/Sharding/DefaultShard.cs @@ -210,8 +210,8 @@ private async Task InternalRunAsync(Uri? uri, CancellationToken stoppingToken) // LINQ is faster here as we avoid double ToType()ing (later in the dispatch handler). var d = (payload.D as IJsonObject)!; - SessionId = (d["session_id"] as IJsonValue)!.Value as string; - var resumeGatewayUrl = (d["resume_gateway_url"] as IJsonValue)!.Value as string; + SessionId = (d["session_id"] as IJsonValue)?.GetValue(); + var resumeGatewayUrl = (d["resume_gateway_url"] as IJsonValue)?.GetValue(); if (resumeGatewayUrl != null) { ResumeUri = new Uri(resumeGatewayUrl); diff --git a/src/Disqord.Gateway/Default/Dispatcher/DefaultGatewayDispatcher.cs b/src/Disqord.Gateway/Default/Dispatcher/DefaultGatewayDispatcher.cs index 8414c9f2d..67d032108 100644 --- a/src/Disqord.Gateway/Default/Dispatcher/DefaultGatewayDispatcher.cs +++ b/src/Disqord.Gateway/Default/Dispatcher/DefaultGatewayDispatcher.cs @@ -5,6 +5,7 @@ using Disqord.Gateway.Api; using Disqord.Gateway.Api.Models; using Disqord.Gateway.Default.Dispatcher; +using Disqord.Serialization.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Qommon; @@ -250,7 +251,7 @@ private void HandleUnknownDispatch(IShard shard, GatewayDispatchReceivedEventArg shard.Logger.LogWarning(_loggedUnknownWarning ? "Received an unknown dispatch {0}.\n{1}" : "Received an unknown dispatch {0}. This message will only appear once for each unknown dispatch.\n{1}", - e.Name, e.Data.ToString()); + e.Name, e.Data.ToJsonString(JsonFormatting.Indented)); if (!_loggedUnknownWarning) _loggedUnknownWarning = true; diff --git a/src/Disqord.Gateway/Default/Dispatcher/Dispatches/Voice/VOICE_STATE_UPDATE.cs b/src/Disqord.Gateway/Default/Dispatcher/Dispatches/Voice/VOICE_STATE_UPDATE.cs index 55a25cb6e..4cf7656f5 100644 --- a/src/Disqord.Gateway/Default/Dispatcher/Dispatches/Voice/VOICE_STATE_UPDATE.cs +++ b/src/Disqord.Gateway/Default/Dispatcher/Dispatches/Voice/VOICE_STATE_UPDATE.cs @@ -41,10 +41,10 @@ public class VoiceStateUpdateDispatchHandler : DispatchHandler() == null) { isLurker = true; - jsonValue.Value = DateTimeOffset.UtcNow; + model.Member.Value["joined_at"] = shard.Serializer.GetJsonNode(DateTimeOffset.UtcNow); } var memberModel = model.Member.Value.ToType()!; diff --git a/src/Disqord.Rest.Api/RestApiException.cs b/src/Disqord.Rest.Api/RestApiException.cs index 6ed8bba92..f3ea914af 100644 --- a/src/Disqord.Rest.Api/RestApiException.cs +++ b/src/Disqord.Rest.Api/RestApiException.cs @@ -136,7 +136,7 @@ static IEnumerable> ExtractErrors(IJsonObject jsonO // Add the key and the message/code for it. var messages = errorsArray.OfType() - .Select(static x => (x.GetValueOrDefault("message") ?? x.GetValueOrDefault("code"))?.ToString()); + .Select(static x => (x.TryGetValue("message", out var message) ? message : x.TryGetValue("code", out var code) ? code : null)?.ToString()); extracted.Add(KeyValuePair.Create(newKey, string.Join("; ", messages))); } diff --git a/src/Disqord.TestBot/Program.cs b/src/Disqord.TestBot/Program.cs index e382e1a71..409bdfd18 100644 --- a/src/Disqord.TestBot/Program.cs +++ b/src/Disqord.TestBot/Program.cs @@ -1,6 +1,11 @@ using System; using Disqord.Bot.Hosting; using Disqord.Gateway; +using Disqord.Gateway.Api.Default; +using Disqord.Serialization.Json; +using Disqord.Serialization.Json.System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Serilog; using Serilog.Events; @@ -23,6 +28,12 @@ private static void Main(string[] args) bot.Prefixes = new[] { "??" }; bot.Intents |= GatewayIntents.DirectMessages | GatewayIntents.DirectReactions; }) + .ConfigureServices(services => + { + services.Replace(ServiceDescriptor.Singleton()); + + services.Configure(x => x.LogsPayloads = true); + }) .UseDefaultServiceProvider(provider => { provider.ValidateScopes = true; diff --git a/src/Disqord.targets b/src/Disqord.targets index 9003135da..b30a5bddf 100644 --- a/src/Disqord.targets +++ b/src/Disqord.targets @@ -1,7 +1,7 @@ - net6.0 + net6.0;net8.0 preview true