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 | |||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "idn", "sample\idn\idn.csproj", "{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}" | |||
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 | |||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | |||
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|x86.ActiveCfg = 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 | |||
GlobalSection(NestedProjects) = preSolution | |||
{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370} = {5DAC796B-0B77-4F84-B790-83DB78C6DFFE} | |||
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A} = {4795640A-030C-4A9A-A9B0-20C56AF4DA3F} | |||
{6AD4FF67-D45E-4E7E-8853-990390D35C9F} = {68EE1EAC-F487-4BAC-917B-233370B3AEA1} | |||
EndGlobalSection | |||
EndGlobal |
@@ -1,10 +1,10 @@ | |||
using System.Text.Json; | |||
using System.Threading.Tasks; | |||
using Refit; | |||
using Discord.Models; | |||
using System.Net.Http.Headers; | |||
using System; | |||
using System.Net.Http; | |||
using Refit; | |||
using Discord.Models; | |||
using Discord.Serialization; | |||
// This is essentially a reimplementation of Wumpus.Net.Rest | |||
namespace Discord.Rest | |||
@@ -26,6 +26,7 @@ namespace Discord.Rest | |||
}; | |||
var jsonOptions = new JsonSerializerOptions(); | |||
jsonOptions.Converters.Add(new OptionalConverter()); | |||
var refitSettings = new RefitSettings | |||
{ | |||
ContentSerializer = new JsonContentSerializer(jsonOptions), | |||
@@ -1,22 +1,41 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
using System.Text.Json; | |||
using System.Text.Json.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 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); | |||
} | |||
} | |||
} |