Skip to content

Commit 91b1b7c

Browse files
committed
Add Newstonsoft.Json converter support
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.
1 parent 7c20e39 commit 91b1b7c

16 files changed

+196
-30
lines changed

readme.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
![Icon](img/icon-32.png) ThisAssembly
1+
![Icon](img/icon-32.png) StructId
22
============
33

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

99
<!-- #content -->
1010
An opinionated strongly-typed ID library that uses `readonly record struct` in C# for

src/StructId.Analyzer/NJsonConverterGenerator.cs

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/StructId.Analyzer/NewableGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ public class NewableGenerator() : TemplateGenerator(
77
"System.Object",
88
ThisAssembly.Resources.Templates.Newable.Text,
99
ThisAssembly.Resources.Templates.NewableT.Text,
10-
TypeCheck.TypeExists);
10+
ReferenceCheck.TypeExists);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System.Text;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.Text;
4+
5+
namespace StructId;
6+
7+
[Generator(LanguageNames.CSharp)]
8+
public class NewtonsoftJsonGenerator() : TemplateGenerator(
9+
"Newtonsoft.Json.JsonConverter",
10+
ThisAssembly.Resources.Templates.NewtonsoftJsonConverter.Text,
11+
ThisAssembly.Resources.Templates.NewtonsoftJsonConverterT.Text,
12+
ReferenceCheck.TypeExists)
13+
{
14+
public override void Initialize(IncrementalGeneratorInitializationContext context)
15+
{
16+
base.Initialize(context);
17+
18+
context.RegisterSourceOutput(
19+
context.CompilationProvider
20+
.Select((x, _) => x.GetTypeByMetadataName("Newtonsoft.Json.JsonConverter")),
21+
(context, source) =>
22+
{
23+
if (source == null)
24+
return;
25+
26+
context.AddSource("NewtonsoftJsonConverter.cs", SourceText.From(
27+
ThisAssembly.Resources.Templates.NewtonsoftJsonConverter_1.Text, Encoding.UTF8));
28+
});
29+
}
30+
}

src/StructId.Analyzer/StructId.Analyzer.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
<ItemGroup>
2222
<TemplateCode Include="..\StructId\Templates\*.cs" Link="StructId\%(Filename)%(Extension)" />
23+
<UpToDateCheck Include="@(TemplateCode)"/>
2324
</ItemGroup>
2425

2526
<Target Name="CopyTemplateCode" Inputs="@(TemplateCode)" Outputs="@(TemplateCode -> '$(IntermediateOutputPath)Templates\%(Filename).txt')">

src/StructId.Analyzer/JsonConverterGenerator.cs renamed to src/StructId.Analyzer/SystemTextJsonGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace StructId;
44

55
[Generator(LanguageNames.CSharp)]
6-
public class JsonConverterGenerator() : TemplateGenerator(
6+
public class SystemTextJsonGenerator() : TemplateGenerator(
77
"System.IParsable`1",
88
ThisAssembly.Resources.Templates.JsonConverter.Text,
99
ThisAssembly.Resources.Templates.JsonConverterT.Text);

src/StructId.Analyzer/TemplateGenerator.cs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
namespace StructId;
1010

11-
public enum TypeCheck
11+
public enum ReferenceCheck
1212
{
1313
/// <summary>
1414
/// The check involves ensuring the type exists in the compilation.
@@ -20,18 +20,18 @@ public enum TypeCheck
2020
ValueIsType,
2121
}
2222

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

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

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

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

4141
var combined = ids.Combine(types)
4242
// NOTE: we never generate for compilations that don't have the specified value interface type
43-
.Where(x => x.Right.InterfaceType != null || x.Right.StringType == null)
43+
.Where(x => x.Right.ReferenceType != null || x.Right.StringType == null)
4444
.Combine(targetNamespace)
4545
.Select((x, _) =>
4646
{
47-
var ((structId, (interfaceType, stringType)), targetNamespace) = x;
47+
var ((structId, (referenceType, stringType)), targetNamespace) = x;
4848

4949
// The value type is either a generic type argument for IStructId<T>, or the string type
5050
// for the non-generic IStructId
@@ -53,15 +53,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
5353
.TypeArguments.OfType<INamedTypeSymbol>().FirstOrDefault() ??
5454
stringType!;
5555

56-
return new TemplateArgs(targetNamespace, structId, valueType, interfaceType!, stringType!);
56+
return new TemplateArgs(targetNamespace, structId, valueType, referenceType!, stringType!);
5757
});
5858

59-
if (interfaceCheck == TypeCheck.ValueIsType)
60-
combined = combined.Where(x => x.ValueType.Is(x.InterfaceType));
59+
if (referenceCheck == ReferenceCheck.ValueIsType)
60+
combined = combined.Where(x => x.ValueType.Is(x.ReferenceType));
6161

6262
context.RegisterImplementationSourceOutput(combined, GenerateCode);
6363
}
64-
6564
void GenerateCode(SourceProductionContext context, TemplateArgs args)
6665
{
6766
var ns = args.StructId.ContainingNamespace.Equals(args.StructId.ContainingModule.GlobalNamespace, SymbolEqualityComparer.Default)

src/StructId.FunctionalTests/Functional.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Newtonsoft.Json;
2+
using Newtonsoft.Json.Linq;
23

34
namespace StructId.Functional;
45

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

3233
var json = JsonConvert.SerializeObject(product, Formatting.Indented);
34+
35+
// Serialized as a primitive
36+
Assert.Equal(JTokenType.String, JObject.Parse(json).Property("Id")!.Value.Type);
37+
3338
var product2 = JsonConvert.DeserializeObject<Product>(json);
3439
Assert.Equal(product, product2);
3540

3641
json = JsonConvert.SerializeObject(user, Formatting.Indented);
42+
43+
// Serialized as a primitive
44+
Assert.Equal(JTokenType.Integer, JObject.Parse(json).Property("Id")!.Value.Type);
45+
Assert.Equal(JTokenType.String, JObject.Parse(json).SelectToken("$.Wallet.Id")!.Type);
46+
3747
var user2 = JsonConvert.DeserializeObject<User>(json);
3848
Assert.Equal(user, user2);
3949
}

src/StructId.Tests/ConstructorGeneratorTests.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public async Task GenerateRecordConstructor()
1010
{
1111
var test = new CSharpSourceGeneratorTest<ConstructorGenerator, DefaultVerifier>
1212
{
13+
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
1314
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck,
1415
TestState =
1516
{
@@ -33,6 +34,7 @@ public async Task GenerateRecordStringConstructor()
3334
{
3435
var test = new CSharpSourceGeneratorTest<ConstructorGenerator, DefaultVerifier>
3536
{
37+
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
3638
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck,
3739
TestState =
3840
{
@@ -56,6 +58,7 @@ public async Task GenerateRecordConstructorInGlobalNamespace()
5658
{
5759
var test = new CSharpSourceGeneratorTest<ConstructorGenerator, DefaultVerifier>
5860
{
61+
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
5962
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck,
6063
TestState =
6164
{
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CSharp.Testing;
8+
using Microsoft.CodeAnalysis.Testing;
9+
using Newtonsoft.Json;
10+
using Newtonsoft.Json.Linq;
11+
using Xunit.Sdk;
12+
13+
namespace StructId;
14+
15+
public class NewtonsoftJsonGeneratorTests
16+
{
17+
[Fact]
18+
public async Task DoesNotGenerateIfNewtonsoftJsonNotPresent()
19+
{
20+
var test = new CSharpSourceGeneratorTest<NewtonsoftJsonGenerator, DefaultVerifier>
21+
{
22+
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
23+
TestState =
24+
{
25+
Sources =
26+
{
27+
"""
28+
using StructId;
29+
30+
public readonly partial record struct UserId(int Value) : IStructId<int>;
31+
""",
32+
},
33+
},
34+
}.WithAnalyzerStructId();
35+
36+
await test.RunAsync();
37+
}
38+
39+
[Fact]
40+
public async Task GenerateIfNewtonsoftJsonPresent()
41+
{
42+
var test = new StructIdSourceGeneratorTest<NewtonsoftJsonGenerator>("int")
43+
{
44+
SolutionTransforms =
45+
{
46+
(solution, projectId) => solution
47+
.GetProject(projectId)?
48+
.AddMetadataReference(MetadataReference.CreateFromFile(typeof(JsonConvert).Assembly.Location))
49+
.Solution ?? solution
50+
},
51+
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
52+
TestState =
53+
{
54+
Sources =
55+
{
56+
"""
57+
using StructId;
58+
59+
public readonly partial record struct UserId(int Value) : IStructId<int>;
60+
""",
61+
},
62+
GeneratedSources =
63+
{
64+
(typeof(NewtonsoftJsonGenerator), "UserId.cs",
65+
ThisAssembly.Resources.StructId.Templates.NewtonsoftJsonConverterT.Text.Replace("TStruct", "UserId").Replace("TValue", "int"),
66+
Encoding.UTF8),
67+
(typeof(NewtonsoftJsonGenerator), "NewtonsoftJsonConverter.cs",
68+
ThisAssembly.Resources.StructId.Templates.NewtonsoftJsonConverter_1.Text,
69+
Encoding.UTF8)
70+
},
71+
},
72+
}.WithAnalyzerStructId();
73+
74+
await test.RunAsync();
75+
}
76+
77+
class StructIdSourceGeneratorTest<TGenerator> : CSharpSourceGeneratorTest<TGenerator, DefaultVerifier>
78+
where TGenerator : new()
79+
{
80+
public StructIdSourceGeneratorTest(string? tvalue = null)
81+
{
82+
ReferenceAssemblies = ReferenceAssemblies.Net.Net80;
83+
84+
if (tvalue != null)
85+
{
86+
TestState.GeneratedSources.Add(
87+
(typeof(NewableGenerator), "UserId.cs",
88+
ThisAssembly.Resources.StructId.Templates.NewableT.Text.Replace("TStruct", "UserId").Replace("TValue", tvalue),
89+
Encoding.UTF8));
90+
TestState.GeneratedSources.Add(
91+
(typeof(ParsableGenerator), "UserId.cs",
92+
ThisAssembly.Resources.StructId.Templates.ParsableT.Text.Replace("TStruct", "UserId").Replace("TValue", "int"),
93+
Encoding.UTF8));
94+
}
95+
else
96+
{
97+
TestState.GeneratedSources.Add(
98+
(typeof(NewableGenerator), "UserId.cs",
99+
ThisAssembly.Resources.StructId.Templates.Newable.Text.Replace("SStruct", "UserId"),
100+
Encoding.UTF8));
101+
TestState.GeneratedSources.Add(
102+
(typeof(ParsableGenerator), "UserId.cs",
103+
ThisAssembly.Resources.StructId.Templates.Parsable.Text.Replace("SStruct", "UserId"),
104+
Encoding.UTF8));
105+
}
106+
}
107+
108+
protected override IEnumerable<Type> GetSourceGenerators()
109+
{
110+
yield return typeof(NewableGenerator);
111+
yield return typeof(ParsableGenerator);
112+
yield return typeof(TGenerator);
113+
}
114+
}
115+
}

0 commit comments

Comments
 (0)