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