8
8
using Microsoft . CodeAnalysis . CSharp ;
9
9
using Microsoft . CodeAnalysis . CSharp . Syntax ;
10
10
using Microsoft . CodeAnalysis . Text ;
11
- using Microsoft . CodeAnalysis . Emit ;
12
- using System . Reflection ;
13
11
using Pluralize . NET ;
14
12
using JsonByExampleGenerator . Generator . Models ;
15
13
using System . Globalization ;
@@ -35,6 +33,31 @@ public void Execute(GeneratorExecutionContext context)
35
33
{
36
34
try
37
35
{
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
+
38
61
// Resolve all json files that are added to the AdditionalFiles in the compilation
39
62
foreach ( var jsonFile in context . AdditionalFiles . Where ( f => f . Path . EndsWith ( ".json" , StringComparison . InvariantCultureIgnoreCase ) ) )
40
63
{
@@ -44,26 +67,14 @@ public void Execute(GeneratorExecutionContext context)
44
67
continue ;
45
68
}
46
69
47
- // Determine if the functionality for easy access to configuration should be enabled
48
- bool configEnabled = IsConfigurationEnabled ( context ) ;
49
-
50
70
var json = JsonDocument . Parse ( jsonFileText . ToString ( ) ) ;
51
71
52
- // The namespace of the code is determined by the assembly name of the compilation
53
- string namespaceName = context . Compilation ? . AssemblyName ?? "JsonByExample" ;
54
-
55
72
// Read the json and build a list of models that can be used to generate classes
56
73
var classModels = new List < ClassModel > ( ) ;
57
74
var jsonElement = json . RootElement ;
58
75
string rootTypeName = GetValidName ( Path . GetFileNameWithoutExtension ( jsonFile . Path ) . Replace ( " " , string . Empty ) , true ) ;
59
76
ResolveTypeRecursive ( context , classModels , jsonElement , rootTypeName ) ;
60
77
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
- }
67
78
68
79
// Attempt to find a Scriban template in the AdditionalFiles that has the same name as the json
69
80
string templateFileName = $ "{ Path . GetFileNameWithoutExtension ( jsonFile . Path ) } .sbntxt";
@@ -131,7 +142,56 @@ public void Execute(GeneratorExecutionContext context)
131
142
/// <param name="compilation">The compilation, so we can find existing types</param>
132
143
private void FilterAndChangeBasedOnExistingCode ( List < ClassModel > classModels , string namespaceName , Compilation compilation )
133
144
{
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 )
135
195
{
136
196
// Find a class in the current compilation that already exists
137
197
var existingClass = compilation . GetTypeByMetadataName ( $ "{ namespaceName } .Json.{ classModel . ClassName } ") ;
@@ -217,11 +277,11 @@ private static void ResolveTypeRecursive(GeneratorExecutionContext context, List
217
277
{
218
278
if ( arrEnumerator . Current . ValueKind == JsonValueKind . Number )
219
279
{
220
- arrPropName = "double" ;
280
+ arrPropName = FindBestNumericType ( arrEnumerator . Current ) ;
221
281
}
222
282
else if ( arrEnumerator . Current . ValueKind == JsonValueKind . String )
223
283
{
224
- arrPropName = "string" ;
284
+ arrPropName = FindBestStringType ( arrEnumerator . Current ) ;
225
285
}
226
286
else if ( arrEnumerator . Current . ValueKind == JsonValueKind . True || arrEnumerator . Current . ValueKind == JsonValueKind . False )
227
287
{
@@ -247,8 +307,8 @@ private static void ResolveTypeRecursive(GeneratorExecutionContext context, List
247
307
248
308
break ;
249
309
}
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 ;
252
312
case JsonValueKind . False :
253
313
case JsonValueKind . True : propertyModel = new PropertyModel ( prop . Name , "bool" , propName ) ; break ;
254
314
case JsonValueKind . Object :
@@ -283,6 +343,53 @@ private static void ResolveTypeRecursive(GeneratorExecutionContext context, List
283
343
}
284
344
}
285
345
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
+
286
393
/// <summary>
287
394
/// Gets a name that is valid in C# and makes it Pascal-case.
288
395
/// Optionally, it can singularize the name, so that a list property has a proper model class.
0 commit comments