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