Skip to content

Commit 686118b

Browse files
committed
Updated packages, allow renaming types, parse property types in an improved way (for numeric values and DateTime)
1 parent c6c0f5f commit 686118b

11 files changed

+450
-34
lines changed

JsonByExampleGenerator.Generator/JsonByExampleGenerator.Generator.csproj

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@
2727
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
2828
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
2929
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" />
30-
<PackageReference Include="NuGetizer" Version="0.4.11" />
30+
<PackageReference Include="NuGetizer" Version="0.6.0">
31+
<PrivateAssets>all</PrivateAssets>
32+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
33+
</PackageReference>
3134
<PackageReference Include="Pluralize.NET" Version="1.0.2" GeneratePathProperty="true" PrivateAssets="all" />
32-
<PackageReference Include="Scriban" Version="3.4.0" GeneratePathProperty="true" IncludeAssets="Build" PrivateAssets="all" />
33-
<PackageReference Include="System.Text.Json" Version="5.0.0" GeneratePathProperty="true" PrivateAssets="all" />
35+
<PackageReference Include="Scriban" Version="3.4.1" GeneratePathProperty="true" IncludeAssets="build" PrivateAssets="all" />
36+
<PackageReference Include="System.Text.Json" Version="5.0.1" GeneratePathProperty="true" PrivateAssets="all" />
3437
</ItemGroup>
3538

3639
<PropertyGroup>

JsonByExampleGenerator.Generator/JsonByExampleTemplate.sbntxt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#nullable disable
2+
using System;
23
using System.Collections.Generic;
34
using System.Text.Json.Serialization;{{ for OptionalDependency in OptionalDependencies}}
45
using {{ OptionalDependency }};

JsonByExampleGenerator.Generator/JsonGenerator.cs

Lines changed: 126 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
using Microsoft.CodeAnalysis.CSharp;
99
using Microsoft.CodeAnalysis.CSharp.Syntax;
1010
using Microsoft.CodeAnalysis.Text;
11-
using Microsoft.CodeAnalysis.Emit;
12-
using System.Reflection;
1311
using Pluralize.NET;
1412
using JsonByExampleGenerator.Generator.Models;
1513
using System.Globalization;
@@ -35,6 +33,31 @@ public void Execute(GeneratorExecutionContext context)
3533
{
3634
try
3735
{
36+
// The namespace of the code is determined by the assembly name of the compilation
37+
string namespaceName = context.Compilation?.AssemblyName ?? "JsonByExample";
38+
39+
// Determine if the functionality for easy access to configuration should be enabled
40+
bool configEnabled = IsConfigurationEnabled(context);
41+
42+
// A list of dependencies to be added to the using statements in the code generation
43+
var optionalDependencies = new List<string>();
44+
if (configEnabled)
45+
{
46+
optionalDependencies.Add("Microsoft.Extensions.Configuration");
47+
}
48+
49+
// Generate code that should only be generated once
50+
const string onlyOnceTemplatePath = "OnlyOnceTemplate.sbntxt";
51+
var onlyOnceTemplate = Template.Parse(EmbeddedResource.GetContent(onlyOnceTemplatePath), onlyOnceTemplatePath);
52+
string onlyOnceGeneratedCode = onlyOnceTemplate.Render(new
53+
{
54+
NamespaceName = namespaceName
55+
}, member => member.Name);
56+
57+
// Add the generated code to the compilation
58+
context.AddSource($"{namespaceName}_onlyonce.gen.cs",
59+
SourceText.From(onlyOnceGeneratedCode, Encoding.UTF8));
60+
3861
// Resolve all json files that are added to the AdditionalFiles in the compilation
3962
foreach (var jsonFile in context.AdditionalFiles.Where(f => f.Path.EndsWith(".json", StringComparison.InvariantCultureIgnoreCase)))
4063
{
@@ -44,26 +67,14 @@ public void Execute(GeneratorExecutionContext context)
4467
continue;
4568
}
4669

47-
// Determine if the functionality for easy access to configuration should be enabled
48-
bool configEnabled = IsConfigurationEnabled(context);
49-
5070
var json = JsonDocument.Parse(jsonFileText.ToString());
5171

52-
// The namespace of the code is determined by the assembly name of the compilation
53-
string namespaceName = context.Compilation?.AssemblyName ?? "JsonByExample";
54-
5572
// Read the json and build a list of models that can be used to generate classes
5673
var classModels = new List<ClassModel>();
5774
var jsonElement = json.RootElement;
5875
string rootTypeName = GetValidName(Path.GetFileNameWithoutExtension(jsonFile.Path).Replace(" ", string.Empty), true);
5976
ResolveTypeRecursive(context, classModels, jsonElement, rootTypeName);
6077

61-
// A list of dependencies to be added to the using statements in the code generation
62-
var optionalDependencies = new List<string>();
63-
if (configEnabled)
64-
{
65-
optionalDependencies.Add("Microsoft.Extensions.Configuration");
66-
}
6778

6879
// Attempt to find a Scriban template in the AdditionalFiles that has the same name as the json
6980
string templateFileName = $"{Path.GetFileNameWithoutExtension(jsonFile.Path)}.sbntxt";
@@ -131,7 +142,56 @@ public void Execute(GeneratorExecutionContext context)
131142
/// <param name="compilation">The compilation, so we can find existing types</param>
132143
private void FilterAndChangeBasedOnExistingCode(List<ClassModel> classModels, string namespaceName, Compilation compilation)
133144
{
134-
foreach(var classModel in classModels)
145+
// Deal with classes that have been decorated with JsonRenamedFrom attribute
146+
// They must be renamed in the model
147+
var renamedAttributes = compilation
148+
.SyntaxTrees
149+
.SelectMany(s => s
150+
.GetRoot()
151+
.DescendantNodes()
152+
.Where(d => d.IsKind(SyntaxKind.Attribute))
153+
.OfType<AttributeSyntax>()
154+
.Where(d => d.Name.ToString() == "JsonRenamedFrom")
155+
.Select(d => new
156+
{
157+
Renamed = (d?.Parent?.Parent as ClassDeclarationSyntax)?.Identifier.ToString().Trim(),
158+
From = d?.ArgumentList?.Arguments.FirstOrDefault()?.ToString().Trim().Trim('\"')
159+
}))
160+
.Where(x => x.From != null
161+
&& x.Renamed != null
162+
&& compilation.GetTypeByMetadataName($"{namespaceName}.Json.{x.Renamed}") != null)
163+
.ToList();
164+
foreach (var classModel in classModels)
165+
{
166+
var match = renamedAttributes.FirstOrDefault(r => r.From == classModel.ClassName);
167+
if (match != null)
168+
{
169+
// Rename class
170+
classModel.ClassName = match.Renamed ?? classModel.ClassName;
171+
172+
// Find all properties and update the type and init
173+
foreach(var property in classModels.SelectMany(p => p.Properties).ToList())
174+
{
175+
// Update init statement, if applicable
176+
if(property.Init == $"new List<{match.From}>()")
177+
{
178+
property.Init = $"new List<{classModel.ClassName}>()";
179+
}
180+
181+
// Rename property type, if applicable
182+
if(property.PropertyType == match.From)
183+
{
184+
property.PropertyType = classModel.ClassName;
185+
}
186+
else if (property.PropertyType == $"IList<{match.From}>")
187+
{
188+
property.PropertyType = $"IList<{classModel.ClassName}>";
189+
}
190+
}
191+
}
192+
}
193+
194+
foreach (var classModel in classModels)
135195
{
136196
// Find a class in the current compilation that already exists
137197
var existingClass = compilation.GetTypeByMetadataName($"{namespaceName}.Json.{classModel.ClassName}");
@@ -217,11 +277,11 @@ private static void ResolveTypeRecursive(GeneratorExecutionContext context, List
217277
{
218278
if (arrEnumerator.Current.ValueKind == JsonValueKind.Number)
219279
{
220-
arrPropName = "double";
280+
arrPropName = FindBestNumericType(arrEnumerator.Current);
221281
}
222282
else if (arrEnumerator.Current.ValueKind == JsonValueKind.String)
223283
{
224-
arrPropName = "string";
284+
arrPropName = FindBestStringType(arrEnumerator.Current);
225285
}
226286
else if (arrEnumerator.Current.ValueKind == JsonValueKind.True || arrEnumerator.Current.ValueKind == JsonValueKind.False)
227287
{
@@ -247,8 +307,8 @@ private static void ResolveTypeRecursive(GeneratorExecutionContext context, List
247307

248308
break;
249309
}
250-
case JsonValueKind.String: propertyModel = new PropertyModel(prop.Name, "string", propName); break;
251-
case JsonValueKind.Number: propertyModel = new PropertyModel(prop.Name, "double", propName); break;
310+
case JsonValueKind.String: propertyModel = new PropertyModel(prop.Name, FindBestStringType(prop.Value), propName); break;
311+
case JsonValueKind.Number: propertyModel = new PropertyModel(prop.Name, FindBestNumericType(prop.Value), propName); break;
252312
case JsonValueKind.False:
253313
case JsonValueKind.True: propertyModel = new PropertyModel(prop.Name, "bool", propName); break;
254314
case JsonValueKind.Object:
@@ -283,6 +343,53 @@ private static void ResolveTypeRecursive(GeneratorExecutionContext context, List
283343
}
284344
}
285345

346+
/// <summary>
347+
/// Based on the value specified, determine an appropriate numeric type.
348+
/// </summary>
349+
/// <param name="propertyValue">Example value of the property</param>
350+
/// <returns>The name of the numeric type</returns>
351+
private static string FindBestNumericType(JsonElement propertyValue)
352+
{
353+
if (propertyValue.TryGetInt32(out _))
354+
{
355+
return "int";
356+
}
357+
358+
if (propertyValue.TryGetInt64(out _))
359+
{
360+
return "long";
361+
}
362+
363+
if (propertyValue.TryGetDouble(out var doubleVal)
364+
&& propertyValue.TryGetDecimal(out var decimalVal)
365+
&& Convert.ToDecimal(doubleVal) == decimalVal)
366+
{
367+
return "double";
368+
}
369+
370+
if (propertyValue.TryGetDecimal(out _))
371+
{
372+
return "decimal";
373+
}
374+
375+
return "object";
376+
}
377+
378+
/// <summary>
379+
/// Based on the value specified, determine if anything better than "string" can be used.
380+
/// </summary>
381+
/// <param name="current">Example value of the property</param>
382+
/// <returns>string or something better</returns>
383+
private static string FindBestStringType(JsonElement propertyValue)
384+
{
385+
if (propertyValue.TryGetDateTime(out _))
386+
{
387+
return "DateTime";
388+
}
389+
390+
return "string";
391+
}
392+
286393
/// <summary>
287394
/// Gets a name that is valid in C# and makes it Pascal-case.
288395
/// Optionally, it can singularize the name, so that a list property has a proper model class.

JsonByExampleGenerator.Generator/Models/ClassModel.cs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Linq;
33
using System.Collections.Generic;
4-
using System.Text;
54

65
namespace JsonByExampleGenerator.Generator.Models
76
{
@@ -10,6 +9,14 @@ namespace JsonByExampleGenerator.Generator.Models
109
/// </summary>
1110
public class ClassModel
1211
{
12+
private static readonly string[] numericPropertyTypeOrder = new[]
13+
{
14+
"int",
15+
"long",
16+
"double",
17+
"decimal"
18+
};
19+
1320
/// <summary>
1421
/// The name of the class, that should be valid in C#.
1522
/// </summary>
@@ -37,7 +44,28 @@ public void Merge(ClassModel classModel)
3744
{
3845
if (classModel != null)
3946
{
40-
Properties.AddRange(classModel.Properties.Except(this.Properties, new PropertyModelEqualityComparer()));
47+
foreach(var property in classModel.Properties)
48+
{
49+
var existingProp = Properties.FirstOrDefault(p => p.PropertyName == property.PropertyName);
50+
if(existingProp == null)
51+
{
52+
Properties.Add(property);
53+
}
54+
else if(existingProp.PropertyType != property.PropertyType)
55+
{
56+
// If there is a less restrictive property type that is needed, it must be changed
57+
if (numericPropertyTypeOrder.Contains(existingProp.PropertyType)
58+
&& numericPropertyTypeOrder.Contains(property.PropertyType)
59+
&& Array.IndexOf(numericPropertyTypeOrder, existingProp.PropertyType) < Array.IndexOf(numericPropertyTypeOrder, property.PropertyType))
60+
{
61+
existingProp.PropertyType = property.PropertyType;
62+
}
63+
else if (existingProp.PropertyType == "DateTime" && property.PropertyType == "string")
64+
{
65+
existingProp.PropertyType = property.PropertyType;
66+
}
67+
}
68+
}
4169
}
4270
}
4371
}

JsonByExampleGenerator.Generator/Models/PropertyModel.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class PropertyModel
88
/// <summary>
99
/// The C# type of the property.
1010
/// </summary>
11-
public string PropertyType { get; private set; }
11+
public string PropertyType { get; internal set; }
1212

1313
/// <summary>
1414
/// The C# safe to use name of the property.
@@ -19,12 +19,12 @@ public class PropertyModel
1919
/// The original property name, before making it safe to use in C#.
2020
/// Can be used for example, for mapping back to json or comments.
2121
/// </summary>
22-
public object PropertyNameOriginal { get; private set; }
22+
public string PropertyNameOriginal { get; private set; }
2323

2424
/// <summary>
2525
/// If the property needs to have a default value, it can be specified here.
2626
/// </summary>
27-
public string? Init { get; set; }
27+
public string? Init { get; internal set; }
2828

2929
/// <summary>
3030
/// Create a new instance of the class.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
3+
namespace {{ NamespaceName }}.Json
4+
{
5+
[AttributeUsage(AttributeTargets.Class)]
6+
internal sealed class JsonRenamedFromAttribute : Attribute
7+
{
8+
internal string SourceName { get; private set; }
9+
10+
internal JsonRenamedFromAttribute(string sourceName)
11+
{
12+
SourceName = sourceName;
13+
}
14+
}
15+
}

0 commit comments

Comments
 (0)