13
13
using Pluralize . NET ;
14
14
using JsonByExampleGenerator . Generator . Models ;
15
15
using System . Globalization ;
16
+ using Scriban ;
17
+ using JsonByExampleGenerator . Generator . Utils ;
16
18
17
19
namespace JsonByExampleGenerator . Generator
18
20
{
21
+ /// <summary>
22
+ /// Source generator that generates C# code based on an example json file.
23
+ /// </summary>
19
24
[ Generator ]
20
25
public class JsonGenerator : ISourceGenerator
21
26
{
22
27
private static readonly IPluralize _pluralizer = new Pluralizer ( ) ;
23
28
29
+ /// <summary>
30
+ /// Executes the generator logic during compilation
31
+ /// </summary>
32
+ /// <param name="context">Generator context that contains info about the compilation</param>
24
33
public void Execute ( GeneratorExecutionContext context )
25
34
{
26
35
try
27
36
{
37
+ // Resolve all json files that are added to the AdditionalFiles in the compilation
28
38
foreach ( var jsonFile in context . AdditionalFiles . Where ( f => f . Path . EndsWith ( ".json" , StringComparison . InvariantCultureIgnoreCase ) ) )
29
39
{
30
40
var jsonFileText = jsonFile . GetText ( context . CancellationToken ) ;
31
- if ( jsonFileText == null )
41
+ if ( jsonFileText == null )
32
42
{
33
43
continue ;
34
44
}
35
45
36
- bool configEnabled = context . Compilation ? . ReferencedAssemblyNames
37
- . Any ( r => string . Equals ( "Microsoft.Extensions.Configuration.Json" , r . Name , StringComparison . InvariantCulture ) )
38
- ?? false ;
39
- configEnabled = configEnabled
40
- && ( context . Compilation ? . ReferencedAssemblyNames
41
- . Any ( r => string . Equals ( "Microsoft.Extensions.Configuration.Binder" , r . Name , StringComparison . InvariantCulture ) ) ?? false ) ;
46
+ // Determine if the functionality for easy access to configuration should be enabled
47
+ bool configEnabled = IsConfigurationEnabled ( context ) ;
42
48
43
49
var json = JsonDocument . Parse ( jsonFileText . ToString ( ) ) ;
44
50
51
+ // The namespace of the code is determined by the assembly name of the compilation
45
52
string namespaceName = context . Compilation ? . AssemblyName ?? "JsonByExample" ;
46
53
54
+ // Read the json and build a list of models that can be used to generate classes
47
55
var classModels = new List < ClassModel > ( ) ;
48
56
var jsonElement = json . RootElement ;
49
57
string rootTypeName = GetValidName ( Path . GetFileNameWithoutExtension ( jsonFile . Path ) . Replace ( " " , string . Empty ) , true ) ;
50
- RenderType ( context , classModels , jsonElement , namespaceName , rootTypeName ) ;
58
+ ResolveTypeRecursive ( context , classModels , jsonElement , rootTypeName ) ;
51
59
52
- var generatedClasses = classModels . Select ( c =>
53
- {
54
- string extensionMethod = string . Empty ;
55
- string configReadMethod = string . Empty ;
56
-
57
- if ( configEnabled )
58
- {
59
-
60
- configReadMethod = $@ "
61
- public static { c . ClassName } FromConfig([System.Diagnostics.CodeAnalysis.NotNull] IConfiguration config)
62
- {{
63
- return config.Get<{ c . ClassName } >();
64
- }}" ;
65
- }
66
-
67
- string result = $@ "
68
- { extensionMethod }
69
- public partial class { c . ClassName }
70
- {{
71
- { string . Join ( "\r \n " , c . Properties . Select ( p => RenderProperty ( p ) ) ) }
72
- { configReadMethod }
73
- }}" ;
60
+ // A list of dependencies to be added to the using statements in the code generation
61
+ var optionalDependencies = new List < string > ( ) ;
62
+ if ( configEnabled )
63
+ {
64
+ optionalDependencies . Add ( "Microsoft.Extensions.Configuration" ) ;
65
+ }
74
66
75
- return result ;
76
- } ) ;
67
+ // Attempt to find a Scriban template in the AdditionalFiles that has the same name as the json
68
+ string templateFileName = $ "{ Path . GetFileNameWithoutExtension ( jsonFile . Path ) } .sbntxt";
69
+ string ? templateContent = context
70
+ . AdditionalFiles
71
+ . FirstOrDefault ( f => Path
72
+ . GetFileName ( f . Path )
73
+ . Equals ( templateFileName , StringComparison . InvariantCultureIgnoreCase ) )
74
+ ? . GetText ( context . CancellationToken )
75
+ ? . ToString ( ) ;
77
76
78
- var optionalDependencies = configEnabled ? "\r \n using Microsoft.Extensions.Configuration;" : string . Empty ;
77
+ Template template ;
78
+ if ( templateContent != null )
79
+ {
80
+ // Parse the template that is in the compilation
81
+ template = Template . Parse ( templateContent , templateFileName ) ;
82
+ }
83
+ else
84
+ {
85
+ // Fallback to the default template
86
+ const string defaultTemplatePath = "JsonByExampleTemplate.sbntxt" ;
87
+ template = Template . Parse ( EmbeddedResource . GetContent ( defaultTemplatePath ) , defaultTemplatePath ) ;
88
+ }
79
89
80
- string generatedCode = $@ "#nullable disable
81
- using System.Collections.Generic;
82
- using System.Text.Json.Serialization;{ optionalDependencies }
90
+ // Use Scriban to render the code using the model that was built
91
+ string generatedCode = template . Render ( new
92
+ {
93
+ OptionalDependencies = optionalDependencies ,
94
+ NamespaceName = namespaceName ,
95
+ ConfigEnabled = configEnabled ,
96
+ ClassModels = classModels
97
+ } , member => member . Name ) ;
83
98
84
- namespace { namespaceName } .Json
85
- {{{string.Join(" \r \n ", generatedClasses)}
86
- } }
87
- #nullable enable" ;
99
+ // Add the generated code to the compilation
88
100
context . AddSource ( $ "{ namespaceName } _{ rootTypeName } .gen.cs",
89
101
SourceText . From ( generatedCode , Encoding . UTF8 ) ) ;
90
102
}
91
103
}
92
- #pragma warning disable CA1031 // Do not catch general exception types
93
104
catch ( Exception ex )
94
- #pragma warning restore CA1031 // Do not catch general exception types
95
105
{
106
+ // Report a diagnostic if an exception occurs while generating code; allows consumers to know what is going on
96
107
string message = $ "Exception: { ex . Message } - { ex . StackTrace } ";
97
108
context . ReportDiagnostic ( Diagnostic . Create (
98
109
new DiagnosticDescriptor (
@@ -106,40 +117,61 @@ namespace {namespaceName}.Json
106
117
}
107
118
}
108
119
109
- private static string RenderProperty ( PropertyModel propertyModel )
120
+ /// <summary>
121
+ /// Find out if Microsoft.Extensions.Configuration.Json is used.
122
+ /// </summary>
123
+ /// <param name="context">The generator execution context</param>
124
+ /// <returns>True if Microsoft.Extensions.Configuration.Json is referenced from the assembly</returns>
125
+ private static bool IsConfigurationEnabled ( GeneratorExecutionContext context )
110
126
{
111
- string init = propertyModel . Init != null ? $ " = { propertyModel . Init } ;" : string . Empty ;
112
- return $ "[JsonPropertyName(\" { propertyModel . PropertyNameOriginal } \" )]\r \n public { propertyModel . PropertyType } { propertyModel . PropertyName } {{ get; set; }}{ init } ";
127
+ bool configEnabled = context . Compilation ? . ReferencedAssemblyNames
128
+ . Any ( r => string . Equals ( "Microsoft.Extensions.Configuration.Json" , r . Name , StringComparison . InvariantCulture ) )
129
+ ?? false ;
130
+ configEnabled = configEnabled
131
+ && ( context . Compilation ? . ReferencedAssemblyNames
132
+ . Any ( r => string . Equals ( "Microsoft.Extensions.Configuration.Binder" , r . Name , StringComparison . InvariantCulture ) ) ?? false ) ;
133
+ return configEnabled ;
113
134
}
114
135
115
- private static void RenderType ( GeneratorExecutionContext context , List < ClassModel > classModels , JsonElement jsonElement , string namespaceName , string typeName )
136
+ /// <summary>
137
+ /// Reads json and fills the classModels list with relevant type definitions.
138
+ /// </summary>
139
+ /// <param name="context">The source generator context</param>
140
+ /// <param name="classModels">A list that needs to be populated with resolved types</param>
141
+ /// <param name="jsonElement">The current json element that is being read</param>
142
+ /// <param name="typeName">The current type name that is being read</param>
143
+ private static void ResolveTypeRecursive ( GeneratorExecutionContext context , List < ClassModel > classModels , JsonElement jsonElement , string typeName )
116
144
{
117
145
var classModel = new ClassModel ( typeName ) ;
118
146
147
+ // Arrays should be enumerated and handled individually
119
148
if ( jsonElement . ValueKind == JsonValueKind . Array )
120
149
{
121
150
var jsonArrayEnumerator = jsonElement . EnumerateArray ( ) ;
122
151
while ( jsonArrayEnumerator . MoveNext ( ) )
123
152
{
124
- RenderType ( context , classModels , jsonArrayEnumerator . Current , namespaceName , typeName ) ;
153
+ ResolveTypeRecursive ( context , classModels , jsonArrayEnumerator . Current , typeName ) ;
125
154
}
126
155
127
156
return ;
128
157
}
129
158
159
+ // Iterate the properties of the json element, they will become model properties
130
160
foreach ( JsonProperty prop in jsonElement . EnumerateObject ( ) )
131
161
{
132
162
string propName = GetValidName ( prop . Name ) ;
133
163
if ( propName . Length > 0 )
134
164
{
135
165
PropertyModel propertyModel ;
136
166
167
+ // The json value kind of the property determines how to map it to a C# type
137
168
switch ( prop . Value . ValueKind )
138
169
{
139
170
case JsonValueKind . Array :
140
171
{
141
172
string arrPropName = GetValidName ( prop . Name , true ) ;
142
173
174
+ // Look at the first element in the array to determine the type of the array
143
175
var arrEnumerator = prop . Value . EnumerateArray ( ) ;
144
176
if ( arrEnumerator . MoveNext ( ) )
145
177
{
@@ -157,7 +189,7 @@ private static void RenderType(GeneratorExecutionContext context, List<ClassMode
157
189
}
158
190
else
159
191
{
160
- RenderType ( context , classModels , prop . Value , namespaceName , arrPropName ) ;
192
+ ResolveTypeRecursive ( context , classModels , prop . Value , arrPropName ) ;
161
193
}
162
194
163
195
propertyModel = new PropertyModel ( prop . Name , $ "IList<{ arrPropName } >", propName )
@@ -182,7 +214,9 @@ private static void RenderType(GeneratorExecutionContext context, List<ClassMode
182
214
case JsonValueKind . Object :
183
215
{
184
216
string objectPropName = GetValidName ( prop . Name , true ) ;
185
- RenderType ( context , classModels , prop . Value , namespaceName , objectPropName ) ;
217
+
218
+ // Create a separate type for objects
219
+ ResolveTypeRecursive ( context , classModels , prop . Value , objectPropName ) ;
186
220
187
221
propertyModel = new PropertyModel ( prop . Name , objectPropName , propName ) ;
188
222
break ;
@@ -196,19 +230,30 @@ private static void RenderType(GeneratorExecutionContext context, List<ClassMode
196
230
}
197
231
}
198
232
233
+ // If there is already a model defined that matches by name, then we add any new properties by merging the models
199
234
var matchingClassModel = classModels . FirstOrDefault ( c => string . Equals ( c . ClassName , classModel . ClassName , StringComparison . InvariantCulture ) ) ;
200
235
if ( matchingClassModel != null )
201
236
{
202
237
matchingClassModel . Merge ( classModel ) ;
203
238
}
204
239
else
205
240
{
241
+ // No need to merge, just add the new class model
206
242
classModels . Add ( classModel ) ;
207
243
}
208
244
}
209
245
246
+ /// <summary>
247
+ /// Gets a name that is valid in C# and makes it Pascal-case.
248
+ /// Optionally, it can singularize the name, so that a list property has a proper model class.
249
+ /// E.g. Cars will have a model type of Car.
250
+ /// </summary>
251
+ /// <param name="typeName">The type name that is possibly not valid in C#</param>
252
+ /// <param name="singularize">If true, the name will be singularized if it is plural</param>
253
+ /// <returns>A valid C# Pascal-case name</returns>
210
254
private static string GetValidName ( string typeName , bool singularize = false )
211
255
{
256
+ // Make a plural form singular using Pluralize.NET
212
257
if ( singularize && _pluralizer . IsPlural ( typeName ) )
213
258
{
214
259
typeName = _pluralizer . Singularize ( typeName ) ;
@@ -218,12 +263,14 @@ private static string GetValidName(string typeName, bool singularize = false)
218
263
bool nextCharUpper = true ;
219
264
for ( int i = 0 ; i < typeName . Length ; i ++ )
220
265
{
266
+ // Strip spaces
221
267
if ( typeName [ i ] == ' ' )
222
268
{
223
269
nextCharUpper = true ;
224
270
continue ;
225
271
}
226
272
273
+ // Pascal casing
227
274
if ( nextCharUpper )
228
275
{
229
276
nextCharUpper = false ;
@@ -240,8 +287,13 @@ private static string GetValidName(string typeName, bool singularize = false)
240
287
return new string ( newTypeName . ToArray ( ) ) ;
241
288
}
242
289
290
+ /// <summary>
291
+ /// Initialization of the generator; allows to setup visitors for syntax.
292
+ /// </summary>
293
+ /// <param name="context">Code generator context</param>
243
294
public void Initialize ( GeneratorInitializationContext context )
244
295
{
296
+ // No implementation needed here; the generator is entirely driven by use of AdditionalFiles
245
297
}
246
298
}
247
299
}
0 commit comments