From f1bff0e3b581bdc35807d5d85ceeabbd7a02a123 Mon Sep 17 00:00:00 2001 From: FiniteReality Date: Fri, 4 Jun 2021 07:48:52 +0100 Subject: [PATCH] Add Serialization subproject --- Directory.Build.targets | 5 +- Discord.Net.sln | 35 +++++ samples/PingPong/PingPong.csproj | 6 +- src/Models/Discord.Net.Models.csproj | 6 +- src/Models/Optional.cs | 98 ------------ src/Serialization/Converters/OptionalConverter.cs | 45 ++++++ .../Converters/OptionalConverterFactory.cs | 36 +++++ src/Serialization/Discord.Net.Serialization.csproj | 12 ++ src/Serialization/DiscriminatedUnionAttribute.cs | 26 ++++ .../DiscriminatedUnionMemberAttribute.cs | 27 ++++ src/Serialization/Optional.cs | 171 +++++++++++++++++++++ tools/Directory.Build.props | 32 ++++ tools/Directory.Build.targets | 25 +++ tools/SourceGenerators/Directory.Build.props | 36 +++++ ...scord.Net.SourceGenerators.Serialization.csproj | 7 + ...SerializationSourceGenerator.ConverterSource.cs | 37 +++++ .../SerializationSourceGenerator.OptionsSource.cs | 67 ++++++++ .../Serialization/SerializationSourceGenerator.cs | 108 +++++++++++++ 18 files changed, 677 insertions(+), 102 deletions(-) delete mode 100644 src/Models/Optional.cs create mode 100644 src/Serialization/Converters/OptionalConverter.cs create mode 100644 src/Serialization/Converters/OptionalConverterFactory.cs create mode 100644 src/Serialization/Discord.Net.Serialization.csproj create mode 100644 src/Serialization/DiscriminatedUnionAttribute.cs create mode 100644 src/Serialization/DiscriminatedUnionMemberAttribute.cs create mode 100644 src/Serialization/Optional.cs create mode 100644 tools/Directory.Build.props create mode 100644 tools/Directory.Build.targets create mode 100644 tools/SourceGenerators/Directory.Build.props create mode 100644 tools/SourceGenerators/Serialization/Discord.Net.SourceGenerators.Serialization.csproj create mode 100644 tools/SourceGenerators/Serialization/SerializationSourceGenerator.ConverterSource.cs create mode 100644 tools/SourceGenerators/Serialization/SerializationSourceGenerator.OptionsSource.cs create mode 100644 tools/SourceGenerators/Serialization/SerializationSourceGenerator.cs diff --git a/Directory.Build.targets b/Directory.Build.targets index 50fad7775..c0bca72b4 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -22,12 +22,13 @@ - - + + + diff --git a/Discord.Net.sln b/Discord.Net.sln index 8635159e4..2f4c96a9a 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -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} diff --git a/samples/PingPong/PingPong.csproj b/samples/PingPong/PingPong.csproj index 3a0beba95..51f903439 100644 --- a/samples/PingPong/PingPong.csproj +++ b/samples/PingPong/PingPong.csproj @@ -6,7 +6,11 @@ - + + + + + diff --git a/src/Models/Discord.Net.Models.csproj b/src/Models/Discord.Net.Models.csproj index b01c93952..80f81f32c 100644 --- a/src/Models/Discord.Net.Models.csproj +++ b/src/Models/Discord.Net.Models.csproj @@ -8,9 +8,13 @@ Shared models between the Discord REST API and Gateway. - + + + + + diff --git a/src/Models/Optional.cs b/src/Models/Optional.cs deleted file mode 100644 index ba31cb061..000000000 --- a/src/Models/Optional.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; - -namespace Discord.Net -{ - /// - /// Container to keep a type that might not be present. - /// - /// Inner type - public struct Optional - { - private readonly T _value; - - /// - /// Gets the inner value of this if present. - /// - /// The value inside this . - /// This has no inner value. - public T Value => !IsSpecified ? throw new InvalidOperationException("This property has no value set.") : _value; - - /// - /// Gets if this has an inner value. - /// - /// A boolean that determines if this has a . - public bool IsSpecified { get; } - - private Optional(T value) - { - _value = value; - IsSpecified = true; - } - - /// - /// Creates a new unspecified . - /// - /// An unspecified . - public static Optional Create() - => default; - - /// - /// Creates a new with the specified . - /// - /// Value that will be specified for this . - /// A specified with the provided value inside. - public static Optional Create(T value) - => new(value); - - /// - /// Gets the or their value. - /// - /// The value inside this or their value. - public T GetValueOrDefault() - => _value; - - /// - /// Gets the or the default value provided. - /// - /// The value inside this or default value provided. - public T GetValueOrDefault(T defaultValue) - => IsSpecified ? _value : defaultValue; - - /// - public override bool Equals(object? other) - { - if (!IsSpecified) - return other == null; - if (other == null || _value == null) - return false; - return _value.Equals(other); - } - - /// - public override int GetHashCode() - => IsSpecified ? _value?.GetHashCode() ?? default : default; - - /// - /// Returns the inner value ToString value or this type fully qualified name. - /// - /// The inner value string value or this type fully qualified name. - public override string? ToString() - => IsSpecified ? _value?.ToString() : default; - - /// - /// Creates a new with the specified . - /// - /// Value to convert - /// A new with the specified - public static implicit operator Optional(T value) - => new(value); - - /// - /// Gets the inner value. - /// - /// Value to convert - /// The inner value - public static explicit operator T(Optional value) - => value.Value; - } -} diff --git a/src/Serialization/Converters/OptionalConverter.cs b/src/Serialization/Converters/OptionalConverter.cs new file mode 100644 index 000000000..78a90a850 --- /dev/null +++ b/src/Serialization/Converters/OptionalConverter.cs @@ -0,0 +1,45 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Discord.Net.Serialization.Converters +{ + /// + /// Defines a converter which can be used to convert instances of + /// . + /// + public sealed class OptionalConverter : JsonConverter + { + private readonly JsonConverter? _valueConverter; + + internal OptionalConverter( + JsonSerializerOptions options) + { + _valueConverter = options.GetConverter(typeof(T)) + as JsonConverter; + } + + /// + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) + { + return _valueConverter != null + ? _valueConverter.Read(ref reader, typeof(T), options) + : JsonSerializer.Deserialize(ref reader, options); + } + + /// + public override void Write(Utf8JsonWriter writer, T value, + JsonSerializerOptions options) + { + if (_valueConverter != null) + { + _valueConverter.Write(writer, value, options); + return; + } + + JsonSerializer.Serialize(writer, value, options); + } + } +} diff --git a/src/Serialization/Converters/OptionalConverterFactory.cs b/src/Serialization/Converters/OptionalConverterFactory.cs new file mode 100644 index 000000000..463c0d61e --- /dev/null +++ b/src/Serialization/Converters/OptionalConverterFactory.cs @@ -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 +{ + /// + /// Defines a converter factory which can be used to create instances of + /// . + /// + public sealed class OptionalConverterFactory : JsonConverterFactory + { + private static readonly Type OptionalType = typeof(Optional<>); + private static readonly Type OptionalConverterType = typeof(OptionalConverter<>); + + /// + public override bool CanConvert(Type typeToConvert) + => typeToConvert.IsGenericType + && typeToConvert.GetGenericTypeDefinition() == OptionalType; + + /// + 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 })!; + } + } +} diff --git a/src/Serialization/Discord.Net.Serialization.csproj b/src/Serialization/Discord.Net.Serialization.csproj new file mode 100644 index 000000000..f35880ec0 --- /dev/null +++ b/src/Serialization/Discord.Net.Serialization.csproj @@ -0,0 +1,12 @@ + + + + net5.0 + + + $(Description) + Serialization primitives used by Discord.Net + + + + diff --git a/src/Serialization/DiscriminatedUnionAttribute.cs b/src/Serialization/DiscriminatedUnionAttribute.cs new file mode 100644 index 000000000..e156d1260 --- /dev/null +++ b/src/Serialization/DiscriminatedUnionAttribute.cs @@ -0,0 +1,26 @@ +using System; + +namespace Discord.Net.Serialization +{ + /// + /// Defines an attribute used to mark discriminated unions. + /// + public class DiscriminatedUnionAttribute : Attribute + { + /// + /// Gets the field or property used to discriminate between types. + /// + public string DiscriminatorField { get; } + + /// + /// Creates a new instance. + /// + /// + /// The field or property used to discriminate between types. + /// + public DiscriminatedUnionAttribute(string discriminatorField) + { + DiscriminatorField = discriminatorField; + } + } +} diff --git a/src/Serialization/DiscriminatedUnionMemberAttribute.cs b/src/Serialization/DiscriminatedUnionMemberAttribute.cs new file mode 100644 index 000000000..619bb4070 --- /dev/null +++ b/src/Serialization/DiscriminatedUnionMemberAttribute.cs @@ -0,0 +1,27 @@ +using System; + +namespace Discord.Net.Serialization +{ + /// + /// Defines an attribute used to mark members of discriminated unions. + /// + public class DiscriminatedUnionMemberAttribute : Attribute + { + /// + /// Gets the discriminator value used to identify this member type. + /// + public string Discriminator { get; } + + /// + /// Creates a new + /// instance. + /// + /// + /// The discriminator value used to identify this member type. + /// + public DiscriminatedUnionMemberAttribute(string discriminator) + { + Discriminator = discriminator; + } + } +} diff --git a/src/Serialization/Optional.cs b/src/Serialization/Optional.cs new file mode 100644 index 000000000..fd6eb19cc --- /dev/null +++ b/src/Serialization/Optional.cs @@ -0,0 +1,171 @@ +using System; +using Discord.Net.Serialization; + +namespace Discord.Net.Serialization +{ + /// + /// Defines a type which may be either undefined, null or an instance of a + /// value. + /// + /// + /// The type which is contained + /// + public struct Optional + { + private readonly T _value; + + /// + /// Gets the inner value of this if present. + /// + /// + /// The value inside this . + /// + /// + /// This has no inner value. + /// + public T Value + => !IsSpecified + ? throw new InvalidOperationException( + "This property has no value set.") + : _value; + + /// + /// Gets if this has an inner value. + /// + /// + /// A boolean that determines if this has a + /// . + /// + public bool IsSpecified { get; } + + private Optional(T value) + { + _value = value; + IsSpecified = true; + } + + /// + /// Creates a new unspecified . + /// + /// + /// An unspecified . + /// + public static Optional Create() + => default; + + /// + /// Creates a new with the specified + /// . + /// + /// + /// Value that will be specified for this . + /// + /// + /// A specified with the provided value + /// inside. + /// + public static Optional Create(T value) + => new(value); + + /// + /// Gets the or their + /// value. + /// + /// + /// The value inside this or their + /// value. + /// + public T GetValueOrDefault() + => _value; + + /// + /// Gets the or the default value provided. + /// + /// + /// The value inside this or default value + /// provided. + /// + public T GetValueOrDefault(T defaultValue) + => IsSpecified ? _value : defaultValue; + + /// + public override bool Equals(object? other) + { + if (!IsSpecified) + return other == null; + if (other == null || _value == null) + return false; + return _value.Equals(other); + } + + /// + public override int GetHashCode() + => IsSpecified ? _value?.GetHashCode() ?? default : default; + + /// + /// Returns the inner value ToString value or this type fully qualified + /// name. + /// + /// + /// The inner value string value or this type fully qualified name. + /// + public override string? ToString() + => IsSpecified ? _value?.ToString() : default; + + /// + /// Creates a new with the specified + /// . + /// + /// Value to convert + /// + /// A new with the specified + /// . + /// + public static implicit operator Optional(T value) + => new(value); + + /// + /// Gets the inner value. + /// + /// + /// Value to convert + /// + /// + /// The inner value. + /// + public static explicit operator T(Optional value) + => value.Value; + + /// + /// Compares two values for equality. + /// + /// + /// The first value to compare. + /// + /// + /// The second value to compare. + /// + /// + /// if the two values are equal, or + /// otherwise. + /// + public static bool operator ==(Optional left, Optional right) + => left.Equals(right); + + /// + /// Compares two values for inequality. + /// + /// + /// The first value to compare. + /// + /// + /// The second value to compare. + /// + /// + /// if the two values are unequal, or + /// otherwise. + /// + public static bool operator !=(Optional left, Optional right) + => !(left == right); + } +} diff --git a/tools/Directory.Build.props b/tools/Directory.Build.props new file mode 100644 index 000000000..a03503380 --- /dev/null +++ b/tools/Directory.Build.props @@ -0,0 +1,32 @@ + + + + + + + + + true + $(MSBuildAllProjects);$(MSBuildThisFileDirectory)..\Directory.Build.props + tools + + + + + + + + + + + + + + diff --git a/tools/Directory.Build.targets b/tools/Directory.Build.targets new file mode 100644 index 000000000..1f014d179 --- /dev/null +++ b/tools/Directory.Build.targets @@ -0,0 +1,25 @@ + + + + + + + + + $(MSBuildAllProjects);$(MSBuildThisFileDirectory)..\Directory.Build.targets + + + + + + + + + diff --git a/tools/SourceGenerators/Directory.Build.props b/tools/SourceGenerators/Directory.Build.props new file mode 100644 index 000000000..262b2c5c9 --- /dev/null +++ b/tools/SourceGenerators/Directory.Build.props @@ -0,0 +1,36 @@ + + + + + + + + + $(MSBuildAllProjects);$(MSBuildThisFileDirectory)..\Directory.Build.props + + + + + + false + true + + $(NoWarn);RS2000;RS2001;RS2002;RS2003;RS2004;RS2005;RS2006;RS2007;RS2008 + + + + + + + + + + + diff --git a/tools/SourceGenerators/Serialization/Discord.Net.SourceGenerators.Serialization.csproj b/tools/SourceGenerators/Serialization/Discord.Net.SourceGenerators.Serialization.csproj new file mode 100644 index 000000000..9f5c4f4ab --- /dev/null +++ b/tools/SourceGenerators/Serialization/Discord.Net.SourceGenerators.Serialization.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0 + + + diff --git a/tools/SourceGenerators/Serialization/SerializationSourceGenerator.ConverterSource.cs b/tools/SourceGenerators/Serialization/SerializationSourceGenerator.ConverterSource.cs new file mode 100644 index 000000000..8b313aa79 --- /dev/null +++ b/tools/SourceGenerators/Serialization/SerializationSourceGenerator.ConverterSource.cs @@ -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(); + }} + }} +}}"; + } + } +} diff --git a/tools/SourceGenerators/Serialization/SerializationSourceGenerator.OptionsSource.cs b/tools/SourceGenerators/Serialization/SerializationSourceGenerator.OptionsSource.cs new file mode 100644 index 000000000..41674de44 --- /dev/null +++ b/tools/SourceGenerators/Serialization/SerializationSourceGenerator.OptionsSource.cs @@ -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 +{ + /// + /// Defines extension methods for adding Discord.Net JSON converters to a + /// instance. + /// + public static partial class JsonSerializerOptionsExtensions + { + /// + /// Adds Discord.Net JSON converters to the passed + /// . + /// + /// + /// The serializer options to add Discord.Net converters to. + /// + /// + /// The modified , so this method + /// can be chained. + /// + public static partial JsonSerializerOptions WithDiscordNetConverters( + this JsonSerializerOptions options); + } +}"; + } + + private static string GenerateSerializerOptionsSourceCode( + List 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; + }} + }} +}}"; + } + } +} diff --git a/tools/SourceGenerators/Serialization/SerializationSourceGenerator.cs b/tools/SourceGenerators/Serialization/SerializationSourceGenerator.cs new file mode 100644 index 000000000..2b3ba8bcd --- /dev/null +++ b/tools/SourceGenerators/Serialization/SerializationSourceGenerator.cs @@ -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(); + + 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 Classes { get; } = new(); + + private readonly Dictionary _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 attrList + && attrList.Any( + list => list.Attributes + .Any(a => IsInterestingAttribute(a, + context.SemanticModel, + _interestingAttributes.Values)))) + { + Classes.Add(classDecl); + } + } + + private static INamedTypeSymbol GetOrAddAttribute( + Dictionary 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 interestingAttributes) + { + var typeInfo = model.GetTypeInfo(attribute.Name); + + return interestingAttributes.Any( + x => SymbolEqualityComparer.Default + .Equals(typeInfo.Type, x)); + } + } + } +}