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