diff --git a/src/Disqord.Bot/Commands/Implementation/Application/ExecutionSteps/Slash/BindOptions.cs b/src/Disqord.Bot/Commands/Implementation/Application/ExecutionSteps/Slash/BindOptions.cs index 27a3e820d..8f80cc2f2 100644 --- a/src/Disqord.Bot/Commands/Implementation/Application/ExecutionSteps/Slash/BindOptions.cs +++ b/src/Disqord.Bot/Commands/Implementation/Application/ExecutionSteps/Slash/BindOptions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Disqord.Serialization.Json; using Qmmands; using Qommon; @@ -84,15 +85,17 @@ static IReadOnlyDictionary GetArguments( continue; } - if (option.Value is not string stringValue) + if (option.Value.Type != JsonValueType.String) { + var value = GetOptionValue(option); arguments[parameter] = actualType.IsEnum - ? Enum.ToObject(actualType, option.Value) - : Convert.ChangeType(option.Value, actualType, context.Locale); + ? Enum.ToObject(actualType, value) + : Convert.ChangeType(value, actualType, context.Locale); continue; } + var stringValue = option.Value.ToType()!; if (interaction is IAutoCompleteInteraction) { // Treat string values as arguments for auto-complete. @@ -105,7 +108,7 @@ static IReadOnlyDictionary GetArguments( { // If the option is just a string, pass it through to type parsing. var rawArguments = context.RawArguments ??= new Dictionary(); - rawArguments[parameter] = option.Value as string; + rawArguments[parameter] = stringValue; continue; } @@ -134,5 +137,30 @@ static IReadOnlyDictionary GetArguments( return Next.ExecuteAsync(context); } + + private static object GetOptionValue(ISlashCommandInteractionOption option) + { + var value = option.Value!; + switch (option.Type) + { + case SlashCommandOptionType.Integer: + { + return value.ToType(); + } + case SlashCommandOptionType.Boolean: + { + return value.ToType(); + } + case SlashCommandOptionType.Number: + { + return value.ToType(); + } + default: + { + Throw.ArgumentOutOfRangeException(nameof(option)); + return default; + } + } + } } } diff --git a/src/Disqord.Core/Disqord.Core.csproj b/src/Disqord.Core/Disqord.Core.csproj index fe4597ca7..22e5a3b34 100644 --- a/src/Disqord.Core/Disqord.Core.csproj +++ b/src/Disqord.Core/Disqord.Core.csproj @@ -14,4 +14,12 @@ + + + + + + + + diff --git a/src/Disqord.Core/Entities/Core/Interactions/Commands/Slash/ISlashCommandInteractionOption.cs b/src/Disqord.Core/Entities/Core/Interactions/Commands/Slash/ISlashCommandInteractionOption.cs index f47e91144..1fa3d699c 100644 --- a/src/Disqord.Core/Entities/Core/Interactions/Commands/Slash/ISlashCommandInteractionOption.cs +++ b/src/Disqord.Core/Entities/Core/Interactions/Commands/Slash/ISlashCommandInteractionOption.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Disqord.Serialization.Json; namespace Disqord; @@ -23,7 +24,7 @@ public interface ISlashCommandInteractionOption /// /// and are mutually exclusive. /// - object? Value { get; } + IJsonValue? Value { get; } /// /// Gets the nested options of this option. diff --git a/src/Disqord.Core/Entities/Transient/ApplicationCommands/Slash/TransientSlashCommandOptionChoice.cs b/src/Disqord.Core/Entities/Transient/ApplicationCommands/Slash/TransientSlashCommandOptionChoice.cs index 209d12b2a..a13848946 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.ToType()!; 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..5417f8f9d 100644 --- a/src/Disqord.Core/Entities/Transient/Interactions/Commands/Slash/TransientSlashCommandInteractionOption.cs +++ b/src/Disqord.Core/Entities/Transient/Interactions/Commands/Slash/TransientSlashCommandInteractionOption.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Disqord.Models; +using Disqord.Serialization.Json; using Qommon; using Qommon.Collections.ReadOnly; @@ -15,7 +16,7 @@ public class TransientSlashCommandInteractionOption : TransientClientEntity Model.Type; /// - public object? Value => Model.Value.GetValueOrDefault()?.Value; + public IJsonValue? Value => Model.Value.GetValueOrDefault(); /// public IReadOnlyDictionary Options diff --git a/src/Disqord.Core/Library/Library.Debug.cs b/src/Disqord.Core/Library/Library.Debug.cs index 748f698e4..e4d6d326e 100644 --- a/src/Disqord.Core/Library/Library.Debug.cs +++ b/src/Disqord.Core/Library/Library.Debug.cs @@ -10,6 +10,8 @@ public static partial class Library [EditorBrowsable(EditorBrowsableState.Never)] public static class Debug { + public static bool LogSafeDeserializationExceptions; + public static bool DumpJson; public static TextWriter DumpWriter diff --git a/src/Disqord.Core/Models/ApplicationCommands/ApplicationCommandOptionChoiceJsonModel.cs b/src/Disqord.Core/Models/ApplicationCommands/ApplicationCommandOptionChoiceJsonModel.cs index 1e5158238..4d6d4c228 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.ToType(); + 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/Models/Extensions/JsonArrayExtensions.cs b/src/Disqord.Core/Models/Extensions/JsonArrayExtensions.cs new file mode 100644 index 000000000..690a8e1aa --- /dev/null +++ b/src/Disqord.Core/Models/Extensions/JsonArrayExtensions.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Disqord.Serialization.Json; +using Microsoft.Extensions.Logging; + +namespace Disqord.Models; + +internal static class JsonArrayExtensions +{ + public static IEnumerable SafelyDeserializeItems(this IJsonArray jsonArray, ILogger logger, [CallerArgumentExpression(nameof(jsonArray))] string? caller = null) + where TJsonModel : JsonModel + { + var count = jsonArray.Count; + for (var i = 0; i < count; i++) + { + TJsonModel? model = null; + try + { + model = jsonArray[i]?.ToType(); + } + catch (Exception ex) + { + if (Library.Debug.LogSafeDeserializationExceptions) + { + logger.LogWarning(ex, "Failed to deserialize {Caller}.", caller); + } + } + + if (model == null) + { + continue; + } + + yield return model; + } + } +} 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..3fe5b95d8 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,25 @@ public class DefaultJsonNode : IJsonNode /// public JToken Token { get; } - private protected readonly JsonSerializer Serializer; + /// + /// Gets the underlying serializer. + /// + public JsonSerializer Serializer { get; } + + /// + public string Path => Token.Path; + + /// + public JsonValueType Type => Token.Type switch + { + JTokenType.Object => JsonValueType.Object, + JTokenType.Array => JsonValueType.Array, + JTokenType.Integer or JTokenType.Float => JsonValueType.Number, + JTokenType.String or JTokenType.Date or JTokenType.Raw or JTokenType.Bytes or JTokenType.Guid or JTokenType.Uri or JTokenType.TimeSpan => JsonValueType.String, + JTokenType.Boolean when Token.Value() => JsonValueType.True, + JTokenType.Boolean when !Token.Value() => JsonValueType.False, + _ => JsonValueType.Null + }; public DefaultJsonNode(JToken token, JsonSerializer serializer) { @@ -37,34 +56,20 @@ public DefaultJsonNode(JToken token, JsonSerializer serializer) /// /// The string representing this node. /// - public string ToString(Formatting formatting) + public string ToJsonString(JsonFormatting formatting) { - return Token.ToString(formatting); - } - - /// - /// Formats this node into an indented JSON representation. - /// - /// - /// The string representing this node. - /// - public override string ToString() - { - 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. - /// - /// A JSON node representing the object. - /// - public static IJsonNode? Create(object? obj, DefaultJsonSerializer serializer) + [return: NotNullIfNotNull("obj")] + internal 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 +84,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..9ed7ef5e3 100644 --- a/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonValue.cs +++ b/src/Disqord.Core/Serialization/Json/Default/Nodes/DefaultJsonValue.cs @@ -15,13 +15,6 @@ 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) { } @@ -31,4 +24,4 @@ public override string ToString() { return Token.ToString(CultureInfo.InvariantCulture); } -} \ No newline at end of file +} diff --git a/src/Disqord.Core/Serialization/Json/IJsonSerializer.cs b/src/Disqord.Core/Serialization/Json/IJsonSerializer.cs index 1aad7985a..6bbbc0d0d 100644 --- a/src/Disqord.Core/Serialization/Json/IJsonSerializer.cs +++ b/src/Disqord.Core/Serialization/Json/IJsonSerializer.cs @@ -1,13 +1,13 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; -using Disqord.Logging; namespace Disqord.Serialization.Json; /// /// Represents a JSON (de)serializer. /// -public interface IJsonSerializer : ILogging +public interface IJsonSerializer { /// /// Deserializes the UTF-8 JSON stream into an object. @@ -34,5 +34,6 @@ public interface IJsonSerializer : ILogging /// /// A JSON node representing the object. /// - IJsonNode GetJsonNode(object? obj); -} \ No newline at end of file + [return: NotNullIfNotNull(nameof(obj))] + IJsonNode? GetJsonNode(object? obj); +} diff --git a/src/Disqord.Core/Serialization/Json/JsonModel.cs b/src/Disqord.Core/Serialization/Json/JsonModel.cs index 69bb8d7a8..293fdf0f0 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; @@ -9,7 +8,7 @@ namespace Disqord.Serialization.Json; /// /// Represents a JSON model. /// -public class JsonModel : IJsonObject +public class JsonModel : IJsonNode { /// /// The runtime cache for extension data dictionaries. @@ -105,62 +104,17 @@ public static bool TryGetExtensionDatum(JsonModel jsonModel, string name, out return false; } - // public void Add(string key, IJsonNode value) - // => ExtensionData.Add(key, value); + string IJsonNode.Path => "$"; - 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; + JsonValueType IJsonNode.Type => JsonValueType.Object; T IJsonNode.ToType() { throw new NotSupportedException(); } - IEnumerator> IEnumerable>.GetEnumerator() + string IJsonNode.ToJsonString(JsonFormatting formatting) { - return ExtensionData.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return ExtensionData.GetEnumerator(); + 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..fea2b5c72 100644 --- a/src/Disqord.Core/Serialization/Json/Nodes/IJsonNode.cs +++ b/src/Disqord.Core/Serialization/Json/Nodes/IJsonNode.cs @@ -5,6 +5,16 @@ /// public interface IJsonNode { + /// + /// Gets the JSON path of this node. + /// + string Path { get; } + + /// + /// Gets the value kind of this node. + /// + JsonValueType Type { get; } + /// /// Converts this JSON node to the given type. /// @@ -13,4 +23,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 b77a36a22..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 -{ } \ No newline at end of file +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..4519d2d6f 100644 --- a/src/Disqord.Core/Serialization/Json/Nodes/IJsonValue.cs +++ b/src/Disqord.Core/Serialization/Json/Nodes/IJsonValue.cs @@ -4,9 +4,4 @@ /// Represents a JSON value node, i.e. a single JSON value. /// public interface IJsonValue : IJsonNode -{ - /// - /// Gets the value of this JSON node. - /// - object? Value { get; set; } -} \ No newline at end of file +{ } diff --git a/src/Disqord.Core/Serialization/Json/Nodes/JsonValueType.cs b/src/Disqord.Core/Serialization/Json/Nodes/JsonValueType.cs new file mode 100644 index 000000000..ad0a614fc --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/Nodes/JsonValueType.cs @@ -0,0 +1,12 @@ +namespace Disqord.Serialization.Json; + +public enum JsonValueType +{ + Null, + True, + False, + Number, + String, + Array, + Object +} 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..95fc1b8d9 --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/EnumConverter.cs @@ -0,0 +1,192 @@ +using System; +using System.Globalization; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +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; + +internal sealed class EnumConverter : JsonConverterFactory +{ + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsEnum; + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var converterType = typeToConvert.GetCustomAttribute() != null + ? typeof(StringEnumConverterImpl<>) + : typeof(NumberEnumConverterImpl<>); + + return Activator.CreateInstance(converterType.MakeGenericType(typeToConvert)) as JsonConverter; + } + + private class NumberEnumConverterImpl : 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 TEnum ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.ReadUInt64FromString(); + 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]); + } + } + + public override void WriteAsPropertyName(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) + { + var buffer = (stackalloc BufferType[20]); + var ulongValue = ((IConvertible) value).ToUInt64(CultureInfo.InvariantCulture); + ulongValue.TryFormat(buffer, out var countWritten); + writer.WritePropertyName(buffer[..countWritten]); + } + } + + private class StringEnumConverterImpl : JsonConverter + where TEnum : struct, Enum + { + private readonly JsonEncodedText[] _names; + private readonly TEnum[] _values; + + public StringEnumConverterImpl() + { + var names = Enum.GetNames(); + var fields = typeof(TEnum).GetFields(BindingFlags.Public | BindingFlags.Static); + foreach (var field in fields) + { + if (field.GetCustomAttribute() is not EnumMemberAttribute enumMemberAttribute + || enumMemberAttribute.Value == null) + { + continue; + } + + for (var i = 0; i < names.Length; i++) + { + if (field.Name != names[i]) + { + continue; + } + + names[i] = enumMemberAttribute.Value; + break; + } + } + + _names = Array.ConvertAll(names, name => JsonEncodedText.Encode(name)); + _values = Enum.GetValues(); + } + + public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + for (var i = 0; i < _names.Length; i++) + { + var name = _names[i]; + + // TODO: performance check + if (reader.ValueTextEquals(name.EncodedUtf8Bytes)) + { + return _values[i]; + } + } + } + else if (reader.TokenType == JsonTokenType.Number) + { + var numberValue = reader.GetUInt64(); + return Unsafe.As(ref numberValue); + } + + Throw.InvalidOperationException("Invalid enum value."); + return default; + } + + public override TEnum ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + TEnum value = default; + for (var i = 0; i < _names.Length; i++) + { + var name = _names[i]; + + // TODO: performance check + if (reader.ValueTextEquals(name.EncodedUtf8Bytes)) + { + value = _values[i]; + break; + } + } + + return value; + } + + public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) + { + var index = Array.IndexOf(_values, value); + if (index != -1) + { + writer.WriteStringValue(_names[index]); + } + else + { + Throw.InvalidOperationException("Invalid enum value."); + } + } + + public override void WriteAsPropertyName(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) + { + var index = Array.IndexOf(_values, value); + if (index != -1) + { + writer.WritePropertyName(_names[index]); + } + else + { + Throw.InvalidOperationException("Invalid enum value."); + } + } + } +} diff --git a/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/JsonNodeConverter.cs b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/JsonNodeConverter.cs new file mode 100644 index 000000000..35efd718e --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/JsonNodeConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Disqord.Serialization.Json.System; + +internal sealed class JsonNodeConverter : JsonConverter +{ + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsAssignableTo(typeof(IJsonNode)) && !typeToConvert.IsAssignableTo(typeof(JsonModel)); + } + + public override IJsonNode? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var node = JsonNode.Parse(ref reader); + return SystemJsonNode.Create(node, options); + } + + public override void Write(Utf8JsonWriter writer, IJsonNode value, JsonSerializerOptions options) + { + if (value is SystemJsonNode systemJsonNode) + { + systemJsonNode.Node.WriteTo(writer, options); + } + else + { + try + { + JsonSerializer.Serialize(writer, value, value.GetType(), options); + } + catch (JsonException ex) + { + JsonUtilities.RethrowJsonException(ex); + } + } + } +} 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..2ed2161f7 --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/NullableConverter.cs @@ -0,0 +1,58 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Disqord.Serialization.Json.System; + +internal sealed class NullableConverter : JsonConverterFactory +{ + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsValueType && Nullable.GetUnderlyingType(typeToConvert) != null; + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return Activator.CreateInstance(typeof(NullableConverterImpl<>).MakeGenericType(Nullable.GetUnderlyingType(typeToConvert)!)) as JsonConverter; + } + + private sealed class NullableConverterImpl : JsonConverter + where TValue : struct + { + public override TValue? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + try + { + return JsonSerializer.Deserialize(ref reader, options); + } + catch (JsonException ex) + { + JsonUtilities.RethrowJsonException(ex); + return default; + } + } + + public override void Write(Utf8JsonWriter writer, TValue? value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + try + { + JsonSerializer.Serialize(writer, value.Value, options); + } + catch (JsonException ex) + { + JsonUtilities.RethrowJsonException(ex); + } + } + } +} diff --git a/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/OptionalConverter.cs b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/OptionalConverter.cs new file mode 100644 index 000000000..fc00d4643 --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/OptionalConverter.cs @@ -0,0 +1,59 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using Qommon; +using Qommon.Serialization; + +namespace Disqord.Serialization.Json.System; + +internal sealed class OptionalConverter : JsonConverterFactory +{ + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsAssignableTo(typeof(IOptional)) + && typeToConvert.IsValueType + && typeToConvert.IsConstructedGenericType + && typeToConvert.GetGenericTypeDefinition() == typeof(Optional<>); + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return Activator.CreateInstance(typeof(OptionalConverterImpl<>).MakeGenericType(typeToConvert.GenericTypeArguments[0])) as JsonConverter; + } + + private sealed class OptionalConverterImpl : JsonConverter> + { + public override Optional Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + try + { + return JsonSerializer.Deserialize(ref reader, options); + } + catch (JsonException ex) + { + JsonUtilities.RethrowJsonException(ex); + return default; + } + } + + public override void Write(Utf8JsonWriter writer, Optional value, JsonSerializerOptions options) + { + var optionalValue = value.Value; + if (optionalValue == null) + { + writer.WriteNullValue(); + } + else + { + try + { + JsonSerializer.Serialize(writer, value.Value, options); + } + catch (JsonException ex) + { + JsonUtilities.RethrowJsonException(ex); + } + } + } + } +} diff --git a/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/SnowflakeConverter.cs b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/SnowflakeConverter.cs new file mode 100644 index 000000000..73224e7ba --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/SnowflakeConverter.cs @@ -0,0 +1,47 @@ +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; + +internal sealed class SnowflakeConverter : JsonConverter +{ + /// + public override bool HandleNull => false; + + public override Snowflake Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + return reader.ReadUInt64FromString(); + } + + return reader.GetUInt64(); + } + + public override Snowflake ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.ReadUInt64FromString(); + } + + public override void Write(Utf8JsonWriter writer, Snowflake value, JsonSerializerOptions options) + { + var buffer = (stackalloc BufferType[20]); + value.RawValue.TryFormat(buffer, out var countWritten); + writer.WriteStringValue(buffer[..countWritten]); + } + + public override void WriteAsPropertyName(Utf8JsonWriter writer, Snowflake value, JsonSerializerOptions options) + { + var buffer = (stackalloc BufferType[20]); + value.RawValue.TryFormat(buffer, out var countWritten); + writer.WritePropertyName(buffer[..countWritten]); + } +} diff --git a/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/StreamConverter.cs b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/StreamConverter.cs new file mode 100644 index 000000000..8c81d548b --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/StreamConverter.cs @@ -0,0 +1,75 @@ +using System; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Qommon; + +namespace Disqord.Serialization.Json.System; + +internal sealed class StreamConverter : JsonConverter +{ + // This header works regardless of the actual type of the attachment. + public const string Header = "data:image/jpeg;base64,"; + + public override Stream? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotSupportedException(); + } + + public override void Write(Utf8JsonWriter writer, Stream stream, JsonSerializerOptions options) + { + Guard.CanRead(stream); + + StringBuilder base64Builder; + if (stream.CanSeek) + { + // Check if it's an empty attachment. + if (stream.Length == 0) + { + writer.WriteStringValue(Header); + return; + } + + // 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 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())); + writer.WriteStringValue(base64); + return; + } + + // Because the stream is seekable we can use its length and position to roughly calculate its base64 length. + base64Builder = new StringBuilder(Header, (int) ((stream.Length - stream.Position) * 1.37f) + Header.Length); + } + else + { + // If the stream isn't seekable all we can do is start reading from it. + base64Builder = new StringBuilder(Header); + } + + // Allocate a byte span buffer for reading data from the stream + // and a char span buffer for the base64 encoded bytes (3/4 ratio). + Span byteSpan = stackalloc byte[3072]; + Span charSpan = stackalloc char[4096]; + int bytesRead; + 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()); + } +} diff --git a/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/StringConverter.cs b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/StringConverter.cs new file mode 100644 index 000000000..282d2d087 --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/Entities/Converters/StringConverter.cs @@ -0,0 +1,59 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Disqord.Serialization.Json.System; + +internal sealed class StringConverter : JsonConverter +{ + /// + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + return reader.GetString(); + } + + if (reader.TokenType == JsonTokenType.Number) + { + return reader.GetUInt64().ToString(); + } + + if (reader.TokenType == JsonTokenType.True) + { + return bool.TrueString; + } + + if (reader.TokenType == JsonTokenType.False) + { + return bool.FalseString; + } + + return null; + } + + /// + public override string? ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetString(); + } + + /// + public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + } + else + { + writer.WriteStringValue(value.AsSpan()); + } + } + + /// + public override void WriteAsPropertyName(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + writer.WritePropertyName(value.AsSpan()); + } +} diff --git a/src/Disqord.Core/Serialization/Json/STJ/Entities/JsonTypeInfoResolver.cs b/src/Disqord.Core/Serialization/Json/STJ/Entities/JsonTypeInfoResolver.cs new file mode 100644 index 000000000..842ee6d7d --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/Entities/JsonTypeInfoResolver.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Nodes; +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 PropertyInfo _ignoreConditionProperty; + + private static readonly ConditionalWeakTable> _extensionDataCache = new(); + + public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) + { + var jsonTypeInfo = base.GetTypeInfo(type, options); + var jsonProperties = jsonTypeInfo.Properties; + var jsonPropertyCount = jsonProperties.Count; + List? jsonPropertiesToRemove = null; + for (var i = 0; i < jsonPropertyCount; i++) + { + var jsonProperty = jsonProperties[i]; + var fieldInfo = jsonProperty.AttributeProvider as FieldInfo; + if (fieldInfo == null) + { + (jsonPropertiesToRemove ??= new()).Add(jsonProperty); + continue; + } + + var attributes = fieldInfo.GetCustomAttributes(); + JsonPropertyAttribute? jsonPropertyAttribute = null; + foreach (var attribute in attributes) + { + if (attribute is JsonIgnoreAttribute) + { + jsonPropertyAttribute = null; + break; + } + + if (jsonPropertyAttribute == null) + { + if (attribute is JsonPropertyAttribute) + { + jsonPropertyAttribute = Unsafe.As(attribute); + } + } + } + + if (jsonPropertyAttribute == null) + { + (jsonPropertiesToRemove ??= new()).Add(jsonProperty); + continue; + } + + jsonProperty.Name = jsonPropertyAttribute.Name; + + if (typeof(IOptional).IsAssignableFrom(jsonProperty.PropertyType)) + { + if (jsonProperty.PropertyType.GenericTypeArguments.Length == 0) + { + Throw.InvalidOperationException($"JSON property type {jsonProperty.PropertyType} is not supported."); + } + + _ignoreConditionProperty.SetValue(jsonProperty, JsonIgnoreCondition.WhenWritingDefault); + } + else + { + if (jsonPropertyAttribute.NullValueHandling == NullValueHandling.Ignore) + { + _ignoreConditionProperty.SetValue(jsonProperty, JsonIgnoreCondition.WhenWritingNull); + } + } + } + + if (jsonPropertiesToRemove != null) + { + foreach (var jsonProperty in jsonPropertiesToRemove) + jsonTypeInfo.Properties.Remove(jsonProperty); + } + + if (type.IsAssignableTo(typeof(JsonModel))) + { + var extensionData = jsonTypeInfo.CreateJsonPropertyInfo(typeof(Dictionary), "InternalExtensionData"); + extensionData.IsExtensionData = true; + + // Necessary for STJ to deserialize the extension data. + extensionData.Set = static (_, _) => { }; + + extensionData.Get = obj => + { + var model = Guard.IsAssignableToType(obj); + return _extensionDataCache.GetValue(model, model => + { + var extensionData = new Dictionary(); + foreach (var property in model.ExtensionData) + { + extensionData[property.Key] = property.Value is JsonModel modelValue + ? JsonSerializer.SerializeToNode(modelValue, options) + : property.Value?.ToType(); + } + + return extensionData; + }); + }; + + // Flush InternalExtensionData to JsonModel.ExtensionData + jsonTypeInfo.OnDeserialized += obj => + { + var model = Guard.IsAssignableToType(obj); + if (_extensionDataCache.TryGetValue(model, out var extensionData)) + { + model.ExtensionData.Clear(); + + foreach (var property in extensionData) + { + model.ExtensionData[property.Key] = SystemJsonNode.Create(JsonSerializer.SerializeToNode(property.Value, options), options); + } + + _extensionDataCache.Remove(model); + } + }; + + jsonTypeInfo.Properties.Add(extensionData); + } + + return jsonTypeInfo; + } + + 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..1a56b626a --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/JsonUtilities.cs @@ -0,0 +1,42 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +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 + } + + [StackTraceHidden] + [DoesNotReturn] + public static void RethrowJsonException(JsonException ex) + { + throw new JsonException(null, ex.InnerException); + } +} diff --git a/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonArray.cs b/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonArray.cs new file mode 100644 index 000000000..2a8156be7 --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonArray.cs @@ -0,0 +1,133 @@ +using System.Collections; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Disqord.Serialization.Json.System; + +/// +/// Represents a default JSON array node. +/// Wraps a . +/// +internal sealed class SystemJsonArray : SystemJsonNode, IJsonArray +{ + /// + public new JsonArray Node => (base.Node as JsonArray)!; + + /// + public int Count => Node.Count; + + /// + public IJsonNode? this[int index] + { + get => Create(Node[index], Options); + set => Node[index] = GetSystemNode(value); + } + + bool ICollection.IsReadOnly => false; + + internal 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)); + } + + /// + 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 Node.Remove(GetSystemNode(item)); + } + + /// + public int IndexOf(IJsonNode? item) + { + return Node.IndexOf(GetSystemNode(item)); + } + + /// + public void Insert(int index, IJsonNode? item) + { + Node.Insert(index, GetSystemNode(item)); + } + + /// + public void RemoveAt(int index) + { + Node.RemoveAt(index); + } + + /// + public IEnumerator GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private sealed class Enumerator : IEnumerator + { + public IJsonNode? Current => Create(_current?.Node, _array.Options); + + 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) + { + _current = (_array[index] as SystemJsonNode)!; + return true; + } + + _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 new file mode 100644 index 000000000..4ee90f546 --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonNode.cs @@ -0,0 +1,110 @@ +using System; +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 . +/// +internal abstract class SystemJsonNode : IJsonNode +{ + /// + /// Gets the underlying . + /// + public JsonNode Node { get; } + + /// + /// Gets the underlying serializer options. + /// + public JsonSerializerOptions Options { get; } + + /// + public string Path => Node.GetPath(); + + /// + public JsonValueType Type => Node.GetValueKind() switch + { + JsonValueKind.Object => JsonValueType.Object, + JsonValueKind.Array => JsonValueType.Array, + JsonValueKind.String => JsonValueType.String, + JsonValueKind.Number => JsonValueType.Number, + JsonValueKind.True => JsonValueType.True, + JsonValueKind.False => JsonValueType.False, + _ => JsonValueType.Null + }; + + private protected SystemJsonNode(JsonNode node, JsonSerializerOptions options) + { + Node = node; + Options = options; + } + + /// + public T? ToType() + { + try + { + var value = Node.Deserialize(Options); + if (typeof(T) != typeof(JsonElement) && value is JsonElement) + { + Throw.ArgumentException($"Cannot convert the value to type {typeof(T)}."); + } + + return value; + } + catch (JsonException ex) + { + SystemJsonSerializer.ThrowSerializationException(isDeserialize: true, typeof(T), ex); + return default; + } + } + + /// + public string ToJsonString(JsonFormatting formatting) + { + return Node.ToJsonString(new JsonSerializerOptions(Options) + { + WriteIndented = formatting == JsonFormatting.Indented + }); + } + + [return: NotNullIfNotNull("obj")] + internal static IJsonNode? Create(object? obj, JsonSerializerOptions options) + { + try + { + var node = JsonSerializer.SerializeToNode(obj, options); + return Create(node, options); + } + catch (JsonException ex) + { + SystemJsonSerializer.ThrowSerializationException(isDeserialize: false, obj?.GetType() ?? typeof(object), ex); + return null; + } + } + + [return: NotNullIfNotNull("node")] + internal static IJsonNode? Create(JsonNode? node, JsonSerializerOptions options) + { + return node switch + { + null => null, + 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 new file mode 100644 index 000000000..00bf169cc --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonObject.cs @@ -0,0 +1,140 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Disqord.Serialization.Json.System; + +/// +/// Represents a default JSON object node. +/// Wraps a . +/// +internal sealed class SystemJsonObject : SystemJsonNode, IJsonObject +{ + /// + public new JsonObject Node => (base.Node as JsonObject)!; + + /// + public int Count => Node.Count; + + /// + 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; + + internal SystemJsonObject(JsonObject @object, JsonSerializerOptions options) + : base(@object, options) + { } + + /// + public void Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + /// + 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 Node.ContainsKey(key); + } + + /// + public bool TryGetValue(string key, out IJsonNode? value) + { + if (Node.TryGetPropertyValue(key, out var node)) + { + value = Create(node, Options); + return true; + } + + value = null; + return false; + } + + /// + public bool Remove(string key) + { + return Node.Remove(key); + } + + private sealed class Enumerator : IEnumerator> + { + public KeyValuePair Current => KeyValuePair.Create(_enumerator.Current.Key, Create(_enumerator.Current.Value, _options)); + + object IEnumerator.Current => Current; + + private readonly IEnumerator> _enumerator; + private readonly JsonSerializerOptions _options; + + internal Enumerator(IEnumerator> enumerator, JsonSerializerOptions options) + { + _enumerator = enumerator; + _options = options; + } + + public bool MoveNext() + => _enumerator.MoveNext(); + + public void Reset() + => _enumerator.Reset(); + + public void Dispose() + => _enumerator.Dispose(); + } + + /// + public IEnumerator> GetEnumerator() + { + return new Enumerator(Node.GetEnumerator(), Options); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} diff --git a/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonValue.cs b/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonValue.cs new file mode 100644 index 000000000..c7fa7c333 --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/Nodes/SystemJsonValue.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Disqord.Serialization.Json.System; + +/// +/// Represents a default JSON value node. +/// Wraps a . +/// +[DebuggerDisplay($"{nameof(DebuggerDisplay)}")] +internal sealed class SystemJsonValue : SystemJsonNode, IJsonValue +{ + internal SystemJsonValue(JsonValue value, JsonSerializerOptions options) + : base(value, options) + { } + + private string DebuggerDisplay => Node.ToJsonString(Options); + + /// + public override string? ToString() + { + return Node.ToJsonString(Options); + } +} diff --git a/src/Disqord.Core/Serialization/Json/STJ/SystemJsonSerializer.cs b/src/Disqord.Core/Serialization/Json/STJ/SystemJsonSerializer.cs new file mode 100644 index 000000000..757d194cf --- /dev/null +++ b/src/Disqord.Core/Serialization/Json/STJ/SystemJsonSerializer.cs @@ -0,0 +1,91 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Disqord.Serialization.Json.System; + +/// +/// Represents a System.Text.Json implementation of . +/// +public sealed class SystemJsonSerializer : IJsonSerializer +{ + /// + /// Gets the underlying . + /// + internal JsonSerializerOptions UnderlyingOptions { get; } + + public SystemJsonSerializer() + { + UnderlyingOptions = new JsonSerializerOptions + { + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, + IncludeFields = true, + IgnoreReadOnlyFields = true, + IgnoreReadOnlyProperties = true, + RespectNullableAnnotations = true, + NewLine = "\n", + UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode, + TypeInfoResolver = new JsonTypeInfoResolver(), + Converters = + { + new EnumConverter(), + new JsonNodeConverter(), + new NullableConverter(), + new OptionalConverter(), + new StringConverter(), + new SnowflakeConverter(), + new StreamConverter(), + } + }; + + UnderlyingOptions.MakeReadOnly(); + } + + /// + public object? Deserialize(Stream stream, Type type) + { + try + { + return JsonSerializer.Deserialize(stream, type, UnderlyingOptions); + } + catch (JsonException ex) + { + ThrowSerializationException(isDeserialize: true, type, ex); + return null; + } + } + + /// + public void Serialize(Stream stream, object obj, IJsonSerializerOptions? options = null) + { + try + { + var serializerOptions = UnderlyingOptions; + if (options != null && options.Formatting == JsonFormatting.Indented) + { + serializerOptions = new JsonSerializerOptions(UnderlyingOptions); + serializerOptions.WriteIndented = true; + } + + JsonSerializer.Serialize(stream, obj, serializerOptions); + } + catch (JsonException ex) + { + ThrowSerializationException(isDeserialize: false, obj.GetType(), ex); + } + } + + /// + [return: NotNullIfNotNull(nameof(obj))] + public IJsonNode? GetJsonNode(object? obj) + { + return SystemJsonNode.Create(obj, UnderlyingOptions); + } + + internal static void ThrowSerializationException(bool isDeserialize, Type type, Exception exception) + { + throw new JsonSerializationException(isDeserialize, type, exception); + } +} diff --git a/src/Disqord.Gateway.Api/Default/Sharding/DefaultShard.cs b/src/Disqord.Gateway.Api/Default/Sharding/DefaultShard.cs index 6961cdd72..0863d6f5d 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)?.ToType(); + var resumeGatewayUrl = (d["resume_gateway_url"] as IJsonValue)?.ToType(); if (resumeGatewayUrl != null) { ResumeUri = new Uri(resumeGatewayUrl); diff --git a/src/Disqord.Gateway.Api/Models/GatewayGuildJsonModel.cs b/src/Disqord.Gateway.Api/Models/GatewayGuildJsonModel.cs index b8c0451ff..116cf6690 100644 --- a/src/Disqord.Gateway.Api/Models/GatewayGuildJsonModel.cs +++ b/src/Disqord.Gateway.Api/Models/GatewayGuildJsonModel.cs @@ -25,7 +25,7 @@ public class GatewayGuildJsonModel : GuildJsonModel public VoiceStateJsonModel[] VoiceStates = null!; [JsonProperty("members")] - public MemberJsonModel[] Members = null!; + public IJsonArray Members = null!; [JsonProperty("channels")] public ChannelJsonModel[] Channels = null!; @@ -34,35 +34,11 @@ public class GatewayGuildJsonModel : GuildJsonModel public ChannelJsonModel[] Threads = null!; [JsonProperty("presences")] - public IJsonNode[] Presences = null!; + public IJsonArray Presences = null!; [JsonProperty("stage_instances")] public StageInstanceJsonModel[] StageInstances = null!; [JsonProperty("guild_scheduled_events")] public GuildScheduledEventJsonModel[] GuildScheduledEvents = null!; - - // Not ideal - handling the deserialization error at the serializer level would be better - public IEnumerable CreatePresences() - { - foreach (var node in Presences) - { - PresenceJsonModel? model = null; - try - { - model = node.ToType(); - } - catch - { - // Ignore bad presence data. - } - - if (model == null) - { - continue; - } - - yield return model; - } - } } diff --git a/src/Disqord.Gateway/Default/Dispatcher/DefaultGatewayDispatcher.cs b/src/Disqord.Gateway/Default/Dispatcher/DefaultGatewayDispatcher.cs index 8414c9f2d..334b3bdbb 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; @@ -238,7 +239,7 @@ public async Task HandleDispatchAsync(object? sender, GatewayDispatchReceivedEve } catch (Exception ex) { - Logger.LogError(ex, "An exception occurred while handling dispatch {0}.\n{1}", e.Name, e.Data?.ToString()); + Logger.LogError(ex, "An exception occurred while handling dispatch {0}.\n{1}", e.Name, e.Data?.ToJsonString(JsonFormatting.Indented)); } } @@ -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/Guild/GUILD_CREATE.cs b/src/Disqord.Gateway/Default/Dispatcher/Dispatches/Guild/GUILD_CREATE.cs index c4089262e..220d16da6 100644 --- a/src/Disqord.Gateway/Default/Dispatcher/Dispatches/Guild/GUILD_CREATE.cs +++ b/src/Disqord.Gateway/Default/Dispatcher/Dispatches/Guild/GUILD_CREATE.cs @@ -2,6 +2,8 @@ using System.Threading.Tasks; using Disqord.Gateway.Api; using Disqord.Gateway.Api.Models; +using Disqord.Models; +using Disqord.Serialization.Json; using Microsoft.Extensions.Logging; namespace Disqord.Gateway.Default.Dispatcher; @@ -17,6 +19,25 @@ public override void Bind(DefaultGatewayDispatcher value) base.Bind(value); } + public override async ValueTask HandleDispatchAsync(IShard shard, IJsonNode data) + { + try + { + await base.HandleDispatchAsync(shard, data).ConfigureAwait(false); + } + catch (Exception) + { + // If an exception occurs during deserialization, make sure to still pop the pending guild. + if (data is IJsonObject jsonObject && jsonObject.TryGetValue("id", out var guildIdNode) && guildIdNode != null) + { + var guildId = guildIdNode.ToType(); + _readyDispatchHandler.PopPendingGuild(shard.Id, guildId); + } + + throw; + } + } + public override async ValueTask HandleDispatchAsync(IShard shard, GatewayGuildJsonModel model) { IGatewayGuild guild; @@ -75,7 +96,7 @@ private IGatewayGuild UpdateCache(GatewayGuildJsonModel model, bool isPending) if (CacheProvider.TryGetUsers(out var userCache) && CacheProvider.TryGetMembers(model.Id, out var memberCache)) { - foreach (var memberModel in model.Members) + foreach (var memberModel in model.Members.SafelyDeserializeItems(Logger)) Dispatcher.GetOrAddMember(userCache, memberCache, model.Id, memberModel); } @@ -154,7 +175,7 @@ private IGatewayGuild UpdateCache(GatewayGuildJsonModel model, bool isPending) if (CacheProvider.TryGetPresences(model.Id, out var presenceCache)) { - foreach (var presenceModel in model.CreatePresences()) + foreach (var presenceModel in model.Presences.SafelyDeserializeItems(Logger)) { if (isPending) { diff --git a/src/Disqord.Gateway/Default/Dispatcher/Dispatches/Guild/GUILD_DELETE.cs b/src/Disqord.Gateway/Default/Dispatcher/Dispatches/Guild/GUILD_DELETE.cs index 4e8b1c2e3..69ab3c9f3 100644 --- a/src/Disqord.Gateway/Default/Dispatcher/Dispatches/Guild/GUILD_DELETE.cs +++ b/src/Disqord.Gateway/Default/Dispatcher/Dispatches/Guild/GUILD_DELETE.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Disqord.Gateway.Api; using Disqord.Gateway.Api.Models; +using Disqord.Serialization.Json; using Microsoft.Extensions.Logging; namespace Disqord.Gateway.Default.Dispatcher; @@ -17,6 +18,25 @@ public override void Bind(DefaultGatewayDispatcher value) base.Bind(value); } + public override async ValueTask HandleDispatchAsync(IShard shard, IJsonNode data) + { + try + { + await base.HandleDispatchAsync(shard, data).ConfigureAwait(false); + } + catch (Exception) + { + // If an exception occurs during deserialization, make sure to still pop the pending guild. + if (data is IJsonObject jsonObject && jsonObject.TryGetValue("id", out var guildIdNode) && guildIdNode != null) + { + var guildId = guildIdNode.ToType(); + _readyDispatchHandler.PopPendingGuild(shard.Id, guildId); + } + + throw; + } + } + public override async ValueTask HandleDispatchAsync(IShard shard, UnavailableGuildJsonModel model) { CachedGuild? guild = null; 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..16b1cf8ff 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()!; diff --git a/src/Disqord.Gateway/Entities/Transient/Guild/TransientGatewayGuild.cs b/src/Disqord.Gateway/Entities/Transient/Guild/TransientGatewayGuild.cs index f544c36e4..477d7d161 100644 --- a/src/Disqord.Gateway/Entities/Transient/Guild/TransientGatewayGuild.cs +++ b/src/Disqord.Gateway/Entities/Transient/Guild/TransientGatewayGuild.cs @@ -123,7 +123,7 @@ public IReadOnlyDictionary Stickers private IReadOnlyDictionary? _voiceStates; - public IReadOnlyDictionary Members => _members ??= Model.Members.ToReadOnlyDictionary((Client, Id), + public IReadOnlyDictionary Members => _members ??= Model.Members.SafelyDeserializeItems(Client.Logger).ToReadOnlyDictionary((Client, Id), (model, _) => model.User.Value.Id, (model, state) => { var (client, guildId) = state; @@ -138,7 +138,7 @@ public IReadOnlyDictionary Stickers private IReadOnlyDictionary? _channels; - public IReadOnlyDictionary Presences => _presences ??= Model.CreatePresences().ToReadOnlyDictionary(Client, + public IReadOnlyDictionary Presences => _presences ??= Model.Presences.SafelyDeserializeItems(Client.Logger).ToReadOnlyDictionary(Client, (model, _) => model.User.Id, (model, client) => new TransientPresence(client, model) as IPresence); diff --git a/src/Disqord.Rest.Api/Content/Json/Interactions/CreateInitialInteractionResponseJsonRestRequestContent.cs b/src/Disqord.Rest.Api/Content/Json/Interactions/CreateInitialInteractionResponseJsonRestRequestContent.cs index a77d9ff9c..3e5e16dc7 100644 --- a/src/Disqord.Rest.Api/Content/Json/Interactions/CreateInitialInteractionResponseJsonRestRequestContent.cs +++ b/src/Disqord.Rest.Api/Content/Json/Interactions/CreateInitialInteractionResponseJsonRestRequestContent.cs @@ -12,7 +12,7 @@ public class CreateInitialInteractionResponseJsonRestRequestContent : JsonModelR public InteractionResponseType Type; [JsonProperty("data")] - public Optional Data; + public Optional Data; IList IAttachmentRestRequestContent.Attachments { @@ -49,7 +49,8 @@ protected override void OnValidate() OptionalGuard.CheckValue(Data, data => { Guard.IsNotNull(data); - data.Validate(); + var model = Guard.IsAssignableToType(data); + model.Validate(); }); } } 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/Disqord.TestBot.csproj b/src/Disqord.TestBot/Disqord.TestBot.csproj index 1ddf12cf3..d6cefd86c 100644 --- a/src/Disqord.TestBot/Disqord.TestBot.csproj +++ b/src/Disqord.TestBot/Disqord.TestBot.csproj @@ -23,4 +23,4 @@ - \ No newline at end of file + diff --git a/src/Disqord.TestBot/Program.cs b/src/Disqord.TestBot/Program.cs index e382e1a71..96f52dc3e 100644 --- a/src/Disqord.TestBot/Program.cs +++ b/src/Disqord.TestBot/Program.cs @@ -1,10 +1,17 @@ using System; using Disqord.Bot.Hosting; using Disqord.Gateway; +using Disqord.Gateway.Api.Default; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Serilog; using Serilog.Events; using Serilog.Sinks.SystemConsole.Themes; +#if NET8_0_OR_GREATER +using Disqord.Serialization.Json; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Disqord.Serialization.Json.System; +#endif namespace Disqord.TestBot { @@ -23,6 +30,13 @@ private static void Main(string[] args) bot.Prefixes = new[] { "??" }; bot.Intents |= GatewayIntents.DirectMessages | GatewayIntents.DirectReactions; }) + .ConfigureServices(services => + { +#if NET8_0_OR_GREATER + services.Replace(ServiceDescriptor.Singleton()); +#endif + 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