-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEfcptConfigGenerator.cs
More file actions
299 lines (256 loc) · 10.3 KB
/
EfcptConfigGenerator.cs
File metadata and controls
299 lines (256 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
namespace JD.Efcpt.Build.Tasks.Config;
/// <summary>
/// Generates efcpt-config.json from the EFCorePowerTools JSON schema.
/// </summary>
public static class EfcptConfigGenerator
{
private const string PrimarySchemaUrl = "https://raw.githubusercontent.com/ErikEJ/EFCorePowerTools/master/samples/efcpt-config.schema.json";
private const string FallbackSchemaUrl = "https://raw.githubusercontent.com/JerrettDavis/JD.Efcpt.Build/refs/heads/main/lib/efcpt-config.schema.json";
/// <summary>
/// Generates a default efcpt-config.json from a schema URL.
/// </summary>
/// <param name="schemaUrl">URL to the schema (optional, tries primary then fallback)</param>
/// <param name="dbContextName">Optional custom DbContext name (default: "ApplicationDbContext")</param>
/// <param name="rootNamespace">Optional custom root namespace (default: "EfcptProject")</param>
/// <returns>Generated JSON string</returns>
public static async Task<string> GenerateFromUrlAsync(
string? schemaUrl = null,
string? dbContextName = null,
string? rootNamespace = null)
{
schemaUrl ??= await TryGetSchemaUrlAsync();
using var client = new HttpClient();
var schemaJson = await client.GetStringAsync(schemaUrl);
return GenerateFromSchema(schemaJson, dbContextName, rootNamespace, schemaUrl);
}
/// <summary>
/// Tries to fetch schema from primary URL, falling back to secondary if needed.
/// </summary>
private static async Task<string> TryGetSchemaUrlAsync()
{
using var client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(5);
try
{
await client.GetStringAsync(PrimarySchemaUrl);
return PrimarySchemaUrl;
}
catch
{
return FallbackSchemaUrl;
}
}
/// <summary>
/// Generates a default efcpt-config.json from a local schema file.
/// </summary>
/// <param name="schemaPath">Path to the schema file</param>
/// <param name="dbContextName">Optional custom DbContext name (default: "ApplicationDbContext")</param>
/// <param name="rootNamespace">Optional custom root namespace (default: "EfcptProject")</param>
/// <param name="schemaUrl">Optional schema URL to include in $schema property (default: primary schema URL)</param>
/// <returns>Generated JSON string</returns>
public static string GenerateFromFile(
string schemaPath,
string? dbContextName = null,
string? rootNamespace = null,
string? schemaUrl = null)
{
var schemaJson = File.ReadAllText(schemaPath);
schemaUrl ??= PrimarySchemaUrl;
return GenerateFromSchema(schemaJson, dbContextName, rootNamespace, schemaUrl);
}
/// <summary>
/// Generates a default efcpt-config.json from schema JSON string.
/// </summary>
/// <param name="schemaJson">The JSON schema as a string</param>
/// <param name="dbContextName">Optional custom DbContext name (default: "ApplicationDbContext")</param>
/// <param name="rootNamespace">Optional custom root namespace (default: "EfcptProject")</param>
/// <param name="schemaUrl">Optional schema URL to include in $schema property (default: primary schema URL)</param>
/// <returns>Generated JSON string</returns>
public static string GenerateFromSchema(
string schemaJson,
string? dbContextName = null,
string? rootNamespace = null,
string? schemaUrl = null)
{
var schema = JsonNode.Parse(schemaJson);
if (schema is null)
throw new InvalidOperationException("Failed to parse schema JSON");
var config = new JsonObject();
// Add $schema property first
schemaUrl ??= PrimarySchemaUrl;
config["$schema"] = schemaUrl;
var definitions = schema["definitions"]?.AsObject();
if (definitions is null)
throw new InvalidOperationException("Schema does not contain definitions section");
// Process each top-level section - only required properties
ProcessCodeGeneration(config, definitions);
ProcessFileLayout(config, definitions);
ProcessNames(config, definitions, dbContextName, rootNamespace);
// Don't process TypeMappings as it's not required
// Serialize with indentation
var options = new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
return JsonSerializer.Serialize(config, options);
}
private static void ProcessCodeGeneration(JsonObject config, JsonObject definitions)
{
var codeGenDef = definitions["CodeGeneration"]?.AsObject();
if (codeGenDef is null) return;
var required = GetRequiredProperties(codeGenDef);
var properties = codeGenDef["properties"]?.AsObject();
if (properties is null) return;
var codeGenConfig = new JsonObject();
// Process only required properties
foreach (var propName in required)
{
// Skip preview properties
if (propName.Contains("-preview", StringComparison.OrdinalIgnoreCase))
continue;
var propDef = properties[propName]?.AsObject();
if (propDef is null) continue;
if (TryGetDefaultValue(propDef, propName, out var defaultValue))
{
codeGenConfig[propName] = defaultValue;
}
}
if (codeGenConfig.Count > 0)
{
config["code-generation"] = codeGenConfig;
}
}
private static void ProcessNames(
JsonObject config,
JsonObject definitions,
string? dbContextName,
string? rootNamespace)
{
var namesDef = definitions["Names"]?.AsObject();
if (namesDef is null) return;
var required = GetRequiredProperties(namesDef);
var properties = namesDef["properties"]?.AsObject();
if (properties is null) return;
var namesConfig = new JsonObject();
// Process only required properties
foreach (var propName in required)
{
// Skip preview properties
if (propName.Contains("-preview", StringComparison.OrdinalIgnoreCase))
continue;
// Use custom values if provided
if (propName == "dbcontext-name" && !string.IsNullOrEmpty(dbContextName))
{
namesConfig[propName] = dbContextName;
}
else if (propName == "root-namespace" && !string.IsNullOrEmpty(rootNamespace))
{
namesConfig[propName] = rootNamespace;
}
else
{
var propDef = properties[propName]?.AsObject();
if (propDef is null) continue;
if (TryGetDefaultValue(propDef, propName, out var defaultValue))
{
namesConfig[propName] = defaultValue!;
}
else
{
// Provide sensible defaults for required string properties
if (propName == "dbcontext-name")
namesConfig[propName] = "ApplicationDbContext";
else if (propName == "root-namespace")
namesConfig[propName] = "EfcptProject";
}
}
}
if (namesConfig.Count > 0)
{
config["names"] = namesConfig;
}
}
private static void ProcessFileLayout(JsonObject config, JsonObject definitions)
{
var fileLayoutDef = definitions["FileLayout"]?.AsObject();
if (fileLayoutDef is null) return;
var required = GetRequiredProperties(fileLayoutDef);
var properties = fileLayoutDef["properties"]?.AsObject();
if (properties is null) return;
var fileLayoutConfig = new JsonObject();
// Process only required properties
foreach (var propName in required)
{
// Skip preview properties
if (propName.Contains("-preview", StringComparison.OrdinalIgnoreCase))
continue;
var propDef = properties[propName]?.AsObject();
if (propDef is null) continue;
if (TryGetDefaultValue(propDef, propName, out var defaultValue))
{
fileLayoutConfig[propName] = defaultValue;
}
}
if (fileLayoutConfig.Count > 0)
{
config["file-layout"] = fileLayoutConfig;
}
}
private static List<string> GetRequiredProperties(JsonObject definition)
{
var requiredArray = definition["required"]?.AsArray();
if (requiredArray is null)
return new List<string>();
return requiredArray
.Select(item => item?.GetValue<string>())
.Where(s => s is not null)
.Cast<string>()
.ToList();
}
private static bool TryGetDefaultValue(JsonObject propertyDef, string propertyName, out JsonNode? defaultValue)
{
// Check if there's an explicit default value
if (propertyDef.TryGetPropertyValue("default", out defaultValue) && defaultValue is not null)
{
defaultValue = defaultValue.DeepClone();
return true;
}
// Check type to determine implicit defaults
var type = propertyDef["type"];
if (type is null)
{
defaultValue = null;
return false;
}
// Handle type as string
if (type is JsonValue typeValue)
{
var typeStr = typeValue.GetValue<string>();
if (typeStr == "boolean")
{
defaultValue = JsonValue.Create(false);
return true;
}
defaultValue = null;
return false;
}
// Handle type as array (e.g., ["string", "null"]) - nullable types
if (type is JsonArray typeArray)
{
// Return null for nullable properties
defaultValue = JsonValue.Create<string?>(null);
return true;
}
defaultValue = null;
return false;
}
}