diff --git a/Discord.API.Tests/Discord.API.Tests.csproj b/Discord.API.Tests/Discord.API.Tests.csproj new file mode 100644 index 0000000..8ccb65d --- /dev/null +++ b/Discord.API.Tests/Discord.API.Tests.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + + false + true + + + + false + + + + + + + + + + + + + + + + + + diff --git a/Discord.API.Tests/JsonTests.cs b/Discord.API.Tests/JsonTests.cs new file mode 100644 index 0000000..b953b52 --- /dev/null +++ b/Discord.API.Tests/JsonTests.cs @@ -0,0 +1,149 @@ +using System.Text.Json; + +namespace Discord.API.Tests; +using Xunit; +using Discord.API; + +public class JsonTests +{ + [Fact] + public void GatewayPacketEncodeJsonTest() + { + var gateway_packet = new GatewayPacket() + { + Op = GatewayPacket.Opcode.Heartbeat + }; + + string json = JsonSerializer.Serialize(gateway_packet, SourceGenerationContext.Default.GatewayPacket); + + Assert.Equal("""{"op":1}""", json); + } + + [Fact] + public void GatewayPacketDecodeTest() + { + const string src = """{"op":1}"""; + var gateway_packet = + JsonSerializer.Deserialize(src, SourceGenerationContext.Default.GatewayPacket); + Assert.Equal(GatewayPacket.Opcode.Heartbeat, gateway_packet?.Op); + } + + [Fact] + public void ReadyPacketDeserialize() + { + const string src = """ + { + "op":0, + "t":"READY", + "s":1, + "d":{ + "v":10, + "user":{ + "id":"1234", + "username":"abrakadabra", + "discriminator":"1111", + "global_name":"glblname" + }, + "guilds":[ + { + "id":"5678", + "unavailable":true + } + ], + "session_id":"abcd", + "resume_gateway_url":"dfgh", + "application":{ + "id":"3333", + "flags":5555 + } + } + } + """; + GatewayPacket? packet = + JsonSerializer.Deserialize(src, SourceGenerationContext.Default.GatewayPacket); + + Assert.NotNull(packet); + Assert.IsType(packet); + + Assert.True(packet is ReadyPacket + { + Op: GatewayPacket.Opcode.Dispatch, + Event: "READY", + Sequence: 1, + Data: + { + User: + { + Id:1234, + Username: "abrakadabra", + Discriminator: "1111", + GlobalName: "glblname" + }, + Version: 10, + SessionId: "abcd", + ResumeGatewayUrl: "dfgh", + Guilds:[ + { + Id: 5678, + Unavailable: true + } + ], + Application: + { + Id: 3333, + Flags: 5555 + } + } + }); + } + + [Fact] + public void ChannelCreateDeserialize() + { + string src = """ + { + "op":0, + "t":"CHANNEL_CREATE", + "s":1, + "d":{ + "id": "922243411795390570", + "type": 2, + "guild_id": "5678", + "position": 3, + "name": "voice csennel", + "topic": "A very interesting topic", + "nsfw": true, + "last_message_id":"6969", + "bitrate":420, + "parent_id": "5555" + } + } + """; + + var gateway_packet = + JsonSerializer.Deserialize(src, SourceGenerationContext.Default.GatewayPacket); + + Assert.IsType(gateway_packet); + + Assert.True(gateway_packet is ChannelCreatePacket + { + Op: GatewayPacket.Opcode.Dispatch, + Event: "CHANNEL_CREATE", + Sequence: 1, + Data: + { + Id: 922243411795390570, + Name: "voice csennel", + GuildId: 5678, + Type: 2, + Position: 3, + Topic: "A very interesting topic", + Nsfw: true, + Bitrate: 420, + ParentId: 5555, + LastMessageId: 6969 + } + }); + + } +} \ No newline at end of file diff --git a/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelCreatePacket.cs b/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelCreatePacket.cs index 5a5e9fb..7f33064 100644 --- a/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelCreatePacket.cs +++ b/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelCreatePacket.cs @@ -3,8 +3,7 @@ using System.Text.Json.Serialization; namespace Discord.API; -[JsonDerivedType(typeof(DispatchPacket), "CHANNEL_CREATE")] -public class ChannelCreatePacket : DispatchPacket +internal class ChannelCreatePacket : DispatchPacket { [JsonRequired] [JsonPropertyName("d")] diff --git a/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelDeletePacket.cs b/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelDeletePacket.cs index 2084237..ea7ff8f 100644 --- a/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelDeletePacket.cs +++ b/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelDeletePacket.cs @@ -3,8 +3,7 @@ using System.Text.Json.Serialization; namespace Discord.API; -[JsonDerivedType(typeof(DispatchPacket), "CHANNEL_DELETE")] -public class ChannelDeletePacket : DispatchPacket +internal class ChannelDeletePacket : DispatchPacket { [JsonRequired] [JsonPropertyName("d")] diff --git a/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelUpdatePacket.cs b/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelUpdatePacket.cs index 2f70826..f863ef4 100644 --- a/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelUpdatePacket.cs +++ b/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelUpdatePacket.cs @@ -3,8 +3,7 @@ using System.Text.Json.Serialization; namespace Discord.API; -[JsonDerivedType(typeof(DispatchPacket), "CHANNEL_UPDATE")] -public class ChannelUpdatePacket : DispatchPacket +internal class ChannelUpdatePacket : DispatchPacket { [JsonRequired] [JsonPropertyName("d")] diff --git a/Discord.API/GatewayPacketTypes/DispatchPacket/DispatchPacket.cs b/Discord.API/GatewayPacketTypes/DispatchPacket/DispatchPacket.cs index 1e03bc7..a7ae7b5 100644 --- a/Discord.API/GatewayPacketTypes/DispatchPacket/DispatchPacket.cs +++ b/Discord.API/GatewayPacketTypes/DispatchPacket/DispatchPacket.cs @@ -3,9 +3,12 @@ using System.Text.Json.Serialization; namespace Discord.API; -[JsonDerivedType(typeof(GatewayPacket), (int)Opcode.Dispatch)] [JsonPolymorphic(IgnoreUnrecognizedTypeDiscriminators = true, TypeDiscriminatorPropertyName = "t", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -public class DispatchPacket : GatewayPacket +[JsonDerivedType(typeof(ChannelCreatePacket))] +[JsonDerivedType(typeof(ChannelUpdatePacket))] +[JsonDerivedType(typeof(ChannelDeletePacket))] +[JsonDerivedType(typeof(ReadyPacket))] +internal class DispatchPacket : GatewayPacket { [JsonPropertyName("t")] [JsonRequired] diff --git a/Discord.API/GatewayPacketTypes/DispatchPacket/ReadyPacket.cs b/Discord.API/GatewayPacketTypes/DispatchPacket/ReadyPacket.cs index 66d84a6..064ae15 100644 --- a/Discord.API/GatewayPacketTypes/DispatchPacket/ReadyPacket.cs +++ b/Discord.API/GatewayPacketTypes/DispatchPacket/ReadyPacket.cs @@ -3,8 +3,9 @@ using System.Text.Json.Serialization; namespace Discord.API; -[JsonDerivedType(typeof(DispatchPacket), "READY")] -public class ReadyPacket : DispatchPacket +[JsonPolymorphic(TypeDiscriminatorPropertyName = "t", IgnoreUnrecognizedTypeDiscriminators = true, UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(ReadyPacket))] +internal class ReadyPacket : DispatchPacket { public sealed class ReadyData { @@ -15,13 +16,13 @@ public class ReadyPacket : DispatchPacket public UserData User; [JsonRequired] - public UnavailableGuildData Guilds; + public UnavailableGuildData[] Guilds; [JsonRequired] public string SessionId; [JsonRequired] - public string ResumeUrl; + public string ResumeGatewayUrl; [JsonRequired] public PartialApplicationData Application; diff --git a/Discord.API/GatewayPacketTypes/GatewayPacket.cs b/Discord.API/GatewayPacketTypes/GatewayPacket.cs index b7b0d40..cce0c8e 100644 --- a/Discord.API/GatewayPacketTypes/GatewayPacket.cs +++ b/Discord.API/GatewayPacketTypes/GatewayPacket.cs @@ -1,9 +1,12 @@ +using System.Runtime.CompilerServices; using System.Text.Json.Serialization; namespace Discord.API; -[JsonPolymorphic(IgnoreUnrecognizedTypeDiscriminators = false, TypeDiscriminatorPropertyName = "op", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -public class GatewayPacket +[JsonPolymorphic(IgnoreUnrecognizedTypeDiscriminators = true, TypeDiscriminatorPropertyName = "op", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(IdentifyPacket))] +[JsonDerivedType(typeof(DispatchPacket))] +internal class GatewayPacket { public enum Opcode { diff --git a/Discord.API/GatewayPacketTypes/GatewayPacketConverter.cs b/Discord.API/GatewayPacketTypes/GatewayPacketConverter.cs new file mode 100644 index 0000000..33128c8 --- /dev/null +++ b/Discord.API/GatewayPacketTypes/GatewayPacketConverter.cs @@ -0,0 +1,72 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Discord.API; + +internal class GatewayPacketConverter : JsonConverter +{ + public override GatewayPacket? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + JsonDocument json_doc = JsonDocument.ParseValue(ref reader); + if(json_doc.RootElement.TryGetProperty("op", out JsonElement opcode_element)) + { + if (opcode_element.ValueKind != JsonValueKind.Number) + { + throw new JsonException("Opcode is not a number"); + } + + GatewayPacket.Opcode op = (GatewayPacket.Opcode)opcode_element.GetInt32(); + + switch (op) + { + case GatewayPacket.Opcode.Dispatch: + if (json_doc.RootElement.TryGetProperty("t", out JsonElement event_element)) + { + if (event_element.ValueKind != JsonValueKind.String) + { + throw new Exception("Event name not string"); + } + + string? event_name = event_element.GetString(); + switch (event_name) + { + case "READY": + return json_doc.Deserialize(SourceGenerationContext.Default.ReadyPacket); + case "CHANNEL_CREATE": + return json_doc.Deserialize(SourceGenerationContext.Default + .ChannelCreatePacket); + case "CHANNEL_UPDATE": + return json_doc.Deserialize(SourceGenerationContext.Default.ChannelUpdatePacket); + case "CHANNEL_DELETE": + return json_doc.Deserialize(SourceGenerationContext.Default.ChannelDeletePacket); + default: + throw new NotSupportedException($"Packet {event_name} is not supported"); + } + } + else + { + throw new JsonException("Event name not found"); + } + default: + throw new NotSupportedException($"Opcode {op} is not supported in json deserialization"); + } + } + else + { + throw new Exception("Opcode not found"); + } + } + + public override void Write(Utf8JsonWriter writer, GatewayPacket value, JsonSerializerOptions options) + { + switch (value) + { + case IdentifyPacket id_packet: + writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes(id_packet, SourceGenerationContext.Default.IdentifyPacket)); + break; + default: + writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes(value, SourceGenerationContext.Default.IdentifyPacket)); + break; + } + } +} \ No newline at end of file diff --git a/Discord.API/GatewayPacketTypes/IdentifyPacket.cs b/Discord.API/GatewayPacketTypes/IdentifyPacket.cs index 4dd2c03..8250d04 100644 --- a/Discord.API/GatewayPacketTypes/IdentifyPacket.cs +++ b/Discord.API/GatewayPacketTypes/IdentifyPacket.cs @@ -1,9 +1,9 @@ using System.Text.Json.Serialization; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. namespace Discord.API; -//[JsonDerivedType(typeof(GatewayPacket), (int)Opcode.Identify)] -public class IdentifyPacket : GatewayPacket +internal class IdentifyPacket : GatewayPacket { public sealed class IdentifyData { @@ -19,7 +19,7 @@ public class IdentifyPacket : GatewayPacket [JsonInclude] [JsonRequired] [JsonPropertyName("token")] - public required string Token { get; set; } + public required string Token { get; init; } [JsonInclude] public PropertiesClass Properties => PropertiesClass.Instance; [JsonInclude] @@ -27,10 +27,10 @@ public class IdentifyPacket : GatewayPacket public int LargeThreshold = 250; public int[]? Shard = null; //presence - public ulong Intents { get; init; } + public required ulong Intents { get; init; } } [JsonPropertyName("d")] - public IdentifyData Data; + public required IdentifyData Data { get; init; } } \ No newline at end of file diff --git a/Discord.API/InternalsVisible.cs b/Discord.API/InternalsVisible.cs new file mode 100644 index 0000000..1e26b11 --- /dev/null +++ b/Discord.API/InternalsVisible.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("Discord.API.Tests")] \ No newline at end of file diff --git a/Discord.API/SourceGenerationContext.cs b/Discord.API/SourceGenerationContext.cs index e48343e..80ffd15 100644 --- a/Discord.API/SourceGenerationContext.cs +++ b/Discord.API/SourceGenerationContext.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices.JavaScript; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -5,9 +6,20 @@ namespace Discord.API; using System.Text.Json; -[JsonSourceGenerationOptions(IgnoreReadOnlyFields = false, IgnoreReadOnlyProperties = false, IncludeFields = true, PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower)] +[JsonSourceGenerationOptions(IgnoreReadOnlyFields = false, + IgnoreReadOnlyProperties = false, + IncludeFields = true, + PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower, + Converters = [typeof(GatewayPacketConverter)], + NumberHandling = JsonNumberHandling.AllowReadingFromString + )] [JsonSerializable(typeof(GatewayPacket))] [JsonSerializable(typeof(IdentifyPacket))] +[JsonSerializable(typeof(ChannelCreatePacket))] +[JsonSerializable(typeof(ChannelUpdatePacket))] +[JsonSerializable(typeof(ChannelDeletePacket))] +[JsonSerializable(typeof(DispatchPacket))] +[JsonSerializable(typeof(ReadyPacket))] internal partial class SourceGenerationContext : JsonSerializerContext { } \ No newline at end of file diff --git a/discord_api.sln b/discord_api.sln index f120141..c689fa3 100644 --- a/discord_api.sln +++ b/discord_api.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.API", "Discord.API\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "example_bot", "example_bot\example_bot.csproj", "{331F5928-8E65-4782-8311-BC1E80F1C002}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.API.Tests", "Discord.API.Tests\Discord.API.Tests.csproj", "{E89333A2-11C4-4CFF-9F45-32D951E871CA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -30,5 +32,9 @@ Global {331F5928-8E65-4782-8311-BC1E80F1C002}.Debug|Any CPU.Build.0 = Debug|Any CPU {331F5928-8E65-4782-8311-BC1E80F1C002}.Release|Any CPU.ActiveCfg = Release|Any CPU {331F5928-8E65-4782-8311-BC1E80F1C002}.Release|Any CPU.Build.0 = Release|Any CPU + {E89333A2-11C4-4CFF-9F45-32D951E871CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E89333A2-11C4-4CFF-9F45-32D951E871CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E89333A2-11C4-4CFF-9F45-32D951E871CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E89333A2-11C4-4CFF-9F45-32D951E871CA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal