From 8422485f9b23f6f63fbf52e6d43e7c9e6fcf5dc9 Mon Sep 17 00:00:00 2001 From: OGR-67 Date: Thu, 19 Mar 2026 22:54:26 +0100 Subject: [PATCH 1/2] feat: Implement DBMLWriter and related interfaces for DBML file generation --- AL2DBML.sln | 15 + .../Interfaces/IAlParser.cs | 1 + .../Interfaces/IDBMLWriter.cs | 8 + .../Interfaces/ISchemaPostProcessor.cs | 8 + src/AL2DBML.DI/AL2DBML.DI.csproj | 1 + src/AL2DBML.DI/AL2DbmlServiceExtensions.cs | 3 +- src/AL2DBML.DI/WriterServiceExtensions.cs | 15 + src/AL2DBML.Parser/AlParser.cs | 5 + src/AL2DBML.Tests/AL2DBML.Tests.csproj | 1 + src/AL2DBML.Tests/TestBase.cs | 2 + src/AL2DBML.Tests/Writer/WriterTests.cs | 454 ++++++++++++++++++ src/DBMLWriter/DBMLWriter.csproj | 14 + src/DBMLWriter/SchemaPostProcessor.cs | 91 ++++ src/DBMLWriter/Writer.cs | 76 +++ 14 files changed, 693 insertions(+), 1 deletion(-) create mode 100644 src/AL2DBML.Application/Interfaces/IDBMLWriter.cs create mode 100644 src/AL2DBML.Application/Interfaces/ISchemaPostProcessor.cs create mode 100644 src/AL2DBML.DI/WriterServiceExtensions.cs create mode 100644 src/AL2DBML.Tests/Writer/WriterTests.cs create mode 100644 src/DBMLWriter/DBMLWriter.csproj create mode 100644 src/DBMLWriter/SchemaPostProcessor.cs create mode 100644 src/DBMLWriter/Writer.cs diff --git a/AL2DBML.sln b/AL2DBML.sln index 07c991e..30722b1 100644 --- a/AL2DBML.sln +++ b/AL2DBML.sln @@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AL2DBML.Application", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AL2DBML.DI", "src\AL2DBML.DI\AL2DBML.DI.csproj", "{6F45FC15-7CA3-4F71-9900-3AE77ED9C14B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DBMLWriter", "src\DBMLWriter\DBMLWriter.csproj", "{D2033B53-9C03-47D9-BA96-2A59F5FDCC95}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -99,6 +101,18 @@ Global {6F45FC15-7CA3-4F71-9900-3AE77ED9C14B}.Release|x64.Build.0 = Release|Any CPU {6F45FC15-7CA3-4F71-9900-3AE77ED9C14B}.Release|x86.ActiveCfg = Release|Any CPU {6F45FC15-7CA3-4F71-9900-3AE77ED9C14B}.Release|x86.Build.0 = Release|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Debug|x64.ActiveCfg = Debug|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Debug|x64.Build.0 = Debug|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Debug|x86.ActiveCfg = Debug|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Debug|x86.Build.0 = Debug|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Release|Any CPU.Build.0 = Release|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Release|x64.ActiveCfg = Release|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Release|x64.Build.0 = Release|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Release|x86.ActiveCfg = Release|Any CPU + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -110,5 +124,6 @@ Global {DFD7C487-21C3-414D-B1B5-71A7923863B4} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {05BCFDA4-6B86-4105-9A52-6B4F9790069D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {6F45FC15-7CA3-4F71-9900-3AE77ED9C14B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {D2033B53-9C03-47D9-BA96-2A59F5FDCC95} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} EndGlobalSection EndGlobal diff --git a/src/AL2DBML.Application/Interfaces/IAlParser.cs b/src/AL2DBML.Application/Interfaces/IAlParser.cs index 987a6cd..0f3689b 100644 --- a/src/AL2DBML.Application/Interfaces/IAlParser.cs +++ b/src/AL2DBML.Application/Interfaces/IAlParser.cs @@ -11,4 +11,5 @@ public interface IAlParser DBMLTable ParseTable(string alTableFileContent); DBMLTable ParseTableExtension(string alTableExtensionFileContent); DBMLColumn ParseField(string alFieldContent); + OutputSchema GetOutputSchema(); } diff --git a/src/AL2DBML.Application/Interfaces/IDBMLWriter.cs b/src/AL2DBML.Application/Interfaces/IDBMLWriter.cs new file mode 100644 index 0000000..3ed6100 --- /dev/null +++ b/src/AL2DBML.Application/Interfaces/IDBMLWriter.cs @@ -0,0 +1,8 @@ +using AL2DBML.Core.Models; + +namespace AL2DBML.Application.Interfaces; + +public interface IDBMLWriter +{ + Task WriteDBMLAsync(OutputSchema outputSchema); +} diff --git a/src/AL2DBML.Application/Interfaces/ISchemaPostProcessor.cs b/src/AL2DBML.Application/Interfaces/ISchemaPostProcessor.cs new file mode 100644 index 0000000..db5cc45 --- /dev/null +++ b/src/AL2DBML.Application/Interfaces/ISchemaPostProcessor.cs @@ -0,0 +1,8 @@ +using AL2DBML.Core.Models; + +namespace AL2DBML.Application.Interfaces; + +public interface ISchemaPostProcessor +{ + OutputSchema Process(OutputSchema schema); +} diff --git a/src/AL2DBML.DI/AL2DBML.DI.csproj b/src/AL2DBML.DI/AL2DBML.DI.csproj index 9aa9afd..288fe95 100644 --- a/src/AL2DBML.DI/AL2DBML.DI.csproj +++ b/src/AL2DBML.DI/AL2DBML.DI.csproj @@ -10,6 +10,7 @@ + diff --git a/src/AL2DBML.DI/AL2DbmlServiceExtensions.cs b/src/AL2DBML.DI/AL2DbmlServiceExtensions.cs index fa0ff8d..d76e2fc 100644 --- a/src/AL2DBML.DI/AL2DbmlServiceExtensions.cs +++ b/src/AL2DBML.DI/AL2DbmlServiceExtensions.cs @@ -8,5 +8,6 @@ public static class AL2DbmlServiceExtensions public static IServiceCollection AddAL2Dbml(this IServiceCollection services) => services .AddApplication() - .AddParser(); + .AddParser() + .AddWriter(); } diff --git a/src/AL2DBML.DI/WriterServiceExtensions.cs b/src/AL2DBML.DI/WriterServiceExtensions.cs new file mode 100644 index 0000000..4ff0219 --- /dev/null +++ b/src/AL2DBML.DI/WriterServiceExtensions.cs @@ -0,0 +1,15 @@ +using AL2DBML.Application.Interfaces; +using DBMLWriter; +using Microsoft.Extensions.DependencyInjection; + +namespace AL2DBML.DI; + +public static class WriterServiceExtensions +{ + public static IServiceCollection AddWriter(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + return services; + } +} diff --git a/src/AL2DBML.Parser/AlParser.cs b/src/AL2DBML.Parser/AlParser.cs index 3f04a11..55acdd2 100644 --- a/src/AL2DBML.Parser/AlParser.cs +++ b/src/AL2DBML.Parser/AlParser.cs @@ -174,4 +174,9 @@ public DBMLColumn ParseField(string alFieldContent) CalcFormula = calcFormula }; } + + public OutputSchema GetOutputSchema() + { + return _outputSchema; + } } diff --git a/src/AL2DBML.Tests/AL2DBML.Tests.csproj b/src/AL2DBML.Tests/AL2DBML.Tests.csproj index 7f8be61..f085486 100644 --- a/src/AL2DBML.Tests/AL2DBML.Tests.csproj +++ b/src/AL2DBML.Tests/AL2DBML.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/src/AL2DBML.Tests/TestBase.cs b/src/AL2DBML.Tests/TestBase.cs index 6b7ef0c..4beeb0b 100644 --- a/src/AL2DBML.Tests/TestBase.cs +++ b/src/AL2DBML.Tests/TestBase.cs @@ -7,6 +7,7 @@ public abstract class TestBase { protected IServiceProvider Services { get; } protected IAlParser _parser { get; private set; } + protected IDBMLWriter _writer { get; private set; } protected TestBase() { @@ -15,6 +16,7 @@ protected TestBase() .BuildServiceProvider(); _parser = Services.GetRequiredService(); + _writer = Services.GetRequiredService(); } protected void ResetParser() diff --git a/src/AL2DBML.Tests/Writer/WriterTests.cs b/src/AL2DBML.Tests/Writer/WriterTests.cs new file mode 100644 index 0000000..ddebf51 --- /dev/null +++ b/src/AL2DBML.Tests/Writer/WriterTests.cs @@ -0,0 +1,454 @@ +using AL2DBML.Core.Models; + +namespace AL2DBML.Tests.Writer; + +public class WriterTests : TestBase +{ + + // --- Schema vide --- + + [Fact] + public async Task WriteDBMLAsync_EmptySchema_ReturnsEmptyString() + { + var result = await _writer.WriteDBMLAsync(new OutputSchema()); + + Assert.Equal(string.Empty, result); + } + + // --- Enums --- + + [Fact] + public async Task WriteDBMLAsync_WithEnum_GeneratesEnumBlock() + { + var schema = new OutputSchema + { + Enums = [new DBMLEnum { Name = "Status", Values = ["Active", "Inactive"] }] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("enum Status {", result); + Assert.Contains(" Active", result); + Assert.Contains(" Inactive", result); + } + + [Fact] + public async Task WriteDBMLAsync_WithEnumWithSpecialChars_QuotesIdentifiers() + { + var schema = new OutputSchema + { + Enums = [new DBMLEnum { Name = "My Enum", Values = ["Value 1"] }] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("enum \"My Enum\" {", result); + Assert.Contains(" \"Value 1\"", result); + } + + // --- Tables --- + + [Fact] + public async Task WriteDBMLAsync_WithSimpleTable_GeneratesTableBlock() + { + var schema = new OutputSchema + { + Tables = [new DBMLTable { Name = "Customer", Fields = [new DBMLColumn { Name = "Name", Type = "Integer" }] }] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("table Customer {", result); + Assert.Contains(" Name Integer", result); + } + + [Fact] + public async Task WriteDBMLAsync_WithPrimaryKeyField_AddsPkAttribute() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = [new DBMLColumn { Name = "No.", Type = "Integer", IsPrimaryKey = true }] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains(" \"No.\" Integer [pk]", result); + } + + [Fact] + public async Task WriteDBMLAsync_WithReferenceField_AddsRefAttribute() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "SalesLine", + Fields = + [ + new DBMLColumn { Name = "CustomerNo", Type = "Integer", References = ["Customer", "Id"] } + ] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("ref: > Customer.Id", result); + } + + [Fact] + public async Task WriteDBMLAsync_WithReferenceToSpecialCharField_QuotesRef() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "SalesLine", + Fields = + [ + new DBMLColumn { Name = "CustomerNo", Type = "Integer", References = ["Customer", "No."] } + ] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("ref: > Customer.\"No.\"", result); + } + + [Fact] + public async Task WriteDBMLAsync_WithSpecialCharsInTableName_QuotesTableName() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Salesperson/Purchaser", + Fields = [new DBMLColumn { Name = "Code", Type = "Integer" }] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("table \"Salesperson/Purchaser\" {", result); + } + + [Fact] + public async Task WriteDBMLAsync_WithSpecialCharsInFieldName_QuotesFieldName() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = [new DBMLColumn { Name = "Search Name", Type = "Integer" }] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains(" \"Search Name\" Integer", result); + } + + [Fact] + public async Task WriteDBMLAsync_WithFlowfield_AddsNoteAttribute() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = + [ + new DBMLColumn + { + Name = "Balance", + Type = "Decimal", + IsFlowfield = true, + CalcFormula = "Sum(Entry.Amount)" + } + ] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("note: 'FlowField: CalcFormula = Sum(Entry.Amount)'", result); + } + + [Fact] + public async Task WriteDBMLAsync_WithFlowfieldWithSingleQuote_EscapesQuote() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = + [ + new DBMLColumn + { + Name = "Balance", + Type = "Decimal", + IsFlowfield = true, + CalcFormula = "Sum('Entry'.Amount)" + } + ] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains(@"CalcFormula = Sum(\'Entry\'.Amount)", result); + } + + [Fact] + public async Task WriteDBMLAsync_NonFlowfieldWithCalcFormula_DoesNotAddNote() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = [new DBMLColumn { Name = "Balance", Type = "Decimal", IsFlowfield = false, CalcFormula = "Sum(Entry.Amount)" }] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.DoesNotContain("note:", result); + } + + [Fact] + public async Task WriteDBMLAsync_EnumsBeforeTables() + { + var schema = new OutputSchema + { + Enums = [new DBMLEnum { Name = "Status", Values = ["Active"] }], + Tables = [new DBMLTable { Name = "Customer", Fields = [new DBMLColumn { Name = "Id", Type = "Integer" }] }] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.True(result.IndexOf("enum") < result.IndexOf("table")); + } + + // --- Non-mutation de l'input --- + + [Fact] + public async Task WriteDBMLAsync_DoesNotMutateInputSchema() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = + [ + new DBMLColumn { Name = "Id", Type = "Integer", IsPrimaryKey = true }, + new DBMLColumn { Name = "UnknownField", Type = "Integer" } + ] + } + ] + }; + + await _writer.WriteDBMLAsync(schema); + + Assert.Equal(2, schema.Tables[0].Fields.Count); + Assert.Contains(schema.Tables[0].Fields, f => f.Name == "UnknownField"); + } + + // --- CleanupUnknownFieldReferences --- + + [Fact] + public async Task WriteDBMLAsync_UnknownRef_ResolvedToSinglePkName() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = [new DBMLColumn { Name = "Id", Type = "Integer", IsPrimaryKey = true }] + }, + new DBMLTable + { + Name = "SalesLine", + Fields = [new DBMLColumn { Name = "CustomerNo", Type = "Integer", References = ["Customer", "UnknownField"] }] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("ref: > Customer.Id", result); + Assert.DoesNotContain("UnknownField", result); + } + + [Fact] + public async Task WriteDBMLAsync_UnknownRef_AlignsTypeWithPkType() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = [new DBMLColumn { Name = "Id", Type = "Guid", IsPrimaryKey = true }] + }, + new DBMLTable + { + Name = "SalesLine", + Fields = [new DBMLColumn { Name = "CustomerNo", Type = "Integer", References = ["Customer", "UnknownField"] }] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains(" CustomerNo Guid", result); + } + + [Fact] + public async Task WriteDBMLAsync_UnknownFieldColumn_RemovedFromTableWithSinglePk() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = + [ + new DBMLColumn { Name = "Id", Type = "Integer", IsPrimaryKey = true }, + new DBMLColumn { Name = "UnknownField", Type = "Integer" } + ] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.DoesNotContain("UnknownField", result); + } + + [Fact] + public async Task WriteDBMLAsync_UnknownRef_NotResolvedWhenTargetHasMultiplePks() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "SalesLine", + Fields = + [ + new DBMLColumn { Name = "DocType", Type = "Integer", IsPrimaryKey = true }, + new DBMLColumn { Name = "LineNo", Type = "Integer", IsPrimaryKey = true } + ] + }, + new DBMLTable + { + Name = "SalesSubLine", + Fields = [new DBMLColumn { Name = "SalesNo", Type = "Integer", References = ["SalesLine", "UnknownField"] }] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("UnknownField", result); + } + + // --- InferSinglePkWhenOnlyUnknownPlusOneColumn --- + + [Fact] + public async Task WriteDBMLAsync_InfersPk_WhenSingleRealFieldPlusUnknownField() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = + [ + new DBMLColumn { Name = "Id", Type = "Integer" }, + new DBMLColumn { Name = "UnknownField", Type = "Integer" } + ] + }, + new DBMLTable + { + Name = "SalesLine", + Fields = [new DBMLColumn { Name = "CustomerNo", Type = "Integer", References = ["Customer", "UnknownField"] }] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.Contains("Id Integer [pk]", result); + Assert.Contains("ref: > Customer.Id", result); + Assert.DoesNotContain("UnknownField", result); + } + + [Fact] + public async Task WriteDBMLAsync_DoesNotInferPk_WhenMultipleRealFields() + { + var schema = new OutputSchema + { + Tables = + [ + new DBMLTable + { + Name = "Customer", + Fields = + [ + new DBMLColumn { Name = "Id", Type = "Integer" }, + new DBMLColumn { Name = "Name", Type = "Integer" }, + new DBMLColumn { Name = "UnknownField", Type = "Integer" } + ] + } + ] + }; + + var result = await _writer.WriteDBMLAsync(schema); + + Assert.DoesNotContain("[pk]", result); + Assert.Contains("UnknownField", result); + } +} diff --git a/src/DBMLWriter/DBMLWriter.csproj b/src/DBMLWriter/DBMLWriter.csproj new file mode 100644 index 0000000..2d8d310 --- /dev/null +++ b/src/DBMLWriter/DBMLWriter.csproj @@ -0,0 +1,14 @@ + + + + net10.0 + enable + enable + + + + + + + + diff --git a/src/DBMLWriter/SchemaPostProcessor.cs b/src/DBMLWriter/SchemaPostProcessor.cs new file mode 100644 index 0000000..a91afe1 --- /dev/null +++ b/src/DBMLWriter/SchemaPostProcessor.cs @@ -0,0 +1,91 @@ +using AL2DBML.Application.Interfaces; +using AL2DBML.Core.Models; + +namespace DBMLWriter; + +public class SchemaPostProcessor : ISchemaPostProcessor +{ + private const string Unknown = "UnknownField"; + + public OutputSchema Process(OutputSchema schema) + { + var copy = DeepCopy(schema); + + InferSinglePkWhenOnlyUnknownPlusOneColumn(copy); + + var singlePkByTable = copy.Tables + .Select(t => new { Table = t, Pks = t.Fields.Where(f => f.IsPrimaryKey).ToList() }) + .Where(x => x.Pks.Count == 1) + .ToDictionary(x => x.Table.Name, x => x.Pks[0], StringComparer.OrdinalIgnoreCase); + + ResolveUnknownFieldReferences(copy, singlePkByTable); + RemoveUnknownFieldColumns(copy, singlePkByTable); + + return copy; + } + + private static OutputSchema DeepCopy(OutputSchema schema) => + new() + { + Enums = schema.Enums + .Select(e => new DBMLEnum { Name = e.Name, Values = [..e.Values] }) + .ToList(), + Tables = schema.Tables + .Select(t => new DBMLTable + { + Name = t.Name, + Fields = t.Fields + .Select(f => new DBMLColumn + { + Name = f.Name, + Type = f.Type, + IsPrimaryKey = f.IsPrimaryKey, + References = f.References?.ToArray(), + IsFlowfield = f.IsFlowfield, + CalcFormula = f.CalcFormula + }) + .ToList() + }) + .ToList() + }; + + private static void InferSinglePkWhenOnlyUnknownPlusOneColumn(OutputSchema schema) + { + foreach (var table in schema.Tables) + { + if (table.Fields.Count == 0 || table.Fields.Any(f => f.IsPrimaryKey)) continue; + + var realFields = table.Fields + .Where(f => !string.Equals(f.Name, Unknown, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + var hasUnknown = table.Fields.Any(f => string.Equals(f.Name, Unknown, StringComparison.OrdinalIgnoreCase)); + + if (realFields.Count == 1 && hasUnknown) + realFields[0].IsPrimaryKey = true; + } + } + + private static void ResolveUnknownFieldReferences(OutputSchema schema, Dictionary singlePkByTable) + { + foreach (var table in schema.Tables) + { + foreach (var field in table.Fields) + { + if (field.References is not { Length: 2 } refs) continue; + if (!string.Equals(refs[1], Unknown, StringComparison.OrdinalIgnoreCase)) continue; + if (!singlePkByTable.TryGetValue(refs[0], out var pkField)) continue; + + field.References[1] = pkField.Name; + if (!string.IsNullOrWhiteSpace(pkField.Type)) + field.Type = pkField.Type; + } + } + } + + private static void RemoveUnknownFieldColumns(OutputSchema schema, Dictionary singlePkByTable) + { + foreach (var table in schema.Tables.Where(t => singlePkByTable.ContainsKey(t.Name))) + table.Fields.RemoveAll(f => string.Equals(f.Name, Unknown, StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/src/DBMLWriter/Writer.cs b/src/DBMLWriter/Writer.cs new file mode 100644 index 0000000..3a32aa9 --- /dev/null +++ b/src/DBMLWriter/Writer.cs @@ -0,0 +1,76 @@ +using System.Text; +using System.Text.RegularExpressions; +using AL2DBML.Application.Interfaces; +using AL2DBML.Core.Models; + +namespace DBMLWriter; + +public class Writer : IDBMLWriter +{ + private readonly ISchemaPostProcessor _postProcessor; + + public Writer(ISchemaPostProcessor postProcessor) + { + _postProcessor = postProcessor; + } + + public Task WriteDBMLAsync(OutputSchema outputSchema) + { + var schema = _postProcessor.Process(outputSchema); + + var sb = new StringBuilder(); + sb.Append(WriteEnums(schema.Enums)); + sb.Append(WriteTables(schema.Tables)); + + return Task.FromResult(sb.ToString()); + } + + private static string Quotes(string name) => + Regex.IsMatch(name, @"[^a-zA-Z0-9_]") ? $"\"{name}\"" : name; + + private static string WriteEnums(List enums) + { + var sb = new StringBuilder(); + foreach (var enumObj in enums) + { + sb.AppendLine($"enum {Quotes(enumObj.Name)} {{"); + foreach (var value in enumObj.Values) + sb.AppendLine($" {Quotes(value)}"); + sb.AppendLine("}"); + sb.AppendLine(); + } + return sb.ToString(); + } + + private static string WriteTables(List tables) + { + var sb = new StringBuilder(); + foreach (var table in tables) + { + sb.AppendLine($"table {Quotes(table.Name)} {{"); + foreach (var field in table.Fields) + { + sb.Append($" {Quotes(field.Name)} {Quotes(field.Type)}"); + + var attributes = new List(); + + if (field.IsPrimaryKey) + attributes.Add("pk"); + + if (field.References is { Length: 2 } refs && !string.IsNullOrEmpty(refs[0])) + attributes.Add($"ref: > {Quotes(refs[0])}.{Quotes(refs[1])}"); + + if (field.IsFlowfield && !string.IsNullOrEmpty(field.CalcFormula)) + attributes.Add($"note: 'FlowField: CalcFormula = {field.CalcFormula.Replace("'", "\\'")}'"); + + if (attributes.Count > 0) + sb.Append(" [" + string.Join(", ", attributes) + "]"); + + sb.AppendLine(); + } + sb.AppendLine("}"); + sb.AppendLine(); + } + return sb.ToString(); + } +} From 33ef2add6be8884169a91f6a4d4663d74e4cc866 Mon Sep 17 00:00:00 2001 From: OGR-67 Date: Fri, 20 Mar 2026 07:32:02 +0100 Subject: [PATCH 2/2] feat: Refactor DBMLWriter and enhance OutputSchema handling with deep copy functionality --- AL2DBML.sln | 2 +- .../Helpers/OutputSchemaHelper.cs | 32 ++++++++++ src/AL2DBML.DI/AL2DBML.DI.csproj | 2 +- src/AL2DBML.DI/WriterServiceExtensions.cs | 4 +- src/AL2DBML.Parser/AlParser.cs | 3 +- src/AL2DBML.Tests/AL2DBML.Tests.csproj | 4 +- src/AL2DBML.Tests/Writer/WriterTests.cs | 58 +++++++++---------- ...riter.csproj => AL2DBLM.DBMLWriter.csproj} | 0 src/DBMLWriter/{Writer.cs => DBMLWriter.cs} | 8 +-- src/DBMLWriter/SchemaPostProcessor.cs | 30 +--------- 10 files changed, 75 insertions(+), 68 deletions(-) create mode 100644 src/AL2DBML.Application/Helpers/OutputSchemaHelper.cs rename src/DBMLWriter/{DBMLWriter.csproj => AL2DBLM.DBMLWriter.csproj} (100%) rename src/DBMLWriter/{Writer.cs => DBMLWriter.cs} (92%) diff --git a/AL2DBML.sln b/AL2DBML.sln index 30722b1..405701d 100644 --- a/AL2DBML.sln +++ b/AL2DBML.sln @@ -17,7 +17,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AL2DBML.Application", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AL2DBML.DI", "src\AL2DBML.DI\AL2DBML.DI.csproj", "{6F45FC15-7CA3-4F71-9900-3AE77ED9C14B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DBMLWriter", "src\DBMLWriter\DBMLWriter.csproj", "{D2033B53-9C03-47D9-BA96-2A59F5FDCC95}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AL2DBLM.DBMLWriter", "src\DBMLWriter\AL2DBLM.DBMLWriter.csproj", "{D2033B53-9C03-47D9-BA96-2A59F5FDCC95}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/AL2DBML.Application/Helpers/OutputSchemaHelper.cs b/src/AL2DBML.Application/Helpers/OutputSchemaHelper.cs new file mode 100644 index 0000000..e447ba5 --- /dev/null +++ b/src/AL2DBML.Application/Helpers/OutputSchemaHelper.cs @@ -0,0 +1,32 @@ +using AL2DBML.Core.Models; + +namespace AL2DBML.Application.Helpers; + +public static class OutputSchemaHelper +{ + public static OutputSchema DeepCopy(OutputSchema schema) => + new() + { + Enums = schema.Enums + .Select(e => new DBMLEnum { Name = e.Name, Values = [.. e.Values] }) + .ToList(), + Tables = schema.Tables + .Select(t => new DBMLTable + { + Name = t.Name, + Fields = t.Fields + .Select(f => new DBMLColumn + { + Name = f.Name, + Type = f.Type, + IsPrimaryKey = f.IsPrimaryKey, + References = f.References?.ToArray(), + IsFlowfield = f.IsFlowfield, + CalcFormula = f.CalcFormula + }) + .ToList() + }) + .ToList() + }; + +} diff --git a/src/AL2DBML.DI/AL2DBML.DI.csproj b/src/AL2DBML.DI/AL2DBML.DI.csproj index 288fe95..b0713e0 100644 --- a/src/AL2DBML.DI/AL2DBML.DI.csproj +++ b/src/AL2DBML.DI/AL2DBML.DI.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/AL2DBML.DI/WriterServiceExtensions.cs b/src/AL2DBML.DI/WriterServiceExtensions.cs index 4ff0219..f3a1fb2 100644 --- a/src/AL2DBML.DI/WriterServiceExtensions.cs +++ b/src/AL2DBML.DI/WriterServiceExtensions.cs @@ -1,5 +1,5 @@ using AL2DBML.Application.Interfaces; -using DBMLWriter; +using AL2DBML.DBMLWriter; using Microsoft.Extensions.DependencyInjection; namespace AL2DBML.DI; @@ -9,7 +9,7 @@ public static class WriterServiceExtensions public static IServiceCollection AddWriter(this IServiceCollection services) { services.AddScoped(); - services.AddScoped(); + services.AddScoped(); return services; } } diff --git a/src/AL2DBML.Parser/AlParser.cs b/src/AL2DBML.Parser/AlParser.cs index 55acdd2..facc431 100644 --- a/src/AL2DBML.Parser/AlParser.cs +++ b/src/AL2DBML.Parser/AlParser.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using AL2DBML.Application.Helpers; using AL2DBML.Application.Interfaces; using AL2DBML.Core.Enums; using AL2DBML.Core.Models; @@ -177,6 +178,6 @@ public DBMLColumn ParseField(string alFieldContent) public OutputSchema GetOutputSchema() { - return _outputSchema; + return OutputSchemaHelper.DeepCopy(_outputSchema); } } diff --git a/src/AL2DBML.Tests/AL2DBML.Tests.csproj b/src/AL2DBML.Tests/AL2DBML.Tests.csproj index f085486..688f859 100644 --- a/src/AL2DBML.Tests/AL2DBML.Tests.csproj +++ b/src/AL2DBML.Tests/AL2DBML.Tests.csproj @@ -17,9 +17,7 @@ - - - + diff --git a/src/AL2DBML.Tests/Writer/WriterTests.cs b/src/AL2DBML.Tests/Writer/WriterTests.cs index ddebf51..fdc6824 100644 --- a/src/AL2DBML.Tests/Writer/WriterTests.cs +++ b/src/AL2DBML.Tests/Writer/WriterTests.cs @@ -53,13 +53,13 @@ public async Task WriteDBMLAsync_WithSimpleTable_GeneratesTableBlock() { var schema = new OutputSchema { - Tables = [new DBMLTable { Name = "Customer", Fields = [new DBMLColumn { Name = "Name", Type = "Integer" }] }] + Tables = [new DBMLTable { Name = "Customer", Fields = [new DBMLColumn { Name = "Name", Type = "Code[20]" }] }] }; var result = await _writer.WriteDBMLAsync(schema); Assert.Contains("table Customer {", result); - Assert.Contains(" Name Integer", result); + Assert.Contains(" Name \"Code[20]\"", result); } [Fact] @@ -72,14 +72,14 @@ public async Task WriteDBMLAsync_WithPrimaryKeyField_AddsPkAttribute() new DBMLTable { Name = "Customer", - Fields = [new DBMLColumn { Name = "No.", Type = "Integer", IsPrimaryKey = true }] + Fields = [new DBMLColumn { Name = "No.", Type = "Code[20]", IsPrimaryKey = true }] } ] }; var result = await _writer.WriteDBMLAsync(schema); - Assert.Contains(" \"No.\" Integer [pk]", result); + Assert.Contains(" \"No.\" \"Code[20]\" [pk]", result); } [Fact] @@ -94,7 +94,7 @@ public async Task WriteDBMLAsync_WithReferenceField_AddsRefAttribute() Name = "SalesLine", Fields = [ - new DBMLColumn { Name = "CustomerNo", Type = "Integer", References = ["Customer", "Id"] } + new DBMLColumn { Name = "CustomerNo", Type = "Code[20]", References = ["Customer", "Id"] } ] } ] @@ -117,7 +117,7 @@ public async Task WriteDBMLAsync_WithReferenceToSpecialCharField_QuotesRef() Name = "SalesLine", Fields = [ - new DBMLColumn { Name = "CustomerNo", Type = "Integer", References = ["Customer", "No."] } + new DBMLColumn { Name = "CustomerNo", Type = "Code[20]", References = ["Customer", "No."] } ] } ] @@ -138,7 +138,7 @@ public async Task WriteDBMLAsync_WithSpecialCharsInTableName_QuotesTableName() new DBMLTable { Name = "Salesperson/Purchaser", - Fields = [new DBMLColumn { Name = "Code", Type = "Integer" }] + Fields = [new DBMLColumn { Name = "Code", Type = "Code[20]" }] } ] }; @@ -158,14 +158,14 @@ public async Task WriteDBMLAsync_WithSpecialCharsInFieldName_QuotesFieldName() new DBMLTable { Name = "Customer", - Fields = [new DBMLColumn { Name = "Search Name", Type = "Integer" }] + Fields = [new DBMLColumn { Name = "Search Name", Type = "Code[20]" }] } ] }; var result = await _writer.WriteDBMLAsync(schema); - Assert.Contains(" \"Search Name\" Integer", result); + Assert.Contains(" \"Search Name\" \"Code[20]\"", result); } [Fact] @@ -252,7 +252,7 @@ public async Task WriteDBMLAsync_EnumsBeforeTables() var schema = new OutputSchema { Enums = [new DBMLEnum { Name = "Status", Values = ["Active"] }], - Tables = [new DBMLTable { Name = "Customer", Fields = [new DBMLColumn { Name = "Id", Type = "Integer" }] }] + Tables = [new DBMLTable { Name = "Customer", Fields = [new DBMLColumn { Name = "Id", Type = "Code[20]" }] }] }; var result = await _writer.WriteDBMLAsync(schema); @@ -274,8 +274,8 @@ public async Task WriteDBMLAsync_DoesNotMutateInputSchema() Name = "Customer", Fields = [ - new DBMLColumn { Name = "Id", Type = "Integer", IsPrimaryKey = true }, - new DBMLColumn { Name = "UnknownField", Type = "Integer" } + new DBMLColumn { Name = "Id", Type = "Code[20]", IsPrimaryKey = true }, + new DBMLColumn { Name = "UnknownField", Type = "Code[20]" } ] } ] @@ -299,12 +299,12 @@ public async Task WriteDBMLAsync_UnknownRef_ResolvedToSinglePkName() new DBMLTable { Name = "Customer", - Fields = [new DBMLColumn { Name = "Id", Type = "Integer", IsPrimaryKey = true }] + Fields = [new DBMLColumn { Name = "Id", Type = "Code[20]", IsPrimaryKey = true }] }, new DBMLTable { Name = "SalesLine", - Fields = [new DBMLColumn { Name = "CustomerNo", Type = "Integer", References = ["Customer", "UnknownField"] }] + Fields = [new DBMLColumn { Name = "CustomerNo", Type = "Code[20]", References = ["Customer", "UnknownField"] }] } ] }; @@ -325,19 +325,19 @@ public async Task WriteDBMLAsync_UnknownRef_AlignsTypeWithPkType() new DBMLTable { Name = "Customer", - Fields = [new DBMLColumn { Name = "Id", Type = "Guid", IsPrimaryKey = true }] + Fields = [new DBMLColumn { Name = "No", Type = "Code[20]", IsPrimaryKey = true }] }, new DBMLTable { Name = "SalesLine", - Fields = [new DBMLColumn { Name = "CustomerNo", Type = "Integer", References = ["Customer", "UnknownField"] }] + Fields = [new DBMLColumn { Name = "CustomerNo", Type = "Code[20]", References = ["Customer", "UnknownField"] }] } ] }; var result = await _writer.WriteDBMLAsync(schema); - Assert.Contains(" CustomerNo Guid", result); + Assert.Contains("CustomerNo \"Code[20]\"", result); } [Fact] @@ -352,8 +352,8 @@ public async Task WriteDBMLAsync_UnknownFieldColumn_RemovedFromTableWithSinglePk Name = "Customer", Fields = [ - new DBMLColumn { Name = "Id", Type = "Integer", IsPrimaryKey = true }, - new DBMLColumn { Name = "UnknownField", Type = "Integer" } + new DBMLColumn { Name = "Id", Type = "Code[20]", IsPrimaryKey = true }, + new DBMLColumn { Name = "UnknownField", Type = "Code[20]" } ] } ] @@ -376,14 +376,14 @@ public async Task WriteDBMLAsync_UnknownRef_NotResolvedWhenTargetHasMultiplePks( Name = "SalesLine", Fields = [ - new DBMLColumn { Name = "DocType", Type = "Integer", IsPrimaryKey = true }, - new DBMLColumn { Name = "LineNo", Type = "Integer", IsPrimaryKey = true } + new DBMLColumn { Name = "DocType", Type = "Code[20]", IsPrimaryKey = true }, + new DBMLColumn { Name = "LineNo", Type = "Code[20]", IsPrimaryKey = true } ] }, new DBMLTable { Name = "SalesSubLine", - Fields = [new DBMLColumn { Name = "SalesNo", Type = "Integer", References = ["SalesLine", "UnknownField"] }] + Fields = [new DBMLColumn { Name = "SalesNo", Type = "Code[20]", References = ["SalesLine", "UnknownField"] }] } ] }; @@ -407,21 +407,21 @@ public async Task WriteDBMLAsync_InfersPk_WhenSingleRealFieldPlusUnknownField() Name = "Customer", Fields = [ - new DBMLColumn { Name = "Id", Type = "Integer" }, - new DBMLColumn { Name = "UnknownField", Type = "Integer" } + new DBMLColumn { Name = "Id", Type = "Code[20]" }, + new DBMLColumn { Name = "UnknownField", Type = "Code[20]" } ] }, new DBMLTable { Name = "SalesLine", - Fields = [new DBMLColumn { Name = "CustomerNo", Type = "Integer", References = ["Customer", "UnknownField"] }] + Fields = [new DBMLColumn { Name = "CustomerNo", Type = "Code[20]", References = ["Customer", "UnknownField"] }] } ] }; var result = await _writer.WriteDBMLAsync(schema); - Assert.Contains("Id Integer [pk]", result); + Assert.Contains("Id \"Code[20]\" [pk]", result); Assert.Contains("ref: > Customer.Id", result); Assert.DoesNotContain("UnknownField", result); } @@ -438,9 +438,9 @@ public async Task WriteDBMLAsync_DoesNotInferPk_WhenMultipleRealFields() Name = "Customer", Fields = [ - new DBMLColumn { Name = "Id", Type = "Integer" }, - new DBMLColumn { Name = "Name", Type = "Integer" }, - new DBMLColumn { Name = "UnknownField", Type = "Integer" } + new DBMLColumn { Name = "Id", Type = "Code[20]" }, + new DBMLColumn { Name = "Name", Type = "Code[20]" }, + new DBMLColumn { Name = "UnknownField", Type = "Code[20]" } ] } ] diff --git a/src/DBMLWriter/DBMLWriter.csproj b/src/DBMLWriter/AL2DBLM.DBMLWriter.csproj similarity index 100% rename from src/DBMLWriter/DBMLWriter.csproj rename to src/DBMLWriter/AL2DBLM.DBMLWriter.csproj diff --git a/src/DBMLWriter/Writer.cs b/src/DBMLWriter/DBMLWriter.cs similarity index 92% rename from src/DBMLWriter/Writer.cs rename to src/DBMLWriter/DBMLWriter.cs index 3a32aa9..8b11a43 100644 --- a/src/DBMLWriter/Writer.cs +++ b/src/DBMLWriter/DBMLWriter.cs @@ -3,13 +3,13 @@ using AL2DBML.Application.Interfaces; using AL2DBML.Core.Models; -namespace DBMLWriter; +namespace AL2DBML.DBMLWriter; -public class Writer : IDBMLWriter +public class DBLMWriter : IDBMLWriter { private readonly ISchemaPostProcessor _postProcessor; - public Writer(ISchemaPostProcessor postProcessor) + public DBLMWriter(ISchemaPostProcessor postProcessor) { _postProcessor = postProcessor; } @@ -57,7 +57,7 @@ private static string WriteTables(List tables) if (field.IsPrimaryKey) attributes.Add("pk"); - if (field.References is { Length: 2 } refs && !string.IsNullOrEmpty(refs[0])) + if (field.References is { Length: 2 } refs && !string.IsNullOrEmpty(refs[0]) && !string.IsNullOrEmpty(refs[1])) attributes.Add($"ref: > {Quotes(refs[0])}.{Quotes(refs[1])}"); if (field.IsFlowfield && !string.IsNullOrEmpty(field.CalcFormula)) diff --git a/src/DBMLWriter/SchemaPostProcessor.cs b/src/DBMLWriter/SchemaPostProcessor.cs index a91afe1..94f4f9a 100644 --- a/src/DBMLWriter/SchemaPostProcessor.cs +++ b/src/DBMLWriter/SchemaPostProcessor.cs @@ -1,7 +1,8 @@ +using AL2DBML.Application.Helpers; using AL2DBML.Application.Interfaces; using AL2DBML.Core.Models; -namespace DBMLWriter; +namespace AL2DBML.DBMLWriter; public class SchemaPostProcessor : ISchemaPostProcessor { @@ -9,7 +10,7 @@ public class SchemaPostProcessor : ISchemaPostProcessor public OutputSchema Process(OutputSchema schema) { - var copy = DeepCopy(schema); + var copy = OutputSchemaHelper.DeepCopy(schema); InferSinglePkWhenOnlyUnknownPlusOneColumn(copy); @@ -24,31 +25,6 @@ public OutputSchema Process(OutputSchema schema) return copy; } - private static OutputSchema DeepCopy(OutputSchema schema) => - new() - { - Enums = schema.Enums - .Select(e => new DBMLEnum { Name = e.Name, Values = [..e.Values] }) - .ToList(), - Tables = schema.Tables - .Select(t => new DBMLTable - { - Name = t.Name, - Fields = t.Fields - .Select(f => new DBMLColumn - { - Name = f.Name, - Type = f.Type, - IsPrimaryKey = f.IsPrimaryKey, - References = f.References?.ToArray(), - IsFlowfield = f.IsFlowfield, - CalcFormula = f.CalcFormula - }) - .ToList() - }) - .ToList() - }; - private static void InferSinglePkWhenOnlyUnknownPlusOneColumn(OutputSchema schema) { foreach (var table in schema.Tables)