@@ -22,12 +22,13 @@ | |||
<!-- Package versions for package references across all projects --> | |||
<ItemGroup> | |||
<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.Logging.Abstractions" 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.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="System.IO.Pipelines" Version="5.0.1" /> | |||
<PackageReference Update="System.Text.Json" Version="5.0.2" /> | |||
@@ -17,6 +17,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Gateway.UnitTes | |||
EndProject | |||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Models", "src\Models\Discord.Net.Models.csproj", "{564A2E82-CE92-42F6-9D4E-8CC09C5CDF17}" | |||
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 | |||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | |||
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|x86.ActiveCfg = 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 | |||
GlobalSection(SolutionProperties) = preSolution | |||
HideSolutionNode = FALSE | |||
@@ -84,6 +116,9 @@ Global | |||
{54A6E396-5186-4D79-893B-6EFD1CF658CB} = {6D7B7A29-83FE-44F2-85E1-7D44B061EA27} | |||
{7EC53EB6-6C15-4FD7-9B83-95F96025C14D} = {A47FC28E-1835-46C3-AFD5-7C048A43C157} | |||
{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 | |||
GlobalSection(ExtensibilityGlobals) = postSolution | |||
SolutionGuid = {36B0BFC9-AF79-4D25-89D4-2EE3C961612B} | |||
@@ -6,7 +6,11 @@ | |||
</PropertyGroup> | |||
<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> | |||
@@ -8,9 +8,13 @@ | |||
Shared models between the Discord REST API and Gateway. | |||
</Description> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<PackageReference Include="System.Text.Json" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<ProjectReference Include="../Serialization/Discord.Net.Serialization.csproj" /> | |||
</ItemGroup> | |||
</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)); | |||
} | |||
} | |||
} | |||
} |