does not support property omission at this time, will need to be added later using a separate converter and base marker class. -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 "git failing to recognize gpg key, this identity is still valid" -----BEGIN PGP SIGNATURE----- iQIzBAEBCgAdFiEErbDRxgZ77MPT+ajAOrLKmA3cYakFAl4bthEACgkQOrLKmA3c YamuWw/7Bn/Ks0mTRN3tg3Z/voETJ/8JQZXJEiW7wwv8c7nSOemxRNB/Tmzo3kzC N6T5fH7Gep4o4iA7CfJ5CZtx+OY92OpyBwsJgkNvANVpjXWCeDaww0Ci5dyVwFUk fFq21l6p2sbM6PB9sEOCvryeIOrgkqBl915MkAlj+/UtnAQ9qFhomIGNLPPFeYOS eaHWjZF6ArbF5NMaOhboDDCIl2nCf+RGEetDoBP2BRaIf+eOyl0lGyQqiY1mNqkD DX8nmcaY5/Lnxhf3pwmYZbqKBPQt5R2FxmqWTg5ey0R4//izE4TJ54nlhdSnTZpH 7ZligmR9rQFdQ5jbSq6cIclo9i988ELHKBgt8mG3SiC4AT0+SBXRpPRBitkA0CPb O4W8J0HrbSFmILx9Zvuy72KC/Zzo+SOS8257S35ihosrlyupcR4zladVcIviAPWk Ovpy85W4uxPdWc6zkMOZSx9OiYFYkNlK/QdNJBXGg7LLcaLf8p33lj+T8UXa7dyC Sw/pW5RL1FYalh7iXF55ylJrKo+oySBejods+ATnmYG4JMywO+GNCE+XLCcDpoBx 9H2z0qJNb5Dgkc4cRulKwYEoT+LQKUhLFdj4wNEqE8mBw0ZoxUiBBqOD1TiZr2mf 1AFQVS/AeOc03t25OfmhNz026OAGy01bjeHr09deT20dsssEpQY= =n76m -----END PGP SIGNATURE-----next
@@ -11,6 +11,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{479564 | |||||
EndProject | EndProject | ||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "idn", "sample\idn\idn.csproj", "{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}" | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "idn", "sample\idn\idn.csproj", "{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}" | ||||
EndProject | EndProject | ||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{68EE1EAC-F487-4BAC-917B-233370B3AEA1}" | |||||
EndProject | |||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Tests.Unit", "test\Discord.Tests.Unit\Discord.Tests.Unit.csproj", "{6AD4FF67-D45E-4E7E-8853-990390D35C9F}" | |||||
EndProject | |||||
Global | Global | ||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
Debug|Any CPU = Debug|Any CPU | Debug|Any CPU = Debug|Any CPU | ||||
@@ -48,9 +52,22 @@ Global | |||||
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|x64.Build.0 = Release|Any CPU | {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|x64.Build.0 = Release|Any CPU | ||||
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|x86.ActiveCfg = Release|Any CPU | {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|x86.ActiveCfg = Release|Any CPU | ||||
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|x86.Build.0 = Release|Any CPU | {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|x86.Build.0 = Release|Any CPU | ||||
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||||
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||||
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|x64.ActiveCfg = Debug|Any CPU | |||||
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|x64.Build.0 = Debug|Any CPU | |||||
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|x86.ActiveCfg = Debug|Any CPU | |||||
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|x86.Build.0 = Debug|Any CPU | |||||
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||||
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|Any CPU.Build.0 = Release|Any CPU | |||||
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|x64.ActiveCfg = Release|Any CPU | |||||
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|x64.Build.0 = Release|Any CPU | |||||
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|x86.ActiveCfg = Release|Any CPU | |||||
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|x86.Build.0 = Release|Any CPU | |||||
EndGlobalSection | EndGlobalSection | ||||
GlobalSection(NestedProjects) = preSolution | GlobalSection(NestedProjects) = preSolution | ||||
{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370} = {5DAC796B-0B77-4F84-B790-83DB78C6DFFE} | {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370} = {5DAC796B-0B77-4F84-B790-83DB78C6DFFE} | ||||
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A} = {4795640A-030C-4A9A-A9B0-20C56AF4DA3F} | {5BE5DE89-53B7-4243-AEA8-FD8A6420908A} = {4795640A-030C-4A9A-A9B0-20C56AF4DA3F} | ||||
{6AD4FF67-D45E-4E7E-8853-990390D35C9F} = {68EE1EAC-F487-4BAC-917B-233370B3AEA1} | |||||
EndGlobalSection | EndGlobalSection | ||||
EndGlobal | EndGlobal |
@@ -1,10 +1,10 @@ | |||||
using System.Text.Json; | using System.Text.Json; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Refit; | |||||
using Discord.Models; | |||||
using System.Net.Http.Headers; | using System.Net.Http.Headers; | ||||
using System; | |||||
using System.Net.Http; | using System.Net.Http; | ||||
using Refit; | |||||
using Discord.Models; | |||||
using Discord.Serialization; | |||||
// This is essentially a reimplementation of Wumpus.Net.Rest | // This is essentially a reimplementation of Wumpus.Net.Rest | ||||
namespace Discord.Rest | namespace Discord.Rest | ||||
@@ -26,6 +26,7 @@ namespace Discord.Rest | |||||
}; | }; | ||||
var jsonOptions = new JsonSerializerOptions(); | var jsonOptions = new JsonSerializerOptions(); | ||||
jsonOptions.Converters.Add(new OptionalConverter()); | |||||
var refitSettings = new RefitSettings | var refitSettings = new RefitSettings | ||||
{ | { | ||||
ContentSerializer = new JsonContentSerializer(jsonOptions), | ContentSerializer = new JsonContentSerializer(jsonOptions), | ||||
@@ -1,22 +1,41 @@ | |||||
using System; | using System; | ||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
using System.Text.Json; | using System.Text.Json; | ||||
using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||
namespace Discord.Serialization | namespace Discord.Serialization | ||||
{ | { | ||||
// 😅 | |||||
public class OptionalConverter<T> : JsonConverter<Optional<T>> | |||||
// TODO: This does not allow us to omit properties at runtime | |||||
// Need to evaluate which cases need us to omit properties and write a separate converter | |||||
// for those. At this time I can only think of the outgoing REST PATCH requests. Incoming | |||||
// omitted properties will be correctly treated as Optional.Unspecified (the default) | |||||
public class OptionalConverter : JsonConverterFactory | |||||
{ | { | ||||
public override Optional<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | |||||
private class OptionalTypeConverter<T> : JsonConverter<Optional<T>> | |||||
{ | { | ||||
throw new NotImplementedException(); | |||||
public override Optional<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | |||||
{ | |||||
if (reader.TokenType == JsonTokenType.Null) | |||||
return Optional<T>.Unspecified; | |||||
else | |||||
return new Optional<T>(JsonSerializer.Deserialize<T>(ref reader, options)); | |||||
} | |||||
public override void Write(Utf8JsonWriter writer, Optional<T> value, JsonSerializerOptions options) | |||||
{ | |||||
if (!value.IsSpecified) | |||||
writer.WriteNullValue(); | |||||
else | |||||
JsonSerializer.Serialize(writer, value.Value, options); | |||||
} | |||||
} | } | ||||
public override void Write(Utf8JsonWriter writer, Optional<T> value, JsonSerializerOptions options) | |||||
public override bool CanConvert(Type typeToConvert) | |||||
=> typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(Optional<>); | |||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) | |||||
{ | { | ||||
throw new NotImplementedException(); | |||||
var innerType = typeToConvert.GetGenericArguments()[0]; | |||||
var converterType = typeof(OptionalTypeConverter<>).MakeGenericType(innerType); | |||||
return (JsonConverter)Activator.CreateInstance(converterType); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -6,7 +6,57 @@ namespace Discord | |||||
{ | { | ||||
public struct Optional<T> | public struct Optional<T> | ||||
{ | { | ||||
public bool IsSpecified { get; private set; } | |||||
public T Value { get; set; } | |||||
public static Optional<T> Unspecified => default; | |||||
public bool IsSpecified { get; } | |||||
private readonly T _innerValue; | |||||
public T Value | |||||
{ | |||||
get | |||||
{ | |||||
if (!IsSpecified) | |||||
throw new UnspecifiedOptionalException(); | |||||
return _innerValue; | |||||
} | |||||
} | |||||
public Optional(T value) | |||||
{ | |||||
IsSpecified = true; | |||||
_innerValue = value; | |||||
} | |||||
public override string ToString() | |||||
{ | |||||
return $"<Optional IsSpecified={IsSpecified}, Value={(IsSpecified ? Value?.ToString() ?? "null" : "(unspecified)")}>"; | |||||
} | |||||
public override bool Equals(object obj) | |||||
{ | |||||
if (obj is Optional<T> opt) | |||||
{ | |||||
if (IsSpecified && opt.IsSpecified) | |||||
return Value?.Equals(opt.Value) ?? opt.Value == null; | |||||
return IsSpecified == opt.IsSpecified; | |||||
} | |||||
return base.Equals(obj); | |||||
} | |||||
public override int GetHashCode() | |||||
=> IsSpecified ? Value?.GetHashCode() ?? 0 : 0; | |||||
public static bool operator ==(Optional<T> a, Optional<T> b) | |||||
=> a.Equals(b); | |||||
public static bool operator !=(Optional<T> a, Optional<T> b) | |||||
=> !a.Equals(b); | |||||
// todo: implement comparing, GetValueOrDefault, hash codes etc | |||||
} | |||||
public class UnspecifiedOptionalException : Exception | |||||
{ | |||||
public UnspecifiedOptionalException() : base("An attempt was made to access an unspecified optional value") { } | |||||
} | } | ||||
} | } |
@@ -0,0 +1,20 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<PropertyGroup> | |||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
<IsPackable>false</IsPackable> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" /> | |||||
<PackageReference Include="xunit" Version="2.4.0" /> | |||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" /> | |||||
<PackageReference Include="coverlet.collector" Version="1.0.1" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<ProjectReference Include="..\..\src\Discord.Net\Discord.Net.csproj" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -0,0 +1,118 @@ | |||||
using System.Text.Json; | |||||
using System.Text.Json.Serialization; | |||||
using Xunit; | |||||
using Discord.Serialization; | |||||
namespace Discord.Tests.Unit.Serialization | |||||
{ | |||||
public class OptionalConverterTests | |||||
{ | |||||
private readonly JsonSerializerOptions _jsonOptions; | |||||
public OptionalConverterTests() | |||||
{ | |||||
_jsonOptions = new JsonSerializerOptions(); | |||||
_jsonOptions.Converters.Add(new OptionalConverter()); | |||||
} | |||||
public class SampleOptionalClass | |||||
{ | |||||
[JsonPropertyName("optional_number")] | |||||
public Optional<int> OptionalNumber { get; set; } | |||||
[JsonPropertyName("required_number")] | |||||
public int RequiredNumber { get; set; } | |||||
public override bool Equals(object obj) | |||||
=> (obj is SampleOptionalClass other) && (other.OptionalNumber == OptionalNumber && other.RequiredNumber == RequiredNumber); | |||||
public override int GetHashCode() | |||||
=> OptionalNumber.GetHashCode() ^ RequiredNumber.GetHashCode(); | |||||
} | |||||
private string expectedOptionalUnset = "{\"optional_number\":null,\"required_number\":10}"; | |||||
private SampleOptionalClass withOptionalUnset = new SampleOptionalClass | |||||
{ | |||||
OptionalNumber = Optional<int>.Unspecified, | |||||
RequiredNumber = 10, | |||||
}; | |||||
private string expectedOptionalSet = "{\"optional_number\":11,\"required_number\":10}"; | |||||
private SampleOptionalClass withOptionalSet = new SampleOptionalClass | |||||
{ | |||||
OptionalNumber = new Optional<int>(11), | |||||
RequiredNumber = 10, | |||||
}; | |||||
[Fact] | |||||
public void OptionalConverter_Can_Write() | |||||
{ | |||||
// todo: is STJ deterministic in writing order? want to make sure this test doesn't fail because of cosmic rays | |||||
var unsetString = JsonSerializer.Serialize(withOptionalUnset, _jsonOptions); | |||||
Assert.Equal(expectedOptionalUnset, unsetString); | |||||
var setString = JsonSerializer.Serialize(withOptionalSet, _jsonOptions); | |||||
Assert.Equal(expectedOptionalSet, setString); | |||||
} | |||||
[Fact] | |||||
public void OptionalConverter_Can_Read() | |||||
{ | |||||
var unset = JsonSerializer.Deserialize<SampleOptionalClass>(expectedOptionalUnset, _jsonOptions); | |||||
Assert.Equal(withOptionalUnset, unset); | |||||
var set = JsonSerializer.Deserialize<SampleOptionalClass>(expectedOptionalSet, _jsonOptions); | |||||
Assert.Equal(withOptionalSet, set); | |||||
} | |||||
public class NestedPoco | |||||
{ | |||||
[JsonPropertyName("name")] | |||||
public string Name { get; set; } | |||||
[JsonPropertyName("age")] | |||||
public int Age { get; set; } | |||||
public override bool Equals(object obj) | |||||
=> (obj is NestedPoco other) && (Name == other.Name && Age == other.Age); | |||||
public override int GetHashCode() | |||||
=> Name.GetHashCode() ^ Age.GetHashCode(); | |||||
} | |||||
public class NestedSampleClass | |||||
{ | |||||
[JsonPropertyName("nested")] | |||||
public Optional<NestedPoco> Nested { get; set; } | |||||
} | |||||
private string expectedNestedWithUnset = "{\"nested\":null}"; | |||||
private NestedSampleClass nestedWithUnset = new NestedSampleClass | |||||
{ | |||||
Nested = Optional<NestedPoco>.Unspecified | |||||
}; | |||||
private string expectedNestedWithSet = "{\"nested\":{\"name\":\"Ashley\",\"age\":23}}"; | |||||
private NestedSampleClass nestedWithSet = new NestedSampleClass | |||||
{ | |||||
Nested = new Optional<NestedPoco>(new NestedPoco | |||||
{ | |||||
Name = "Ashley", | |||||
Age = 23 | |||||
}), | |||||
}; | |||||
[Fact] | |||||
public void OptionalConverter_Can_Write_Nested_Poco() | |||||
{ | |||||
var unset = JsonSerializer.Serialize(nestedWithUnset, _jsonOptions); | |||||
Assert.Equal(expectedNestedWithUnset, unset); | |||||
var set = JsonSerializer.Serialize(nestedWithSet, _jsonOptions); | |||||
Assert.Equal(expectedNestedWithSet, set); | |||||
} | |||||
[Fact] | |||||
public void OptionalConverter_Can_Read_Nested_Poco() | |||||
{ | |||||
var unset = JsonSerializer.Deserialize<NestedSampleClass>(expectedNestedWithUnset, _jsonOptions); | |||||
Assert.Equal(nestedWithUnset.Nested, unset.Nested); | |||||
var set = JsonSerializer.Deserialize<NestedSampleClass>(expectedNestedWithSet, _jsonOptions); | |||||
Assert.Equal(nestedWithSet.Nested, set.Nested); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,13 @@ | |||||
using Xunit; | |||||
namespace Discord.Tests.Unit | |||||
{ | |||||
public class UnitTest1 | |||||
{ | |||||
[Fact] | |||||
public void Test1() | |||||
{ | |||||
Assert.True(true); | |||||
} | |||||
} | |||||
} |