diff --git a/src/Loom.Templates/templates/loom-api/.template.config/template.json b/src/Loom.Templates/templates/loom-api/.template.config/template.json index 225b04e..309f1dd 100644 --- a/src/Loom.Templates/templates/loom-api/.template.config/template.json +++ b/src/Loom.Templates/templates/loom-api/.template.config/template.json @@ -15,10 +15,29 @@ "language": "C#", "type": "solution" }, - "sourceName": "MyApp", "preferNameDirectory": true, "defaultName": "MyApp", "symbols": { + "safeName": { + "type": "generated", + "generator": "regex", + "dataType": "string", + "replaces": "MyApp", + "fileRename": "MyApp", + "parameters": { + "source": "name", + "steps": [ + { + "regex": "[^A-Za-z0-9_.]", + "replacement": "_" + }, + { + "regex": "(^|\\.)([0-9])", + "replacement": "$1_$2" + } + ] + } + }, "userSecretsId": { "type": "generated", "generator": "guid", @@ -38,6 +57,10 @@ { "regex": "[^A-Za-z0-9_]", "replacement": "_" + }, + { + "regex": "^([0-9])", + "replacement": "_$1" } ] } diff --git a/src/Loom.Templates/templates/loom-api/src/MyApp.Api/Features/Widgets/WidgetConfiguration.cs b/src/Loom.Templates/templates/loom-api/src/MyApp.Api/Features/Widgets/WidgetConfiguration.cs index 9edbdb5..66fa536 100644 --- a/src/Loom.Templates/templates/loom-api/src/MyApp.Api/Features/Widgets/WidgetConfiguration.cs +++ b/src/Loom.Templates/templates/loom-api/src/MyApp.Api/Features/Widgets/WidgetConfiguration.cs @@ -14,7 +14,7 @@ namespace MyApp.Api.Features.Widgets; /// internal sealed class WidgetConfiguration : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + void IEntityTypeConfiguration.Configure(EntityTypeBuilder builder) { ArgumentNullException.ThrowIfNull(builder); diff --git a/src/Loom.Templates/templates/loom-api/src/MyApp.Api/Infrastructure/AppDbContext.cs b/src/Loom.Templates/templates/loom-api/src/MyApp.Api/Infrastructure/AppDbContext.cs index cb26654..f35f8cd 100644 --- a/src/Loom.Templates/templates/loom-api/src/MyApp.Api/Infrastructure/AppDbContext.cs +++ b/src/Loom.Templates/templates/loom-api/src/MyApp.Api/Infrastructure/AppDbContext.cs @@ -9,6 +9,7 @@ namespace MyApp.Api.Infrastructure; /// public sealed class AppDbContext(DbContextOptions options) : DbContext(options) { + /// Gets the widgets. Only aggregate roots get a set of their own. public DbSet Widgets => Set(); // Conventions, not model creation. Property discovery skips types it does not recognise, so an diff --git a/src/Loom.Templates/templates/loom-api/src/MyApp.Api/Infrastructure/DesignTimeDbContextFactory.cs b/src/Loom.Templates/templates/loom-api/src/MyApp.Api/Infrastructure/DesignTimeDbContextFactory.cs index 8dd2981..996fbb7 100644 --- a/src/Loom.Templates/templates/loom-api/src/MyApp.Api/Infrastructure/DesignTimeDbContextFactory.cs +++ b/src/Loom.Templates/templates/loom-api/src/MyApp.Api/Infrastructure/DesignTimeDbContextFactory.cs @@ -12,7 +12,7 @@ namespace MyApp.Api.Infrastructure; /// internal sealed class DesignTimeDbContextFactory : IDesignTimeDbContextFactory { - public AppDbContext CreateDbContext(string[] args) + AppDbContext IDesignTimeDbContextFactory.CreateDbContext(string[] args) { DbContextOptionsBuilder builder = new(); builder.UseNpgsql("Host=design-time;Database=designtime;Username=none;Password=none"); diff --git a/src/Loom.Templates/templates/loom-worker/.template.config/template.json b/src/Loom.Templates/templates/loom-worker/.template.config/template.json index b9db89b..d2e2550 100644 --- a/src/Loom.Templates/templates/loom-worker/.template.config/template.json +++ b/src/Loom.Templates/templates/loom-worker/.template.config/template.json @@ -15,10 +15,29 @@ "language": "C#", "type": "solution" }, - "sourceName": "MyApp", "preferNameDirectory": true, "defaultName": "MyApp", "symbols": { + "safeName": { + "type": "generated", + "generator": "regex", + "dataType": "string", + "replaces": "MyApp", + "fileRename": "MyApp", + "parameters": { + "source": "name", + "steps": [ + { + "regex": "[^A-Za-z0-9_.]", + "replacement": "_" + }, + { + "regex": "(^|\\.)([0-9])", + "replacement": "$1_$2" + } + ] + } + }, "userSecretsId": { "type": "generated", "generator": "guid", @@ -38,6 +57,10 @@ { "regex": "[^A-Za-z0-9_]", "replacement": "_" + }, + { + "regex": "^([0-9])", + "replacement": "_$1" } ] } diff --git a/src/Loom.Templates/templates/loom-worker/src/MyApp.Worker/Features/Widgets/WidgetConfiguration.cs b/src/Loom.Templates/templates/loom-worker/src/MyApp.Worker/Features/Widgets/WidgetConfiguration.cs index b0a1129..1633b28 100644 --- a/src/Loom.Templates/templates/loom-worker/src/MyApp.Worker/Features/Widgets/WidgetConfiguration.cs +++ b/src/Loom.Templates/templates/loom-worker/src/MyApp.Worker/Features/Widgets/WidgetConfiguration.cs @@ -13,7 +13,7 @@ namespace MyApp.Worker.Features.Widgets; /// internal sealed class WidgetConfiguration : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + void IEntityTypeConfiguration.Configure(EntityTypeBuilder builder) { ArgumentNullException.ThrowIfNull(builder); diff --git a/src/Loom.Templates/templates/loom-worker/src/MyApp.Worker/Infrastructure/AppDbContext.cs b/src/Loom.Templates/templates/loom-worker/src/MyApp.Worker/Infrastructure/AppDbContext.cs index fa26804..69be7ed 100644 --- a/src/Loom.Templates/templates/loom-worker/src/MyApp.Worker/Infrastructure/AppDbContext.cs +++ b/src/Loom.Templates/templates/loom-worker/src/MyApp.Worker/Infrastructure/AppDbContext.cs @@ -9,6 +9,7 @@ namespace MyApp.Worker.Infrastructure; /// public sealed class AppDbContext(DbContextOptions options) : DbContext(options) { + /// Gets the widgets. Only aggregate roots get a set of their own. public DbSet Widgets => Set(); // Conventions, not model creation. Property discovery skips types it does not recognise, so an diff --git a/src/Loom.Templates/templates/loom-worker/src/MyApp.Worker/Infrastructure/DesignTimeDbContextFactory.cs b/src/Loom.Templates/templates/loom-worker/src/MyApp.Worker/Infrastructure/DesignTimeDbContextFactory.cs index e3071d2..4cf4a78 100644 --- a/src/Loom.Templates/templates/loom-worker/src/MyApp.Worker/Infrastructure/DesignTimeDbContextFactory.cs +++ b/src/Loom.Templates/templates/loom-worker/src/MyApp.Worker/Infrastructure/DesignTimeDbContextFactory.cs @@ -12,7 +12,7 @@ namespace MyApp.Worker.Infrastructure; /// internal sealed class DesignTimeDbContextFactory : IDesignTimeDbContextFactory { - public AppDbContext CreateDbContext(string[] args) + AppDbContext IDesignTimeDbContextFactory.CreateDbContext(string[] args) { DbContextOptionsBuilder builder = new(); builder.UseNpgsql("Host=design-time;Database=designtime;Username=none;Password=none"); diff --git a/tests/Loom.Templates.Tests/TemplateManifestTests.cs b/tests/Loom.Templates.Tests/TemplateManifestTests.cs index bcd5a3e..bb4157d 100644 --- a/tests/Loom.Templates.Tests/TemplateManifestTests.cs +++ b/tests/Loom.Templates.Tests/TemplateManifestTests.cs @@ -33,7 +33,7 @@ public async Task The_Manifest_Is_Valid_Json_With_The_Members_Dotnet_New_Require { JsonElement manifest = await ReadManifestAsync(template); - foreach (string member in (string[])["identity", "name", "shortName", "sourceName"]) + foreach (string member in (string[])["identity", "name", "shortName"]) { await Assert.That(manifest.TryGetProperty(member, out JsonElement value) && value.ValueKind is JsonValueKind.String && @@ -53,12 +53,22 @@ public async Task The_Short_Name_Matches_The_Directory(string template) [Test] [MethodDataSource(nameof(Templates))] - public async Task The_Source_Name_Occurs_In_The_Content_It_Renames(string template) + public async Task Exactly_One_Symbol_Renames_Files(string template) { JsonElement manifest = await ReadManifestAsync(template); - string sourceName = manifest.GetProperty("sourceName").GetString()!; - await Assert.That(await OccurrencesAsync(template, sourceName)).IsGreaterThan(0); + string[] renamers = + [ + .. manifest.GetProperty("symbols").EnumerateObject() + .Where(symbol => symbol.Value.TryGetProperty("fileRename", out _)) + .Select(symbol => symbol.Name), + ]; + + // The token is renamed and replaced by one symbol rather than by sourceName, so that paths and + // file contents get the same sanitised value. A name like "My-App" is a valid directory and an + // invalid identifier; two mechanisms would disagree about which to write where. + await Assert.That(renamers.Length).IsEqualTo(1); + await Assert.That(manifest.TryGetProperty("sourceName", out _)).IsFalse(); } [Test]