Json (de)serilazion works with some tests

This commit is contained in:
Kecskeméti László 2024-06-25 18:40:11 +02:00
parent b4540af9db
commit a7a087b637
13 changed files with 297 additions and 20 deletions

View File

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<PropertyGroup>
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Include="xunit" Version="2.5.3"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"/>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Discord.API\Discord.API.csproj" />
</ItemGroup>
</Project>

View File

@ -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<GatewayPacket>(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<GatewayPacket>(src, SourceGenerationContext.Default.GatewayPacket);
Assert.NotNull(packet);
Assert.IsType<ReadyPacket>(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<ChannelCreatePacket>(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
}
});
}
}

View File

@ -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")]

View File

@ -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")]

View File

@ -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")]

View File

@ -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]

View File

@ -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;

View File

@ -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
{

View File

@ -0,0 +1,72 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Discord.API;
internal class GatewayPacketConverter : JsonConverter<GatewayPacket>
{
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;
}
}
}

View File

@ -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; }
}

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly:InternalsVisibleTo("Discord.API.Tests")]

View File

@ -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
{
}

View File

@ -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