Browse Source

net: add barebones optional converter

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
Christopher Felegy 5 years ago
parent
commit
07f74b70b4
7 changed files with 251 additions and 13 deletions
  1. +17
    -0
      Discord.Net.sln
  2. +4
    -3
      src/Discord.Net/Rest/DiscordRestApi.cs
  3. +27
    -8
      src/Discord.Net/Serialization/OptionalConverter.cs
  4. +52
    -2
      src/Discord.Net/Utilities/Optional.cs
  5. +20
    -0
      test/Discord.Tests.Unit/Discord.Tests.Unit.csproj
  6. +118
    -0
      test/Discord.Tests.Unit/Serialization/OptionalConverterTests.cs
  7. +13
    -0
      test/Discord.Tests.Unit/UnitTest1.cs

+ 17
- 0
Discord.Net.sln View File

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

+ 4
- 3
src/Discord.Net/Rest/DiscordRestApi.cs View File

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


+ 27
- 8
src/Discord.Net/Serialization/OptionalConverter.cs View File

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

+ 52
- 2
src/Discord.Net/Utilities/Optional.cs View File

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

+ 20
- 0
test/Discord.Tests.Unit/Discord.Tests.Unit.csproj View File

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

+ 118
- 0
test/Discord.Tests.Unit/Serialization/OptionalConverterTests.cs View File

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

+ 13
- 0
test/Discord.Tests.Unit/UnitTest1.cs View File

@@ -0,0 +1,13 @@
using Xunit;

namespace Discord.Tests.Unit
{
public class UnitTest1
{
[Fact]
public void Test1()
{
Assert.True(true);
}
}
}

Loading…
Cancel
Save