Skip to content

Commit

Permalink
LoadConfigAsync. (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
chullybun authored Mar 10, 2024
1 parent a55a3ef commit 39e5feb
Show file tree
Hide file tree
Showing 23 changed files with 124 additions and 148 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Represents the **NuGet** versions.

## v2.1.0
- *Enhancement:* Added `CodeGenerator.LoadConfigAsync` to enable the loading of the configuration without having to execute the code generation. This is useful when needing to either further validate the configuration prior to execution, or be able to query the configuration without initiating the code generation.
- *Fixed:* All dependencies updated to the latest version.

## v2.0.0
- *Enhancement:* **Breaking change** - underlying JSON serialization has been changed from `Newtonsoft.Json` to `System.Text.Json`, with new `Utility.JsonSerializer` encapsulating logic to enable. The following steps are required to migrate existing usage:
- Rename all attribute references from `JsonProperty` to `JsonPropertyName`.
Expand Down
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Attribute | Description
[`CodeGenPropertyAttribute`](./src/OnRamp/Config/CodeGenPropertyAttribute.cs) | Defines validation (`IsMandatory`, `IsUnique` and `Options`) and documentation for a property (non-collection).
[`CodeGenPropertyCollectionAttribute`](./src/OnRamp/Config/CodeGenPropertyCollectionAttribute.cs) | Defines validation (`IsMandatory`) and documentation for a collection property.

The configuration must also use the [Newtonsoft Json.NET serializer attributes](https://www.newtonsoft.com/json/help/html/SerializationAttributes.htm) as [Json.NET](https://www.newtonsoft.com/json/help) is used internally to perform all JSON deserialization.
The configuration must also use the `System.Text.Json` [serializer attributes](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/customize-properties) as [`System.Text.Json`](https://learn.microsoft.com/en-us/dotnet/standard/serialization/) is used internally to perform all JSON deserialization.

<br/>

Expand All @@ -85,17 +85,16 @@ The configuration must also use the [Newtonsoft Json.NET serializer attributes](
An example is as follows:

``` csharp
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
[CodeGenClass("Entity", Title = "'Entity' object.", Description = "The `Entity` object.", Markdown = "This is a _sample_ markdown.", ExampleMarkdown = "This is an `example` markdown.")]
[CodeGenCategory("Key", Title = "Provides the _Key_ configuration.")]
[CodeGenCategory("Collection", Title = "Provides related child (hierarchical) configuration.")]
public class EntityConfig : ConfigRootBase<EntityConfig>
{
[JsonProperty("name")]
[JsonPropertyName("name")]
[CodeGenProperty("Key", Title = "The entity name.", IsMandatory = true)]
public string? Name { get; set; }

[JsonProperty("properties")]
[JsonPropertyName("properties")]
[CodeGenPropertyCollection("Collection", Title = "The `Property` collection.", IsImportant = true)]
public List<PropertyConfig>? Properties { get; set; }

Expand All @@ -105,22 +104,21 @@ public class EntityConfig : ConfigRootBase<EntityConfig>
}
}

[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
[CodeGenClass("Property", Title = "'Property' object.", Description = "The `Property` object.")]
[CodeGenCategory("Key", Title = "Provides the _Key_ configuration.")]
public class PropertyConfig : ConfigBase<EntityConfig, EntityConfig>
{
public override string QualifiedKeyName => BuildQualifiedKeyName("Property", Name);

[JsonProperty("name")]
[JsonPropertyName("name")]
[CodeGenProperty("Key", Title = "The property name.", IsMandatory = true, IsUnique = true)]
public string? Name { get; set; }

[JsonProperty("type")]
[JsonPropertyName("type")]
[CodeGenProperty("Key", Title = "The property type.", Description = "This is a more detailed description for the property type.", IsImportant = true, Options = new string[] { "string", "int", "decimal" })]
public string? Type { get; set; }

[JsonProperty("isNullable")]
[JsonPropertyName("isNullable")]
[CodeGenProperty("Key", Title = "Indicates whether the property is nullable.")]
public bool? IsNullable { get; set; }

Expand Down
34 changes: 11 additions & 23 deletions src/OnRamp/CodeGenOutputArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,36 @@ namespace OnRamp
/// <summary>
/// The resulting <see cref="CodeGenOutputArgs"/> arguments.
/// </summary>
public class CodeGenOutputArgs
/// <param name="script">The corresponding <see cref="CodeGenScriptItem"/>.</param>
/// <param name="directoryName">The optional generated directory name.</param>
/// <param name="fileName">The generated file name.</param>
/// <param name="genOncePattern">The generated gen-once file name.</param>
/// <param name="content">The generated content.</param>
public class CodeGenOutputArgs(CodeGenScriptItem script, string? directoryName, string fileName, string? genOncePattern, string? content)
{
/// <summary>
/// Initializes a new instance of the <see cref="CodeGenOutputArgs"/> class.
/// </summary>
/// <param name="script">The corresponding <see cref="CodeGenScriptItem"/>.</param>
/// <param name="directoryName">The optional generated directory name.</param>
/// <param name="fileName">The generated file name.</param>
/// <param name="genOncePattern">The generated gen-once file name.</param>
/// <param name="content">The generated content.</param>
public CodeGenOutputArgs(CodeGenScriptItem script, string? directoryName, string fileName, string? genOncePattern, string? content)
{
Script = script ?? throw new ArgumentNullException(nameof(script));
DirectoryName = directoryName;
FileName = string.IsNullOrEmpty(fileName) ? throw new ArgumentNullException(nameof(fileName)) : fileName;
GenOncePattern = genOncePattern;
Content = content;
}

/// <summary>
/// Gets the <see cref="CodeGenScriptItem"/>.
/// </summary>
public CodeGenScriptItem Script { get; }
public CodeGenScriptItem Script { get; } = script ?? throw new ArgumentNullException(nameof(script));

/// <summary>
/// Gets the optional generated directory name.
/// </summary>
public string? DirectoryName { get; }
public string? DirectoryName { get; } = directoryName;

/// <summary>
/// Gets the generated file name.
/// </summary>
public string FileName { get; }
public string FileName { get; } = string.IsNullOrEmpty(fileName) ? throw new ArgumentNullException(nameof(fileName)) : fileName;

/// <summary>
/// Gets the generated content.
/// </summary>
public string? Content { get; }
public string? Content { get; } = content;

/// <summary>
/// Gets the gen-once file name pattern (where specified).
/// </summary>
public string? GenOncePattern { get; }
public string? GenOncePattern { get; } = genOncePattern;
}
}
69 changes: 48 additions & 21 deletions src/OnRamp/CodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public static async Task<TCodeGenerator> CreateAsync<TCodeGenerator>(ICodeGenera
/// </summary>
private static async Task<CodeGenScript> LoadScriptsAsync(ICodeGeneratorArgs args)
{
var r = StreamLocator.GetScriptStreamReader(args.ScriptFileName ?? throw new CodeGenException("Script file name must be specified."), args.Assemblies.ToArray(), StreamLocator.YamlJsonExtensions);
var r = StreamLocator.GetScriptStreamReader(args.ScriptFileName ?? throw new CodeGenException("Script file name must be specified."), [.. args.Assemblies], StreamLocator.YamlJsonExtensions);
using var s = r.StreamReader ?? throw new CodeGenException($"Script '{args.ScriptFileName}' does not exist.");
args.ScriptFileName = r.FileName;
return await LoadScriptStreamAsync(args, null, args.ScriptFileName, s).ConfigureAwait(false);
Expand Down Expand Up @@ -84,7 +84,7 @@ private static async Task<CodeGenScript> LoadScriptStreamAsync(ICodeGeneratorArg
{
foreach (var ifn in scripts.Inherits)
{
using var s = StreamLocator.GetScriptStreamReader(ifn, args.Assemblies.ToArray(), StreamLocator.YamlJsonExtensions).StreamReader ?? throw new CodeGenException($"Script '{ifn}' does not exist.");
using var s = StreamLocator.GetScriptStreamReader(ifn, [.. args.Assemblies], StreamLocator.YamlJsonExtensions).StreamReader ?? throw new CodeGenException($"Script '{ifn}' does not exist.");
var inherit = await LoadScriptStreamAsync(args, rootScript, ifn, s).ConfigureAwait(false);
foreach (var iscript in inherit.Generators!)
{
Expand Down Expand Up @@ -131,35 +131,28 @@ protected CodeGenerator(ICodeGeneratorArgs args, CodeGenScript scripts)
public ICodeGeneratorArgs CodeGenArgs { get; }

/// <summary>
/// Execute the code-generation; loads the configuration file and executes each of the scripted templates.
/// Loads the <see cref="IRootConfig"/> <see cref="ConfigBase"/> from the specified <paramref name="configFileName"/>.
/// </summary>
/// <param name="configFileName">The filename (defaults to <see cref="CodeGenArgs"/>) to load the content from the file system (primary) or <see cref="ICodeGeneratorArgs.Assemblies"/> (secondary, recursive until found).</param>
/// <returns>The resultant <see cref="CodeGenStatistics"/>.</returns>
/// <exception cref="CodeGenException">Thrown when an error is encountered during the code-generation.</exception>
/// <exception cref="CodeGenChangesFoundException">Thrown where the code-generation would result in changes to an underlying artefact. This is managed by setting <see cref="ICodeGeneratorArgs.ExpectNoChanges"/> to <c>true</c>.</exception>
public async Task<CodeGenStatistics> GenerateAsync(string? configFileName = null)
/// <param name="configFileName">The configuration file name.</param>
/// <returns>The <see cref="IRootConfig"/> <see cref="ConfigBase"/>.</returns>
public async Task<ConfigBase> LoadConfigAsync(string? configFileName = null)
{
var fn = configFileName ?? CodeGenArgs.ConfigFileName ?? throw new CodeGenException("Config file must be specified.");
var r = StreamLocator.GetStreamReader(fn, null, CodeGenArgs.Assemblies.ToArray());
var r = StreamLocator.GetStreamReader(fn, null, [.. CodeGenArgs.Assemblies]);
using var sr = r.StreamReader ?? throw new CodeGenException($"Config '{fn}' does not exist.");
return await GenerateAsync(fn, sr, StreamLocator.GetContentType(r.FileName)).ConfigureAwait(false);
return await LoadConfigAsync(sr, StreamLocator.GetContentType(r.FileName), r.FileName).ConfigureAwait(false);
}

/// <summary>
/// Execute the code-generation; loads the configuration from the <paramref name="configReader"/> and executes each of the scripted templates.
/// Loads the <see cref="IRootConfig"/> <see cref="ConfigBase"/> from the specified <paramref name="configReader"/>.
/// </summary>
/// <param name="configReader">The <see cref="TextReader"/> containing the configuration.</param>
/// <param name="contentType">The corresponding <see cref="StreamContentType"/>.</param>
/// <param name="configFileName">The optional configuration file name used specifically in error messages.</param>
/// <returns>The resultant <see cref="CodeGenStatistics"/>.</returns>
/// <exception cref="CodeGenException">Thrown when an error is encountered during the code-generation.</exception>
/// <exception cref="CodeGenChangesFoundException">Thrown where the code-generation would result in changes to an underlying artefact. This is managed by setting <see cref="ICodeGeneratorArgs.ExpectNoChanges"/> to <c>true</c>.</exception>
public async Task<CodeGenStatistics> GenerateAsync(TextReader configReader, StreamContentType contentType, string configFileName = "<stream>") => await GenerateAsync(configFileName, configReader, contentType).ConfigureAwait(false);

/// <summary>
/// Executes the code-generation.
/// </summary>
private async Task<CodeGenStatistics> GenerateAsync(string configFileName, TextReader configReader, StreamContentType contentType)
public async Task<ConfigBase> LoadConfigAsync(TextReader configReader, StreamContentType contentType, string configFileName = "<stream>")
{
ConfigBase? config;
IRootConfig rootConfig;
Expand Down Expand Up @@ -199,11 +192,45 @@ private async Task<CodeGenStatistics> GenerateAsync(string configFileName, TextR
{
await ce.AfterPrepareAsync(rootConfig).ConfigureAwait(false);
}

return config;
}
catch (CodeGenException cgex)
{
throw new CodeGenException($"Config '{configFileName}' is invalid: {cgex.Message}");
}
}

/// <summary>
/// Execute the code-generation; loads the configuration file and executes each of the scripted templates.
/// </summary>
/// <param name="configFileName">The filename (defaults to <see cref="CodeGenArgs"/>) to load the content from the file system (primary) or <see cref="ICodeGeneratorArgs.Assemblies"/> (secondary, recursive until found).</param>
/// <returns>The resultant <see cref="CodeGenStatistics"/>.</returns>
/// <exception cref="CodeGenException">Thrown when an error is encountered during the code-generation.</exception>
/// <exception cref="CodeGenChangesFoundException">Thrown where the code-generation would result in changes to an underlying artefact. This is managed by setting <see cref="ICodeGeneratorArgs.ExpectNoChanges"/> to <c>true</c>.</exception>
public async Task<CodeGenStatistics> GenerateAsync(string? configFileName = null)
=> await GenerateAsync(await LoadConfigAsync(configFileName).ConfigureAwait(false)).ConfigureAwait(false);

/// <summary>
/// Execute the code-generation; loads the configuration from the <paramref name="configReader"/> and executes each of the scripted templates.
/// </summary>
/// <param name="configReader">The <see cref="TextReader"/> containing the configuration.</param>
/// <param name="contentType">The corresponding <see cref="StreamContentType"/>.</param>
/// <param name="configFileName">The optional configuration file name used specifically in error messages.</param>
/// <returns>The resultant <see cref="CodeGenStatistics"/>.</returns>
/// <exception cref="CodeGenException">Thrown when an error is encountered during the code-generation.</exception>
/// <exception cref="CodeGenChangesFoundException">Thrown where the code-generation would result in changes to an underlying artefact. This is managed by setting <see cref="ICodeGeneratorArgs.ExpectNoChanges"/> to <c>true</c>.</exception>
public async Task<CodeGenStatistics> GenerateAsync(TextReader configReader, StreamContentType contentType, string configFileName = "<stream>")
=> await GenerateAsync(await LoadConfigAsync(configReader, contentType, configFileName).ConfigureAwait(false)).ConfigureAwait(false);

/// <summary>
/// Executes the code-generation for the specific <paramref name="config"/>.
/// </summary>
/// <param name="config">The <see cref="IRootConfig"/> <see cref="ConfigBase"/>.</param>
public Task<CodeGenStatistics> GenerateAsync(ConfigBase config)
{
if (config is not IRootConfig rootConfig)
throw new ArgumentException("Configuration must implement IRootConfig.", nameof(config));

// Generate the scripted artefacts.
var overallStopwatch = Stopwatch.StartNew();
Expand Down Expand Up @@ -232,7 +259,7 @@ private async Task<CodeGenStatistics> GenerateAsync(string configFileName, TextR

overallStopwatch.Stop();
overallStats.ElapsedMilliseconds = overallStopwatch.ElapsedMilliseconds;
return overallStats;
return Task.FromResult(overallStats);
}

/// <summary>
Expand Down Expand Up @@ -268,7 +295,7 @@ protected virtual void OnCodeGenerated(CodeGenOutputArgs outputArgs, CodeGenStat
else
{
// Perform a wildcard search and stop code-gen where any matches.
if (di.GetFiles(outputArgs.GenOncePattern).Any())
if (di.GetFiles(outputArgs.GenOncePattern).Length != 0)
return;
}
}
Expand Down Expand Up @@ -314,7 +341,7 @@ protected virtual void OnCodeGenerated(CodeGenOutputArgs outputArgs, CodeGenStat
private static string[] ConvertContentIntoLines(string? content)
{
if (content is null)
return Array.Empty<string>();
return [];

string line;
var lines = new List<string>();
Expand All @@ -324,7 +351,7 @@ private static string[] ConvertContentIntoLines(string? content)
lines.Add(line);
}

return lines.ToArray();
return [.. lines];
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions src/OnRamp/CodeGeneratorArgsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ public abstract class CodeGeneratorArgsBase : CodeGeneratorDbArgsBase, ICodeGene
public DirectoryInfo? OutputDirectory { get; set; }

/// <inheritdoc/>
public List<Assembly> Assemblies { get; } = new List<Assembly>();
public List<Assembly> Assemblies { get; } = [];

/// <inheritdoc/>
public Dictionary<string, object?> Parameters { get; } = new Dictionary<string, object?>();
public Dictionary<string, object?> Parameters { get; } = [];

/// <inheritdoc/>
public ILogger? Logger { get; set; }
Expand Down
11 changes: 3 additions & 8 deletions src/OnRamp/Config/CodeGenCategoryAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,14 @@ namespace OnRamp.Config
/// <summary>
/// Represents the <i>code-generation</i> class category configuration.
/// </summary>
/// <param name="category">The grouping category name.</param>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class CodeGenCategoryAttribute : Attribute
public sealed class CodeGenCategoryAttribute(string category) : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="CodeGenCategoryAttribute"/> class.
/// </summary>
/// <param name="category">The grouping category name.</param>
public CodeGenCategoryAttribute(string category) => Category = category;

/// <summary>
/// Gets or sets the category name.
/// </summary>
public string Category { get; }
public string Category { get; } = category;

/// <summary>
/// Gets or sets the title.
Expand Down
11 changes: 3 additions & 8 deletions src/OnRamp/Config/CodeGenClassAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,14 @@ namespace OnRamp.Config
/// <summary>
/// Represents the <i>code-generation</i> class configuration.
/// </summary>
/// <param name="name">The class name.</param>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class CodeGenClassAttribute : Attribute
public sealed class CodeGenClassAttribute(string name) : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="CodeGenClassAttribute"/> class.
/// </summary>
/// <param name="name">The class name.</param>
public CodeGenClassAttribute(string name) => Name = name ?? throw new ArgumentNullException(nameof(name));

/// <summary>
/// Gets the class name.
/// </summary>
public string Name { get; }
public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name));

/// <summary>
/// Gets or sets the title.
Expand Down
Loading

0 comments on commit 39e5feb

Please sign in to comment.