Browse Source

Add Serialization subproject

feature/3.0-serialization-generator
FiniteReality 4 years ago
parent
commit
f1bff0e3b5
18 changed files with 677 additions and 102 deletions
  1. +3
    -2
      Directory.Build.targets
  2. +35
    -0
      Discord.Net.sln
  3. +5
    -1
      samples/PingPong/PingPong.csproj
  4. +5
    -1
      src/Models/Discord.Net.Models.csproj
  5. +0
    -98
      src/Models/Optional.cs
  6. +45
    -0
      src/Serialization/Converters/OptionalConverter.cs
  7. +36
    -0
      src/Serialization/Converters/OptionalConverterFactory.cs
  8. +12
    -0
      src/Serialization/Discord.Net.Serialization.csproj
  9. +26
    -0
      src/Serialization/DiscriminatedUnionAttribute.cs
  10. +27
    -0
      src/Serialization/DiscriminatedUnionMemberAttribute.cs
  11. +171
    -0
      src/Serialization/Optional.cs
  12. +32
    -0
      tools/Directory.Build.props
  13. +25
    -0
      tools/Directory.Build.targets
  14. +36
    -0
      tools/SourceGenerators/Directory.Build.props
  15. +7
    -0
      tools/SourceGenerators/Serialization/Discord.Net.SourceGenerators.Serialization.csproj
  16. +37
    -0
      tools/SourceGenerators/Serialization/SerializationSourceGenerator.ConverterSource.cs
  17. +67
    -0
      tools/SourceGenerators/Serialization/SerializationSourceGenerator.OptionsSource.cs
  18. +108
    -0
      tools/SourceGenerators/Serialization/SerializationSourceGenerator.cs

+ 3
- 2
Directory.Build.targets View File

@@ -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" />


+ 35
- 0
Discord.Net.sln View File

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


+ 5
- 1
samples/PingPong/PingPong.csproj View File

@@ -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>


+ 5
- 1
src/Models/Discord.Net.Models.csproj View File

@@ -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>

+ 0
- 98
src/Models/Optional.cs View File

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

+ 45
- 0
src/Serialization/Converters/OptionalConverter.cs View File

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

+ 36
- 0
src/Serialization/Converters/OptionalConverterFactory.cs View File

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

+ 12
- 0
src/Serialization/Discord.Net.Serialization.csproj View File

@@ -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>

+ 26
- 0
src/Serialization/DiscriminatedUnionAttribute.cs View File

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

+ 27
- 0
src/Serialization/DiscriminatedUnionMemberAttribute.cs View File

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

+ 171
- 0
src/Serialization/Optional.cs View File

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

+ 32
- 0
tools/Directory.Build.props View File

@@ -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>

+ 25
- 0
tools/Directory.Build.targets View File

@@ -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>

+ 36
- 0
tools/SourceGenerators/Directory.Build.props View File

@@ -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>

+ 7
- 0
tools/SourceGenerators/Serialization/Discord.Net.SourceGenerators.Serialization.csproj View File

@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

</Project>

+ 37
- 0
tools/SourceGenerators/Serialization/SerializationSourceGenerator.ConverterSource.cs View File

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

+ 67
- 0
tools/SourceGenerators/Serialization/SerializationSourceGenerator.OptionsSource.cs View File

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

+ 108
- 0
tools/SourceGenerators/Serialization/SerializationSourceGenerator.cs View File

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

Loading…
Cancel
Save