@@ -22,12 +22,13 @@ | |||||
<!-- Package versions for package references across all projects --> | <!-- Package versions for package references across all projects --> | ||||
<ItemGroup> | <ItemGroup> | ||||
<PackageReference Update="coverlet.collector" Version="3.0.3" /> | <PackageReference Update="coverlet.collector" Version="3.0.3" /> | ||||
<PackageReference Update="Microsoft.Net.Compilers.Toolset" Version="3.10.0-3.final" /> | |||||
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="16.10.0" /> | |||||
<PackageReference Update="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" /> | |||||
<PackageReference Update="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" /> | <PackageReference Update="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" /> | ||||
<PackageReference Update="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" /> | <PackageReference Update="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" /> | ||||
<PackageReference Update="Microsoft.Extensions.Hosting" Version="5.0.0" /> | <PackageReference Update="Microsoft.Extensions.Hosting" Version="5.0.0" /> | ||||
<PackageReference Update="Microsoft.Extensions.Options" Version="5.0.0" /> | <PackageReference Update="Microsoft.Extensions.Options" Version="5.0.0" /> | ||||
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="16.10.0" /> | |||||
<PackageReference Update="Microsoft.Net.Compilers.Toolset" Version="3.9.0" /> | |||||
<PackageReference Update="Microsoft.SourceLink.GitHub" Version="1.0.0" /> | <PackageReference Update="Microsoft.SourceLink.GitHub" Version="1.0.0" /> | ||||
<PackageReference Update="System.IO.Pipelines" Version="5.0.1" /> | <PackageReference Update="System.IO.Pipelines" Version="5.0.1" /> | ||||
<PackageReference Update="System.Text.Json" Version="5.0.2" /> | <PackageReference Update="System.Text.Json" Version="5.0.2" /> | ||||
@@ -17,6 +17,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Gateway.UnitTes | |||||
EndProject | EndProject | ||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Models", "src\Models\Discord.Net.Models.csproj", "{564A2E82-CE92-42F6-9D4E-8CC09C5CDF17}" | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Models", "src\Models\Discord.Net.Models.csproj", "{564A2E82-CE92-42F6-9D4E-8CC09C5CDF17}" | ||||
EndProject | EndProject | ||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{80F15CCA-4587-49F9-81FE-73FFC3E131BD}" | |||||
EndProject | |||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SourceGenerators", "SourceGenerators", "{811BBF1D-D37B-415A-969F-2BF354F3082E}" | |||||
EndProject | |||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.SourceGenerators.Serialization", "tools\SourceGenerators\Serialization\Discord.Net.SourceGenerators.Serialization.csproj", "{2B1C884B-F8AC-450B-BAA4-210F717DAA42}" | |||||
EndProject | |||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Serialization", "src\Serialization\Discord.Net.Serialization.csproj", "{1288AE15-BFE7-45E4-9769-9D45FE9DD1D3}" | |||||
EndProject | |||||
Global | Global | ||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
Debug|Any CPU = Debug|Any CPU | Debug|Any CPU = Debug|Any CPU | ||||
@@ -75,6 +83,30 @@ Global | |||||
{564A2E82-CE92-42F6-9D4E-8CC09C5CDF17}.Release|x64.Build.0 = Release|Any CPU | {564A2E82-CE92-42F6-9D4E-8CC09C5CDF17}.Release|x64.Build.0 = Release|Any CPU | ||||
{564A2E82-CE92-42F6-9D4E-8CC09C5CDF17}.Release|x86.ActiveCfg = Release|Any CPU | {564A2E82-CE92-42F6-9D4E-8CC09C5CDF17}.Release|x86.ActiveCfg = Release|Any CPU | ||||
{564A2E82-CE92-42F6-9D4E-8CC09C5CDF17}.Release|x86.Build.0 = Release|Any CPU | {564A2E82-CE92-42F6-9D4E-8CC09C5CDF17}.Release|x86.Build.0 = Release|Any CPU | ||||
{2B1C884B-F8AC-450B-BAA4-210F717DAA42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||||
{2B1C884B-F8AC-450B-BAA4-210F717DAA42}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||||
{2B1C884B-F8AC-450B-BAA4-210F717DAA42}.Debug|x64.ActiveCfg = Debug|Any CPU | |||||
{2B1C884B-F8AC-450B-BAA4-210F717DAA42}.Debug|x64.Build.0 = Debug|Any CPU | |||||
{2B1C884B-F8AC-450B-BAA4-210F717DAA42}.Debug|x86.ActiveCfg = Debug|Any CPU | |||||
{2B1C884B-F8AC-450B-BAA4-210F717DAA42}.Debug|x86.Build.0 = Debug|Any CPU | |||||
{2B1C884B-F8AC-450B-BAA4-210F717DAA42}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||||
{2B1C884B-F8AC-450B-BAA4-210F717DAA42}.Release|Any CPU.Build.0 = Release|Any CPU | |||||
{2B1C884B-F8AC-450B-BAA4-210F717DAA42}.Release|x64.ActiveCfg = Release|Any CPU | |||||
{2B1C884B-F8AC-450B-BAA4-210F717DAA42}.Release|x64.Build.0 = Release|Any CPU | |||||
{2B1C884B-F8AC-450B-BAA4-210F717DAA42}.Release|x86.ActiveCfg = Release|Any CPU | |||||
{2B1C884B-F8AC-450B-BAA4-210F717DAA42}.Release|x86.Build.0 = Release|Any CPU | |||||
{1288AE15-BFE7-45E4-9769-9D45FE9DD1D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||||
{1288AE15-BFE7-45E4-9769-9D45FE9DD1D3}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||||
{1288AE15-BFE7-45E4-9769-9D45FE9DD1D3}.Debug|x64.ActiveCfg = Debug|Any CPU | |||||
{1288AE15-BFE7-45E4-9769-9D45FE9DD1D3}.Debug|x64.Build.0 = Debug|Any CPU | |||||
{1288AE15-BFE7-45E4-9769-9D45FE9DD1D3}.Debug|x86.ActiveCfg = Debug|Any CPU | |||||
{1288AE15-BFE7-45E4-9769-9D45FE9DD1D3}.Debug|x86.Build.0 = Debug|Any CPU | |||||
{1288AE15-BFE7-45E4-9769-9D45FE9DD1D3}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||||
{1288AE15-BFE7-45E4-9769-9D45FE9DD1D3}.Release|Any CPU.Build.0 = Release|Any CPU | |||||
{1288AE15-BFE7-45E4-9769-9D45FE9DD1D3}.Release|x64.ActiveCfg = Release|Any CPU | |||||
{1288AE15-BFE7-45E4-9769-9D45FE9DD1D3}.Release|x64.Build.0 = Release|Any CPU | |||||
{1288AE15-BFE7-45E4-9769-9D45FE9DD1D3}.Release|x86.ActiveCfg = Release|Any CPU | |||||
{1288AE15-BFE7-45E4-9769-9D45FE9DD1D3}.Release|x86.Build.0 = Release|Any CPU | |||||
EndGlobalSection | EndGlobalSection | ||||
GlobalSection(SolutionProperties) = preSolution | GlobalSection(SolutionProperties) = preSolution | ||||
HideSolutionNode = FALSE | HideSolutionNode = FALSE | ||||
@@ -84,6 +116,9 @@ Global | |||||
{54A6E396-5186-4D79-893B-6EFD1CF658CB} = {6D7B7A29-83FE-44F2-85E1-7D44B061EA27} | {54A6E396-5186-4D79-893B-6EFD1CF658CB} = {6D7B7A29-83FE-44F2-85E1-7D44B061EA27} | ||||
{7EC53EB6-6C15-4FD7-9B83-95F96025C14D} = {A47FC28E-1835-46C3-AFD5-7C048A43C157} | {7EC53EB6-6C15-4FD7-9B83-95F96025C14D} = {A47FC28E-1835-46C3-AFD5-7C048A43C157} | ||||
{564A2E82-CE92-42F6-9D4E-8CC09C5CDF17} = {CD5CFA4B-143E-4495-8BFD-AF419226CBE5} | {564A2E82-CE92-42F6-9D4E-8CC09C5CDF17} = {CD5CFA4B-143E-4495-8BFD-AF419226CBE5} | ||||
{811BBF1D-D37B-415A-969F-2BF354F3082E} = {80F15CCA-4587-49F9-81FE-73FFC3E131BD} | |||||
{2B1C884B-F8AC-450B-BAA4-210F717DAA42} = {811BBF1D-D37B-415A-969F-2BF354F3082E} | |||||
{1288AE15-BFE7-45E4-9769-9D45FE9DD1D3} = {CD5CFA4B-143E-4495-8BFD-AF419226CBE5} | |||||
EndGlobalSection | EndGlobalSection | ||||
GlobalSection(ExtensibilityGlobals) = postSolution | GlobalSection(ExtensibilityGlobals) = postSolution | ||||
SolutionGuid = {36B0BFC9-AF79-4D25-89D4-2EE3C961612B} | SolutionGuid = {36B0BFC9-AF79-4D25-89D4-2EE3C961612B} | ||||
@@ -6,7 +6,11 @@ | |||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
<ProjectReference Include="../../src/Gateway/Discord.Net.Gateway.csproj" /> | |||||
<ProjectReference Include="../../src/Models/Discord.Net.Models.csproj" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<ProjectReference Include="../../tools/SourceGenerators/Serialization/Discord.Net.SourceGenerators.Serialization.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> | |||||
</ItemGroup> | </ItemGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
@@ -8,9 +8,13 @@ | |||||
Shared models between the Discord REST API and Gateway. | Shared models between the Discord REST API and Gateway. | ||||
</Description> | </Description> | ||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
<PackageReference Include="System.Text.Json" /> | <PackageReference Include="System.Text.Json" /> | ||||
</ItemGroup> | </ItemGroup> | ||||
<ItemGroup> | |||||
<ProjectReference Include="../Serialization/Discord.Net.Serialization.csproj" /> | |||||
</ItemGroup> | |||||
</Project> | </Project> |
@@ -1,98 +0,0 @@ | |||||
using System; | |||||
namespace Discord.Net | |||||
{ | |||||
/// <summary> | |||||
/// Container to keep a type that might not be present. | |||||
/// </summary> | |||||
/// <typeparam name="T">Inner type</typeparam> | |||||
public struct Optional<T> | |||||
{ | |||||
private readonly T _value; | |||||
/// <summary> | |||||
/// Gets the inner value of this <see cref="Optional{T}"/> if present. | |||||
/// </summary> | |||||
/// <returns>The value inside this <see cref="Optional{T}"/>.</returns> | |||||
/// <exception cref="InvalidOperationException">This <see cref="Optional{T}"/> has no inner value.</exception> | |||||
public T Value => !IsSpecified ? throw new InvalidOperationException("This property has no value set.") : _value; | |||||
/// <summary> | |||||
/// Gets if this <see cref="Optional{T}"/> has an inner value. | |||||
/// </summary> | |||||
/// <returns>A boolean that determines if this <see cref="Optional{T}"/> has a <see cref="Value"/>.</returns> | |||||
public bool IsSpecified { get; } | |||||
private Optional(T value) | |||||
{ | |||||
_value = value; | |||||
IsSpecified = true; | |||||
} | |||||
/// <summary> | |||||
/// Creates a new unspecified <see cref="Optional{T}"/>. | |||||
/// </summary> | |||||
/// <returns>An unspecified <see cref="Optional{T}"/>.</returns> | |||||
public static Optional<T> Create() | |||||
=> default; | |||||
/// <summary> | |||||
/// Creates a new <see cref="Optional{T}"/> with the specified <paramref name="value"/>. | |||||
/// </summary> | |||||
/// <param name="value">Value that will be specified for this <see cref="Optional{T}"/>.</param> | |||||
/// <returns>A specified <see cref="Optional{T}"/> with the provided value inside.</returns> | |||||
public static Optional<T> Create(T value) | |||||
=> new(value); | |||||
/// <summary> | |||||
/// Gets the <see cref="Value"/> or their <see langword="default"/> value. | |||||
/// </summary> | |||||
/// <returns>The value inside this <see cref="Optional{T}"/> or their <see langword="default"/> value.</returns> | |||||
public T GetValueOrDefault() | |||||
=> _value; | |||||
/// <summary> | |||||
/// Gets the <see cref="Value"/> or the default value provided. | |||||
/// </summary> | |||||
/// <returns>The value inside this <see cref="Optional{T}"/> or default value provided.</returns> | |||||
public T GetValueOrDefault(T defaultValue) | |||||
=> IsSpecified ? _value : defaultValue; | |||||
/// <inheritdoc/> | |||||
public override bool Equals(object? other) | |||||
{ | |||||
if (!IsSpecified) | |||||
return other == null; | |||||
if (other == null || _value == null) | |||||
return false; | |||||
return _value.Equals(other); | |||||
} | |||||
/// <inheritdoc/> | |||||
public override int GetHashCode() | |||||
=> IsSpecified ? _value?.GetHashCode() ?? default : default; | |||||
/// <summary> | |||||
/// Returns the inner value ToString value or this type fully qualified name. | |||||
/// </summary> | |||||
/// <returns>The inner value string value or this type fully qualified name.</returns> | |||||
public override string? ToString() | |||||
=> IsSpecified ? _value?.ToString() : default; | |||||
/// <summary> | |||||
/// Creates a new <see cref="Optional{T}"/> with the specified <paramref name="value"/>. | |||||
/// </summary> | |||||
/// <param name="value">Value to convert</param> | |||||
/// <returns>A new <see cref="Optional{T}"/> with the specified <paramref name="value"/></returns> | |||||
public static implicit operator Optional<T>(T value) | |||||
=> new(value); | |||||
/// <summary> | |||||
/// Gets the inner value. | |||||
/// </summary> | |||||
/// <param name="value">Value to convert</param> | |||||
/// <returns>The inner value</returns> | |||||
public static explicit operator T(Optional<T> value) | |||||
=> value.Value; | |||||
} | |||||
} |
@@ -0,0 +1,45 @@ | |||||
using System; | |||||
using System.Text.Json; | |||||
using System.Text.Json.Serialization; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Net.Serialization.Converters | |||||
{ | |||||
/// <summary> | |||||
/// Defines a converter which can be used to convert instances of | |||||
/// <see cref="Optional{T}"/>. | |||||
/// </summary> | |||||
public sealed class OptionalConverter<T> : JsonConverter<T> | |||||
{ | |||||
private readonly JsonConverter<T>? _valueConverter; | |||||
internal OptionalConverter( | |||||
JsonSerializerOptions options) | |||||
{ | |||||
_valueConverter = options.GetConverter(typeof(T)) | |||||
as JsonConverter<T>; | |||||
} | |||||
/// <inheritdoc/> | |||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, | |||||
JsonSerializerOptions options) | |||||
{ | |||||
return _valueConverter != null | |||||
? _valueConverter.Read(ref reader, typeof(T), options) | |||||
: JsonSerializer.Deserialize<T>(ref reader, options); | |||||
} | |||||
/// <inheritdoc/> | |||||
public override void Write(Utf8JsonWriter writer, T value, | |||||
JsonSerializerOptions options) | |||||
{ | |||||
if (_valueConverter != null) | |||||
{ | |||||
_valueConverter.Write(writer, value, options); | |||||
return; | |||||
} | |||||
JsonSerializer.Serialize(writer, value, options); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,36 @@ | |||||
using System; | |||||
using System.Diagnostics; | |||||
using System.Linq; | |||||
using System.Text.Json; | |||||
using System.Text.Json.Serialization; | |||||
namespace Discord.Net.Serialization.Converters | |||||
{ | |||||
/// <summary> | |||||
/// Defines a converter factory which can be used to create instances of | |||||
/// <see cref="OptionalConverter{T}"/>. | |||||
/// </summary> | |||||
public sealed class OptionalConverterFactory : JsonConverterFactory | |||||
{ | |||||
private static readonly Type OptionalType = typeof(Optional<>); | |||||
private static readonly Type OptionalConverterType = typeof(OptionalConverter<>); | |||||
/// <inheritdoc/> | |||||
public override bool CanConvert(Type typeToConvert) | |||||
=> typeToConvert.IsGenericType | |||||
&& typeToConvert.GetGenericTypeDefinition() == OptionalType; | |||||
/// <inheritdoc/> | |||||
public override JsonConverter? CreateConverter(Type typeToConvert, | |||||
JsonSerializerOptions options) | |||||
{ | |||||
Debug.Assert(typeToConvert.IsGenericType); | |||||
var underlyingType = typeToConvert.GenericTypeArguments[0]; | |||||
return (JsonConverter)Activator.CreateInstance( | |||||
OptionalConverterType.MakeGenericType(underlyingType), | |||||
args: new[] { options })!; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,12 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<PropertyGroup> | |||||
<TargetFramework>net5.0</TargetFramework> | |||||
<Description> | |||||
$(Description) | |||||
Serialization primitives used by Discord.Net | |||||
</Description> | |||||
</PropertyGroup> | |||||
</Project> |
@@ -0,0 +1,26 @@ | |||||
using System; | |||||
namespace Discord.Net.Serialization | |||||
{ | |||||
/// <summary> | |||||
/// Defines an attribute used to mark discriminated unions. | |||||
/// </summary> | |||||
public class DiscriminatedUnionAttribute : Attribute | |||||
{ | |||||
/// <summary> | |||||
/// Gets the field or property used to discriminate between types. | |||||
/// </summary> | |||||
public string DiscriminatorField { get; } | |||||
/// <summary> | |||||
/// Creates a new <see cref="DiscriminatedUnionAttribute"/> instance. | |||||
/// </summary> | |||||
/// <param name="discriminatorField"> | |||||
/// The field or property used to discriminate between types. | |||||
/// </param> | |||||
public DiscriminatedUnionAttribute(string discriminatorField) | |||||
{ | |||||
DiscriminatorField = discriminatorField; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,27 @@ | |||||
using System; | |||||
namespace Discord.Net.Serialization | |||||
{ | |||||
/// <summary> | |||||
/// Defines an attribute used to mark members of discriminated unions. | |||||
/// </summary> | |||||
public class DiscriminatedUnionMemberAttribute : Attribute | |||||
{ | |||||
/// <summary> | |||||
/// Gets the discriminator value used to identify this member type. | |||||
/// </summary> | |||||
public string Discriminator { get; } | |||||
/// <summary> | |||||
/// Creates a new <see cref="DiscriminatedUnionMemberAttribute"/> | |||||
/// instance. | |||||
/// </summary> | |||||
/// <param name="discriminator"> | |||||
/// The discriminator value used to identify this member type. | |||||
/// </param> | |||||
public DiscriminatedUnionMemberAttribute(string discriminator) | |||||
{ | |||||
Discriminator = discriminator; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,171 @@ | |||||
using System; | |||||
using Discord.Net.Serialization; | |||||
namespace Discord.Net.Serialization | |||||
{ | |||||
/// <summary> | |||||
/// Defines a type which may be either undefined, null or an instance of a | |||||
/// value. | |||||
/// </summary> | |||||
/// <typeparam name="T"> | |||||
/// The type which is contained | |||||
/// </typeparam> | |||||
public struct Optional<T> | |||||
{ | |||||
private readonly T _value; | |||||
/// <summary> | |||||
/// Gets the inner value of this <see cref="Optional{T}"/> if present. | |||||
/// </summary> | |||||
/// <returns> | |||||
/// The value inside this <see cref="Optional{T}"/>. | |||||
/// </returns> | |||||
/// <exception cref="InvalidOperationException"> | |||||
/// This <see cref="Optional{T}"/> has no inner value. | |||||
/// </exception> | |||||
public T Value | |||||
=> !IsSpecified | |||||
? throw new InvalidOperationException( | |||||
"This property has no value set.") | |||||
: _value; | |||||
/// <summary> | |||||
/// Gets if this <see cref="Optional{T}"/> has an inner value. | |||||
/// </summary> | |||||
/// <returns> | |||||
/// A boolean that determines if this <see cref="Optional{T}"/> has a | |||||
/// <see cref="Value"/>. | |||||
/// </returns> | |||||
public bool IsSpecified { get; } | |||||
private Optional(T value) | |||||
{ | |||||
_value = value; | |||||
IsSpecified = true; | |||||
} | |||||
/// <summary> | |||||
/// Creates a new unspecified <see cref="Optional{T}"/>. | |||||
/// </summary> | |||||
/// <returns> | |||||
/// An unspecified <see cref="Optional{T}"/>. | |||||
/// </returns> | |||||
public static Optional<T> Create() | |||||
=> default; | |||||
/// <summary> | |||||
/// Creates a new <see cref="Optional{T}"/> with the specified | |||||
/// <paramref name="value"/>. | |||||
/// </summary> | |||||
/// <param name="value"> | |||||
/// Value that will be specified for this <see cref="Optional{T}"/>. | |||||
/// </param> | |||||
/// <returns> | |||||
/// A specified <see cref="Optional{T}"/> with the provided value | |||||
/// inside. | |||||
/// </returns> | |||||
public static Optional<T> Create(T value) | |||||
=> new(value); | |||||
/// <summary> | |||||
/// Gets the <see cref="Value"/> or their <see langword="default"/> | |||||
/// value. | |||||
/// </summary> | |||||
/// <returns> | |||||
/// The value inside this <see cref="Optional{T}"/> or their | |||||
/// <see langword="default"/> value. | |||||
/// </returns> | |||||
public T GetValueOrDefault() | |||||
=> _value; | |||||
/// <summary> | |||||
/// Gets the <see cref="Value"/> or the default value provided. | |||||
/// </summary> | |||||
/// <returns> | |||||
/// The value inside this <see cref="Optional{T}"/> or default value | |||||
/// provided. | |||||
/// </returns> | |||||
public T GetValueOrDefault(T defaultValue) | |||||
=> IsSpecified ? _value : defaultValue; | |||||
/// <inheritdoc/> | |||||
public override bool Equals(object? other) | |||||
{ | |||||
if (!IsSpecified) | |||||
return other == null; | |||||
if (other == null || _value == null) | |||||
return false; | |||||
return _value.Equals(other); | |||||
} | |||||
/// <inheritdoc/> | |||||
public override int GetHashCode() | |||||
=> IsSpecified ? _value?.GetHashCode() ?? default : default; | |||||
/// <summary> | |||||
/// Returns the inner value ToString value or this type fully qualified | |||||
/// name. | |||||
/// </summary> | |||||
/// <returns> | |||||
/// The inner value string value or this type fully qualified name. | |||||
/// </returns> | |||||
public override string? ToString() | |||||
=> IsSpecified ? _value?.ToString() : default; | |||||
/// <summary> | |||||
/// Creates a new <see cref="Optional{T}"/> with the specified | |||||
/// <paramref name="value"/>. | |||||
/// </summary> | |||||
/// <param name="value">Value to convert</param> | |||||
/// <returns> | |||||
/// A new <see cref="Optional{T}"/> with the specified | |||||
/// <paramref name="value"/>. | |||||
/// </returns> | |||||
public static implicit operator Optional<T>(T value) | |||||
=> new(value); | |||||
/// <summary> | |||||
/// Gets the inner value. | |||||
/// </summary> | |||||
/// <param name="value"> | |||||
/// Value to convert | |||||
/// </param> | |||||
/// <returns> | |||||
/// The inner value. | |||||
/// </returns> | |||||
public static explicit operator T(Optional<T> value) | |||||
=> value.Value; | |||||
/// <summary> | |||||
/// Compares two <see cref="Optional{T}"/> values for equality. | |||||
/// </summary> | |||||
/// <param name="left"> | |||||
/// The first value to compare. | |||||
/// </param> | |||||
/// <param name="right"> | |||||
/// The second value to compare. | |||||
/// </param> | |||||
/// <returns> | |||||
/// <see langword="true"/> if the two values are equal, or | |||||
/// <see langword="false"/> otherwise. | |||||
/// </returns> | |||||
public static bool operator ==(Optional<T> left, Optional<T> right) | |||||
=> left.Equals(right); | |||||
/// <summary> | |||||
/// Compares two <see cref="Optional{T}"/> values for inequality. | |||||
/// </summary> | |||||
/// <param name="left"> | |||||
/// The first value to compare. | |||||
/// </param> | |||||
/// <param name="right"> | |||||
/// The second value to compare. | |||||
/// </param> | |||||
/// <returns> | |||||
/// <see langword="true"/> if the two values are unequal, or | |||||
/// <see langword="false"/> otherwise. | |||||
/// </returns> | |||||
public static bool operator !=(Optional<T> left, Optional<T> right) | |||||
=> !(left == right); | |||||
} | |||||
} |
@@ -0,0 +1,32 @@ | |||||
<?xml version="1.0" encoding="utf-8"?> | |||||
<!-- Based on https://github.com/terrafx/terrafx/blob/master/Directory.Build.props --> | |||||
<!-- Copyright © Tanner Gooding and Contributors --> | |||||
<Project> | |||||
<!-- | |||||
Directory.Build.props is automatically picked up and imported by | |||||
Microsoft.Common.props. This file needs to exist, even if empty so that | |||||
files in the parent directory tree, with the same name, are not imported | |||||
instead. The import fairly early and only Sdk.props will have been | |||||
imported beforehand. We also don't need to add ourselves to | |||||
MSBuildAllProjects, as that is done by the file that imports us. | |||||
--> | |||||
<PropertyGroup> | |||||
<EmbedUntrackedSources>true</EmbedUntrackedSources> | |||||
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileDirectory)..\Directory.Build.props</MSBuildAllProjects> | |||||
<DiscordNetProjectCategory>tools</DiscordNetProjectCategory> | |||||
</PropertyGroup> | |||||
<Import Project="$(MSBuildThisFileDirectory)..\Directory.Build.props" /> | |||||
<ItemGroup> | |||||
<InternalsVisibleTo Include="$(MSBuildProjectName).UnitTests" PublicKey="$(AssemblyOriginatorPublicKey)" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="Microsoft.SourceLink.GitHub" IsImplicitlyDefined="true" PrivateAssets="all" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -0,0 +1,25 @@ | |||||
<?xml version="1.0" encoding="utf-8"?> | |||||
<!-- Based on https://github.com/terrafx/terrafx/blob/master/Directory.Build.props --> | |||||
<!-- Copyright © Tanner Gooding and Contributors --> | |||||
<Project> | |||||
<!-- | |||||
Directory.Build.targets is automatically picked up and imported by | |||||
Microsoft.Common.targets. This file needs to exist, even if empty so that | |||||
files in the parent directory tree, with the same name, are not imported | |||||
instead. The import fairly late and most other props/targets will have | |||||
been imported beforehand. We also don't need to add ourselves to | |||||
MSBuildAllProjects, as that is done by the file that imports us. | |||||
--> | |||||
<PropertyGroup> | |||||
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileDirectory)..\Directory.Build.targets</MSBuildAllProjects> | |||||
</PropertyGroup> | |||||
<Import Project="$(MSBuildThisFileDirectory)..\Directory.Build.targets" /> | |||||
<!-- Empty target so that `dotnet test` will work on the solution --> | |||||
<!-- https://github.com/Microsoft/vstest/issues/411 --> | |||||
<Target Name="VSTest" /> | |||||
</Project> |
@@ -0,0 +1,36 @@ | |||||
<?xml version="1.0" encoding="utf-8"?> | |||||
<!-- Based on https://github.com/terrafx/terrafx/blob/master/Directory.Build.props --> | |||||
<!-- Copyright © Tanner Gooding and Contributors --> | |||||
<Project> | |||||
<!-- | |||||
Directory.Build.props is automatically picked up and imported by | |||||
Microsoft.Common.props. This file needs to exist, even if empty so that | |||||
files in the parent directory tree, with the same name, are not imported | |||||
instead. The import fairly early and only Sdk.props will have been | |||||
imported beforehand. We also don't need to add ourselves to | |||||
MSBuildAllProjects, as that is done by the file that imports us. | |||||
--> | |||||
<PropertyGroup> | |||||
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileDirectory)..\Directory.Build.props</MSBuildAllProjects> | |||||
</PropertyGroup> | |||||
<Import Project="$(MSBuildThisFileDirectory)..\Directory.Build.props" /> | |||||
<PropertyGroup> | |||||
<GenerateDocumentationFile>false</GenerateDocumentationFile> | |||||
<NoPackageAnalysis>true</NoPackageAnalysis> | |||||
<!-- Disable release tracking analyzers due to weird behaviour with OmniSharp --> | |||||
<NoWarn>$(NoWarn);RS2000;RS2001;RS2002;RS2003;RS2004;RS2005;RS2006;RS2007;RS2008</NoWarn> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" IsImplicitlyDefined="true" PrivateAssets="all" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<None Include="$(OutputPath)$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -0,0 +1,7 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<PropertyGroup> | |||||
<TargetFramework>netstandard2.0</TargetFramework> | |||||
</PropertyGroup> | |||||
</Project> |
@@ -0,0 +1,37 @@ | |||||
using Microsoft.CodeAnalysis; | |||||
namespace Discord.Net.SourceGenerators.Serialization | |||||
{ | |||||
public partial class SerializationSourceGenerator | |||||
{ | |||||
private static string GenerateConverter(INamedTypeSymbol @class) | |||||
{ | |||||
return $@" | |||||
using System; | |||||
using System.Text.Json; | |||||
using System.Text.Json.Serialization; | |||||
namespace Discord.Net.Serialization.Converters | |||||
{{ | |||||
public class {@class.Name}Converter : JsonConverter<{@class.ToDisplayString()}> | |||||
{{ | |||||
public override {@class.ToDisplayString()} Read( | |||||
ref Utf8JsonReader reader, | |||||
Type typeToConvert, | |||||
JsonSerializerOptions options) | |||||
{{ | |||||
return default; | |||||
}} | |||||
public override void Write( | |||||
Utf8JsonWriter writer, | |||||
{@class.ToDisplayString()} value, | |||||
JsonSerializerOptions options) | |||||
{{ | |||||
writer.WriteNull(); | |||||
}} | |||||
}} | |||||
}}"; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,67 @@ | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
namespace Discord.Net.SourceGenerators.Serialization | |||||
{ | |||||
public partial class SerializationSourceGenerator | |||||
{ | |||||
private static string GenerateSerializerOptionsTemplateSourceCode() | |||||
{ | |||||
return @" | |||||
using System; | |||||
using System.Text.Json; | |||||
namespace Discord.Net.Serialization | |||||
{ | |||||
/// <summary> | |||||
/// Defines extension methods for adding Discord.Net JSON converters to a | |||||
/// <see cref=""JsonSerializerOptions""/> instance. | |||||
/// </summary> | |||||
public static partial class JsonSerializerOptionsExtensions | |||||
{ | |||||
/// <summary> | |||||
/// Adds Discord.Net JSON converters to the passed | |||||
/// <see cref=""JsonSerializerOptions""/>. | |||||
/// </summary> | |||||
/// <param name=""options""> | |||||
/// The serializer options to add Discord.Net converters to. | |||||
/// </param> | |||||
/// <returns> | |||||
/// The modified <see cref=""JsonSerializerOptions""/>, so this method | |||||
/// can be chained. | |||||
/// </returns> | |||||
public static partial JsonSerializerOptions WithDiscordNetConverters( | |||||
this JsonSerializerOptions options); | |||||
} | |||||
}"; | |||||
} | |||||
private static string GenerateSerializerOptionsSourceCode( | |||||
List<string> converters) | |||||
{ | |||||
var snippets = string.Join("\n", | |||||
converters.Select( | |||||
x => $"options.Converters.Add(new {x}());")); | |||||
return $@" | |||||
using System; | |||||
using System.Text.Json; | |||||
using Discord.Net.Serialization.Converters; | |||||
namespace Discord.Net.Serialization | |||||
{{ | |||||
public static partial class JsonSerializerOptionsExtensions | |||||
{{ | |||||
public static partial JsonSerializerOptions WithDiscordNetConverters( | |||||
this JsonSerializerOptions options) | |||||
{{ | |||||
options.Converters.Add(new OptionalConverterFactory()); | |||||
{snippets} | |||||
return options; | |||||
}} | |||||
}} | |||||
}}"; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,108 @@ | |||||
using System; | |||||
using System.Collections; | |||||
using System.Collections.Generic; | |||||
using System.Diagnostics; | |||||
using System.Linq; | |||||
using System.Reflection; | |||||
using Microsoft.CodeAnalysis; | |||||
using Microsoft.CodeAnalysis.CSharp.Syntax; | |||||
namespace Discord.Net.SourceGenerators.Serialization | |||||
{ | |||||
[Generator] | |||||
public partial class SerializationSourceGenerator : ISourceGenerator | |||||
{ | |||||
public void Execute(GeneratorExecutionContext context) | |||||
{ | |||||
var receiver = (SyntaxReceiver)context.SyntaxContextReceiver!; | |||||
var converters = new List<string>(); | |||||
foreach (var @class in receiver.Classes) | |||||
{ | |||||
var semanticModel = context.Compilation.GetSemanticModel( | |||||
@class.SyntaxTree); | |||||
if (semanticModel.GetDeclaredSymbol(@class) is | |||||
not INamedTypeSymbol classSymbol) | |||||
throw new InvalidOperationException( | |||||
"Could not find named type symbol for " + | |||||
$"{@class.Identifier}"); | |||||
context.AddSource( | |||||
$"Converters.{classSymbol.Name}", | |||||
GenerateConverter(classSymbol)); | |||||
converters.Add($"{classSymbol.Name}Converter"); | |||||
} | |||||
context.AddSource("SerializerOptions.Complete", | |||||
GenerateSerializerOptionsSourceCode(converters)); | |||||
} | |||||
public void Initialize(GeneratorInitializationContext context) | |||||
{ | |||||
context.RegisterForPostInitialization(PostInitialize); | |||||
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); | |||||
} | |||||
public static void PostInitialize( | |||||
GeneratorPostInitializationContext context) | |||||
=> context.AddSource("SerializerOptions.Template", | |||||
GenerateSerializerOptionsTemplateSourceCode()); | |||||
internal class SyntaxReceiver : ISyntaxContextReceiver | |||||
{ | |||||
public List<ClassDeclarationSyntax> Classes { get; } = new(); | |||||
private readonly Dictionary<string, INamedTypeSymbol> _interestingAttributes | |||||
= new(); | |||||
public void OnVisitSyntaxNode(GeneratorSyntaxContext context) | |||||
{ | |||||
_ = GetOrAddAttribute(_interestingAttributes, | |||||
context.SemanticModel, | |||||
"Discord.Net.Serialization.DiscriminatedUnionAttribute"); | |||||
_ = GetOrAddAttribute(_interestingAttributes, | |||||
context.SemanticModel, | |||||
"Discord.Net.Serialization.DiscriminatedUnionMemberAttribute"); | |||||
if (context.Node is ClassDeclarationSyntax classDecl | |||||
&& classDecl.AttributeLists is | |||||
SyntaxList<AttributeListSyntax> attrList | |||||
&& attrList.Any( | |||||
list => list.Attributes | |||||
.Any(a => IsInterestingAttribute(a, | |||||
context.SemanticModel, | |||||
_interestingAttributes.Values)))) | |||||
{ | |||||
Classes.Add(classDecl); | |||||
} | |||||
} | |||||
private static INamedTypeSymbol GetOrAddAttribute( | |||||
Dictionary<string, INamedTypeSymbol> cache, | |||||
SemanticModel model, string name) | |||||
{ | |||||
if (!cache.TryGetValue(name, out var type)) | |||||
{ | |||||
type = model.Compilation.GetTypeByMetadataName(name); | |||||
Debug.Assert(type != null); | |||||
cache.Add(name, type!); | |||||
} | |||||
return type!; | |||||
} | |||||
private static bool IsInterestingAttribute( | |||||
AttributeSyntax attribute, SemanticModel model, | |||||
IEnumerable<INamedTypeSymbol> interestingAttributes) | |||||
{ | |||||
var typeInfo = model.GetTypeInfo(attribute.Name); | |||||
return interestingAttributes.Any( | |||||
x => SymbolEqualityComparer.Default | |||||
.Equals(typeInfo.Type, x)); | |||||
} | |||||
} | |||||
} | |||||
} |