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