Skip to content

Commit

Permalink
Add Newstonsoft.Json converter support
Browse files Browse the repository at this point in the history
This is the first template that truly showcases the dynamic nature of our approach: you only need to add a reference to the Newtonsoft.Json package, and you get the added support, without any additional configuration whatesoever.
  • Loading branch information
kzu committed Nov 25, 2024
1 parent 7c20e39 commit 91b1b7c
Show file tree
Hide file tree
Showing 16 changed files with 196 additions and 30 deletions.
10 changes: 5 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
![Icon](img/icon-32.png) ThisAssembly
![Icon](img/icon-32.png) StructId
============

[![Version](https://img.shields.io/nuget/vpre/ThisAssembly.svg?color=royalblue)](https://www.nuget.org/packages/ThisAssembly)
[![Downloads](https://img.shields.io/nuget/dt/ThisAssembly.svg?color=green)](https://www.nuget.org/packages/ThisAssembly)
[![License](https://img.shields.io/github/license/devlooped/ThisAssembly.svg?color=blue)](https://github.com//devlooped/ThisAssembly/blob/main/license.txt)
[![Build](https://github.com/devlooped/ThisAssembly/workflows/build/badge.svg?branch=main)](https://github.com/devlooped/ThisAssembly/actions)
[![Version](https://img.shields.io/nuget/vpre/StructId.svg?color=royalblue)](https://www.nuget.org/packages/StructId)
[![Downloads](https://img.shields.io/nuget/dt/StructId.svg?color=green)](https://www.nuget.org/packages/StructId)
[![License](https://img.shields.io/github/license/devlooped/StructId.svg?color=blue)](https://github.com//devlooped/StructId/blob/main/license.txt)
[![Build](https://github.com/devlooped/StructId/workflows/build/badge.svg?branch=main)](https://github.com/devlooped/StructId/actions)

<!-- #content -->
An opinionated strongly-typed ID library that uses `readonly record struct` in C# for
Expand Down
10 changes: 0 additions & 10 deletions src/StructId.Analyzer/NJsonConverterGenerator.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/StructId.Analyzer/NewableGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ public class NewableGenerator() : TemplateGenerator(
"System.Object",
ThisAssembly.Resources.Templates.Newable.Text,
ThisAssembly.Resources.Templates.NewableT.Text,
TypeCheck.TypeExists);
ReferenceCheck.TypeExists);
30 changes: 30 additions & 0 deletions src/StructId.Analyzer/NewtonsoftJsonGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

namespace StructId;

[Generator(LanguageNames.CSharp)]
public class NewtonsoftJsonGenerator() : TemplateGenerator(
"Newtonsoft.Json.JsonConverter",
ThisAssembly.Resources.Templates.NewtonsoftJsonConverter.Text,
ThisAssembly.Resources.Templates.NewtonsoftJsonConverterT.Text,
ReferenceCheck.TypeExists)
{
public override void Initialize(IncrementalGeneratorInitializationContext context)
{
base.Initialize(context);

context.RegisterSourceOutput(
context.CompilationProvider
.Select((x, _) => x.GetTypeByMetadataName("Newtonsoft.Json.JsonConverter")),
(context, source) =>
{
if (source == null)
return;

context.AddSource("NewtonsoftJsonConverter.cs", SourceText.From(
ThisAssembly.Resources.Templates.NewtonsoftJsonConverter_1.Text, Encoding.UTF8));
});
}
}
1 change: 1 addition & 0 deletions src/StructId.Analyzer/StructId.Analyzer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

<ItemGroup>
<TemplateCode Include="..\StructId\Templates\*.cs" Link="StructId\%(Filename)%(Extension)" />
<UpToDateCheck Include="@(TemplateCode)"/>
</ItemGroup>

<Target Name="CopyTemplateCode" Inputs="@(TemplateCode)" Outputs="@(TemplateCode -> '$(IntermediateOutputPath)Templates\%(Filename).txt')">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace StructId;

[Generator(LanguageNames.CSharp)]
public class JsonConverterGenerator() : TemplateGenerator(
public class SystemTextJsonGenerator() : TemplateGenerator(
"System.IParsable`1",
ThisAssembly.Resources.Templates.JsonConverter.Text,
ThisAssembly.Resources.Templates.JsonConverterT.Text);
21 changes: 10 additions & 11 deletions src/StructId.Analyzer/TemplateGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace StructId;

public enum TypeCheck
public enum ReferenceCheck
{
/// <summary>
/// The check involves ensuring the type exists in the compilation.
Expand All @@ -20,18 +20,18 @@ public enum TypeCheck
ValueIsType,
}

public abstract class TemplateGenerator(string valueType, string stringTemplate, string typeTemplate, TypeCheck interfaceCheck = TypeCheck.ValueIsType) : IIncrementalGenerator
public abstract class TemplateGenerator(string referenceType, string stringTemplate, string typeTemplate, ReferenceCheck referenceCheck = ReferenceCheck.ValueIsType) : IIncrementalGenerator
{
record struct TemplateArgs(string TargetNamespace, INamedTypeSymbol StructId, INamedTypeSymbol ValueType, INamedTypeSymbol InterfaceType, INamedTypeSymbol StringType);
record struct TemplateArgs(string TargetNamespace, INamedTypeSymbol StructId, INamedTypeSymbol ValueType, INamedTypeSymbol ReferenceType, INamedTypeSymbol StringType);

public void Initialize(IncrementalGeneratorInitializationContext context)
public virtual void Initialize(IncrementalGeneratorInitializationContext context)
{
var targetNamespace = context.AnalyzerConfigOptionsProvider
.Select((x, _) => x.GlobalOptions.TryGetValue("build_property.StructIdNamespace", out var ns) ? ns : "StructId");

// Locate the required types
var types = context.CompilationProvider
.Select((x, _) => (InterfaceType: x.GetTypeByMetadataName(valueType), StringType: x.GetTypeByMetadataName("System.String")));
.Select((x, _) => (ReferenceType: x.GetTypeByMetadataName(referenceType), StringType: x.GetTypeByMetadataName("System.String")));

var ids = context.CompilationProvider
.SelectMany((x, _) => x.Assembly.GetAllTypes().OfType<INamedTypeSymbol>())
Expand All @@ -40,11 +40,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)

var combined = ids.Combine(types)
// NOTE: we never generate for compilations that don't have the specified value interface type
.Where(x => x.Right.InterfaceType != null || x.Right.StringType == null)
.Where(x => x.Right.ReferenceType != null || x.Right.StringType == null)
.Combine(targetNamespace)
.Select((x, _) =>
{
var ((structId, (interfaceType, stringType)), targetNamespace) = x;
var ((structId, (referenceType, stringType)), targetNamespace) = x;
// The value type is either a generic type argument for IStructId<T>, or the string type
// for the non-generic IStructId
Expand All @@ -53,15 +53,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.TypeArguments.OfType<INamedTypeSymbol>().FirstOrDefault() ??
stringType!;
return new TemplateArgs(targetNamespace, structId, valueType, interfaceType!, stringType!);
return new TemplateArgs(targetNamespace, structId, valueType, referenceType!, stringType!);
});

if (interfaceCheck == TypeCheck.ValueIsType)
combined = combined.Where(x => x.ValueType.Is(x.InterfaceType));
if (referenceCheck == ReferenceCheck.ValueIsType)
combined = combined.Where(x => x.ValueType.Is(x.ReferenceType));

context.RegisterImplementationSourceOutput(combined, GenerateCode);
}

void GenerateCode(SourceProductionContext context, TemplateArgs args)
{
var ns = args.StructId.ContainingNamespace.Equals(args.StructId.ContainingModule.GlobalNamespace, SymbolEqualityComparer.Default)
Expand Down
10 changes: 10 additions & 0 deletions src/StructId.FunctionalTests/Functional.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace StructId.Functional;

Expand Down Expand Up @@ -30,10 +31,19 @@ public void Newtonsoft()
var user = new User(new UserId(1), "User", new Wallet(new WalletId("1234"), "Wallet"));

var json = JsonConvert.SerializeObject(product, Formatting.Indented);

// Serialized as a primitive
Assert.Equal(JTokenType.String, JObject.Parse(json).Property("Id")!.Value.Type);

var product2 = JsonConvert.DeserializeObject<Product>(json);
Assert.Equal(product, product2);

json = JsonConvert.SerializeObject(user, Formatting.Indented);

// Serialized as a primitive
Assert.Equal(JTokenType.Integer, JObject.Parse(json).Property("Id")!.Value.Type);
Assert.Equal(JTokenType.String, JObject.Parse(json).SelectToken("$.Wallet.Id")!.Type);

var user2 = JsonConvert.DeserializeObject<User>(json);
Assert.Equal(user, user2);
}
Expand Down
3 changes: 3 additions & 0 deletions src/StructId.Tests/ConstructorGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public async Task GenerateRecordConstructor()
{
var test = new CSharpSourceGeneratorTest<ConstructorGenerator, DefaultVerifier>
{
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck,
TestState =
{
Expand All @@ -33,6 +34,7 @@ public async Task GenerateRecordStringConstructor()
{
var test = new CSharpSourceGeneratorTest<ConstructorGenerator, DefaultVerifier>
{
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck,
TestState =
{
Expand All @@ -56,6 +58,7 @@ public async Task GenerateRecordConstructorInGlobalNamespace()
{
var test = new CSharpSourceGeneratorTest<ConstructorGenerator, DefaultVerifier>
{
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck,
TestState =
{
Expand Down
115 changes: 115 additions & 0 deletions src/StructId.Tests/NewtonsoftJsonGeneratorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit.Sdk;

namespace StructId;

public class NewtonsoftJsonGeneratorTests
{
[Fact]
public async Task DoesNotGenerateIfNewtonsoftJsonNotPresent()
{
var test = new CSharpSourceGeneratorTest<NewtonsoftJsonGenerator, DefaultVerifier>
{
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
TestState =
{
Sources =
{
"""
using StructId;

public readonly partial record struct UserId(int Value) : IStructId<int>;
""",
},
},
}.WithAnalyzerStructId();

await test.RunAsync();
}

[Fact]
public async Task GenerateIfNewtonsoftJsonPresent()
{
var test = new StructIdSourceGeneratorTest<NewtonsoftJsonGenerator>("int")
{
SolutionTransforms =
{
(solution, projectId) => solution
.GetProject(projectId)?
.AddMetadataReference(MetadataReference.CreateFromFile(typeof(JsonConvert).Assembly.Location))
.Solution ?? solution
},
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
TestState =
{
Sources =
{
"""
using StructId;

public readonly partial record struct UserId(int Value) : IStructId<int>;
""",
},
GeneratedSources =
{
(typeof(NewtonsoftJsonGenerator), "UserId.cs",
ThisAssembly.Resources.StructId.Templates.NewtonsoftJsonConverterT.Text.Replace("TStruct", "UserId").Replace("TValue", "int"),
Encoding.UTF8),
(typeof(NewtonsoftJsonGenerator), "NewtonsoftJsonConverter.cs",
ThisAssembly.Resources.StructId.Templates.NewtonsoftJsonConverter_1.Text,
Encoding.UTF8)
},
},
}.WithAnalyzerStructId();

await test.RunAsync();
}

class StructIdSourceGeneratorTest<TGenerator> : CSharpSourceGeneratorTest<TGenerator, DefaultVerifier>
where TGenerator : new()
{
public StructIdSourceGeneratorTest(string? tvalue = null)
{
ReferenceAssemblies = ReferenceAssemblies.Net.Net80;

if (tvalue != null)
{
TestState.GeneratedSources.Add(
(typeof(NewableGenerator), "UserId.cs",
ThisAssembly.Resources.StructId.Templates.NewableT.Text.Replace("TStruct", "UserId").Replace("TValue", tvalue),
Encoding.UTF8));
TestState.GeneratedSources.Add(
(typeof(ParsableGenerator), "UserId.cs",
ThisAssembly.Resources.StructId.Templates.ParsableT.Text.Replace("TStruct", "UserId").Replace("TValue", "int"),
Encoding.UTF8));
}
else
{
TestState.GeneratedSources.Add(
(typeof(NewableGenerator), "UserId.cs",
ThisAssembly.Resources.StructId.Templates.Newable.Text.Replace("SStruct", "UserId"),
Encoding.UTF8));
TestState.GeneratedSources.Add(
(typeof(ParsableGenerator), "UserId.cs",
ThisAssembly.Resources.StructId.Templates.Parsable.Text.Replace("SStruct", "UserId"),
Encoding.UTF8));
}
}

protected override IEnumerable<Type> GetSourceGenerators()
{
yield return typeof(NewableGenerator);
yield return typeof(ParsableGenerator);
yield return typeof(TGenerator);
}
}
}
9 changes: 9 additions & 0 deletions src/StructId.Tests/RecordAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public async Task ReadonlyRecordStructNoStructId()
{
var test = new Test
{
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
TestCode =
"""
public readonly record struct UserId(int Value);
Expand All @@ -29,6 +30,7 @@ public async Task RecordStructNoStructId()
{
var test = new Test
{
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
TestCode =
"""
public record struct UserId(int Value);
Expand All @@ -43,6 +45,7 @@ public async Task ReadonlyStringRecordStructNotPartial()
{
var test = new Test
{
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
TestCode =
"""
using StructId;
Expand All @@ -62,6 +65,7 @@ public async Task ReadonlyRecordStructNotPartial()
{
var test = new Test
{
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
TestCode =
"""
using StructId;
Expand All @@ -81,6 +85,7 @@ public async Task PartialRecordStructNotReadonly()
{
var test = new Test
{
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
TestCode =
"""
using StructId;
Expand All @@ -102,6 +107,7 @@ public async Task PartialStringRecordStructNotReadonly()
{
var test = new Test
{
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
TestCode =
"""
using StructId;
Expand All @@ -124,6 +130,7 @@ public async Task RecordStructNotReadonlyNorPartial()
{
var test = new Test
{
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
TestCode =
"""
using StructId;
Expand All @@ -143,6 +150,7 @@ public async Task StructNotStructId()
{
var test = new Test
{
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
TestCode =
"""
using StructId;
Expand All @@ -162,6 +170,7 @@ public async Task ClassNotStructId()
{
var test = new Test
{
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
TestCode =
"""
using StructId;
Expand Down
Loading

0 comments on commit 91b1b7c

Please sign in to comment.