From 6782591e38774d6bcc416eca26390a6074ff5489 Mon Sep 17 00:00:00 2001 From: OGR-67 Date: Sat, 21 Mar 2026 06:07:19 +0100 Subject: [PATCH 1/4] feat: Implement parsing strategies and command structure for AL project files --- .gitignore | 3 + .../Interfaces/IAlParser.cs | 1 + src/AL2DBML.CLI/AL2DBML.CLI.csproj | 10 ++++ src/AL2DBML.CLI/Commands/GenerateCommand.cs | 55 +++++++++++++++++++ src/AL2DBML.CLI/Enums/InputType.cs | 9 +++ src/AL2DBML.CLI/Program.cs | 41 +++++++++++++- src/AL2DBML.CLI/Services/FileSystemService.cs | 36 ++++++++++++ .../Strategies/FolderInputStrategy.cs | 26 +++++++++ src/AL2DBML.CLI/Strategies/IInputStrategy.cs | 8 +++ .../Strategies/InputStrategyFactory.cs | 26 +++++++++ .../Strategies/SingleFileInputStrategy.cs | 35 ++++++++++++ .../Strategies/WorkspaceInputStrategy.cs | 19 +++++++ src/AL2DBML.DI/ParserServiceExtensions.cs | 3 +- src/AL2DBML.Parser/AlParser.cs | 6 ++ src/AL2DBML.Tests/TestBase.cs | 2 +- 15 files changed, 276 insertions(+), 4 deletions(-) create mode 100644 src/AL2DBML.CLI/Commands/GenerateCommand.cs create mode 100644 src/AL2DBML.CLI/Enums/InputType.cs create mode 100644 src/AL2DBML.CLI/Services/FileSystemService.cs create mode 100644 src/AL2DBML.CLI/Strategies/FolderInputStrategy.cs create mode 100644 src/AL2DBML.CLI/Strategies/IInputStrategy.cs create mode 100644 src/AL2DBML.CLI/Strategies/InputStrategyFactory.cs create mode 100644 src/AL2DBML.CLI/Strategies/SingleFileInputStrategy.cs create mode 100644 src/AL2DBML.CLI/Strategies/WorkspaceInputStrategy.cs diff --git a/.gitignore b/.gitignore index bc78471..bbd09d9 100644 --- a/.gitignore +++ b/.gitignore @@ -482,3 +482,6 @@ $RECYCLE.BIN/ # Vim temporary swap files *.swp + +# Generated schema files +*.dbml diff --git a/src/AL2DBML.Application/Interfaces/IAlParser.cs b/src/AL2DBML.Application/Interfaces/IAlParser.cs index 0f3689b..9839253 100644 --- a/src/AL2DBML.Application/Interfaces/IAlParser.cs +++ b/src/AL2DBML.Application/Interfaces/IAlParser.cs @@ -12,4 +12,5 @@ public interface IAlParser DBMLTable ParseTableExtension(string alTableExtensionFileContent); DBMLColumn ParseField(string alFieldContent); OutputSchema GetOutputSchema(); + void ClearOutputSchema(); } diff --git a/src/AL2DBML.CLI/AL2DBML.CLI.csproj b/src/AL2DBML.CLI/AL2DBML.CLI.csproj index 66629b4..2962959 100644 --- a/src/AL2DBML.CLI/AL2DBML.CLI.csproj +++ b/src/AL2DBML.CLI/AL2DBML.CLI.csproj @@ -12,4 +12,14 @@ + + + + + + + + + + diff --git a/src/AL2DBML.CLI/Commands/GenerateCommand.cs b/src/AL2DBML.CLI/Commands/GenerateCommand.cs new file mode 100644 index 0000000..ccc87eb --- /dev/null +++ b/src/AL2DBML.CLI/Commands/GenerateCommand.cs @@ -0,0 +1,55 @@ +using System.ComponentModel; +using AL2DBML.Application.Interfaces; +using AL2DBML.CLI.Services; +using AL2DBML.CLI.Strategies; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace AL2DBML.CLI.Commands; + +public class GenerateSettings : CommandSettings +{ + [CommandOption("-i|--input ")] + [Description("The path to the AL project folder, AL file, or vscode workspace file of AL projects to parse.")] + public string InputPath { get; init; } = "."; + + [CommandOption("-o|--output ")] + [Description("The path to the output file or directory.")] + public string OutputPath { get; init; } = "."; + + [CommandOption("-n|--name ")] + [Description("The name of the output file (without extension). 'schema' by default.")] + public string OutputName { get; init; } = "schema"; +} + +public class GenerateCommand : AsyncCommand +{ + private readonly IAlParser _alParser; + private readonly IDBMLWriter _dbmlWriter; + + public GenerateCommand(IAlParser alParser, IDBMLWriter dbmlWriter) + { + _alParser = alParser; + _dbmlWriter = dbmlWriter; + } + + protected override async Task ExecuteAsync(CommandContext context, GenerateSettings settings, CancellationToken cancellationToken) + { + var inputType = FileSystemService.GetInputType(settings.InputPath); + if (inputType == Enums.InputType.NotSupported) + { + AnsiConsole.MarkupLine("[red]Error:[/] Unsupported input type. Please provide a valid directory, AL file, or vscode workspace file."); + return -1; + } + + var outputPath = Path.Combine(settings.OutputPath, $"{settings.OutputName}.dbml"); + + var factory = new InputStrategyFactory(inputType, _alParser); + var outputSchema = factory.Strategy.Execute(settings.InputPath); + + var dbmlContent = await _dbmlWriter.WriteDBMLAsync(outputSchema); + File.WriteAllText(outputPath, dbmlContent); + + return 0; + } +} diff --git a/src/AL2DBML.CLI/Enums/InputType.cs b/src/AL2DBML.CLI/Enums/InputType.cs new file mode 100644 index 0000000..7083567 --- /dev/null +++ b/src/AL2DBML.CLI/Enums/InputType.cs @@ -0,0 +1,9 @@ +namespace AL2DBML.CLI.Enums; + +public enum InputType +{ + Directory, + ALFile, + WorkspaceFile, + NotSupported +} diff --git a/src/AL2DBML.CLI/Program.cs b/src/AL2DBML.CLI/Program.cs index 3751555..bd28bf6 100644 --- a/src/AL2DBML.CLI/Program.cs +++ b/src/AL2DBML.CLI/Program.cs @@ -1,2 +1,39 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +using AL2DBML.CLI.Commands; +using AL2DBML.DI; +using Microsoft.Extensions.DependencyInjection; +using Spectre.Console.Cli; + + + +// Register services +var services = new ServiceCollection(); +services + .AddAL2Dbml() + .AddScoped(); + +var registrar = new TypeRegistrar(services); + +var app = new CommandApp(registrar); + +app.Configure(config => +{ + config.AddCommand("generate"); +}); + +return await app.RunAsync(args); + +public sealed class TypeRegistrar(IServiceCollection services) : ITypeRegistrar +{ + public ITypeResolver Build() => new TypeResolver(services.BuildServiceProvider()); + + public void Register(Type service, Type implementation) => services.AddSingleton(service, implementation); + + public void RegisterInstance(Type service, object implementation) => services.AddSingleton(service, implementation); + + public void RegisterLazy(Type service, Func factory) => services.AddSingleton(service, _ => factory()); +} + +public sealed class TypeResolver(IServiceProvider provider) : ITypeResolver +{ + public object? Resolve(Type? type) => type == null ? null : provider.GetService(type); +} diff --git a/src/AL2DBML.CLI/Services/FileSystemService.cs b/src/AL2DBML.CLI/Services/FileSystemService.cs new file mode 100644 index 0000000..3dbe49f --- /dev/null +++ b/src/AL2DBML.CLI/Services/FileSystemService.cs @@ -0,0 +1,36 @@ +using AL2DBML.CLI.Enums; + +namespace AL2DBML.CLI.Services; + +public static class FileSystemService +{ + public static InputType GetInputType(string path) + { + if (Directory.Exists(path)) + return InputType.Directory; + + if (File.Exists(path)) + { + var extension = Path.GetExtension(path); + if (extension.Equals(".al", StringComparison.OrdinalIgnoreCase)) + return InputType.ALFile; + if (extension.Equals(".code-workspace", StringComparison.OrdinalIgnoreCase)) + return InputType.WorkspaceFile; + } + + return InputType.NotSupported; + } + + public static List ScanDirectory(string directoryPath) + { + var paths = new List(); + var files = Directory.GetFiles(directoryPath, "*.al", SearchOption.AllDirectories); + + foreach (var file in files) + { + paths.Add(file); + } + + return paths; + } +} diff --git a/src/AL2DBML.CLI/Strategies/FolderInputStrategy.cs b/src/AL2DBML.CLI/Strategies/FolderInputStrategy.cs new file mode 100644 index 0000000..e7f0fcb --- /dev/null +++ b/src/AL2DBML.CLI/Strategies/FolderInputStrategy.cs @@ -0,0 +1,26 @@ +using AL2DBML.Application.Interfaces; +using AL2DBML.CLI.Services; +using AL2DBML.Core.Models; + +namespace AL2DBML.CLI.Strategies; + +public class FolderInputStrategy : IInputStrategy +{ + private readonly IAlParser _alParser; + + public FolderInputStrategy(IAlParser alParser) + { + _alParser = alParser; + } + + public OutputSchema Execute(string inputPath) + { + var files = FileSystemService.ScanDirectory(inputPath); + var singleFileStrategy = new SingleFileInputStrategy(_alParser); + + foreach (var file in files) + singleFileStrategy.Execute(file); + + return _alParser.GetOutputSchema(); + } +} diff --git a/src/AL2DBML.CLI/Strategies/IInputStrategy.cs b/src/AL2DBML.CLI/Strategies/IInputStrategy.cs new file mode 100644 index 0000000..6be96e5 --- /dev/null +++ b/src/AL2DBML.CLI/Strategies/IInputStrategy.cs @@ -0,0 +1,8 @@ +using AL2DBML.Core.Models; + +namespace AL2DBML.CLI.Strategies; + +public interface IInputStrategy +{ + OutputSchema Execute(string inputPath); +} diff --git a/src/AL2DBML.CLI/Strategies/InputStrategyFactory.cs b/src/AL2DBML.CLI/Strategies/InputStrategyFactory.cs new file mode 100644 index 0000000..e9985d9 --- /dev/null +++ b/src/AL2DBML.CLI/Strategies/InputStrategyFactory.cs @@ -0,0 +1,26 @@ +using AL2DBML.Application.Interfaces; +using AL2DBML.CLI.Enums; + +namespace AL2DBML.CLI.Strategies; + +public class InputStrategyFactory +{ + public IInputStrategy Strategy { get; init; } + public InputStrategyFactory(InputType inputType, IAlParser alParser) + { + switch (inputType) + { + case InputType.Directory: + Strategy = new FolderInputStrategy(alParser); + break; + case InputType.ALFile: + Strategy = new SingleFileInputStrategy(alParser); + break; + case InputType.WorkspaceFile: + Strategy = new WorkspaceInputStrategy(alParser); + break; + default: + throw new NotSupportedException($"Input type {inputType} is not supported."); + } + } +} diff --git a/src/AL2DBML.CLI/Strategies/SingleFileInputStrategy.cs b/src/AL2DBML.CLI/Strategies/SingleFileInputStrategy.cs new file mode 100644 index 0000000..17ab891 --- /dev/null +++ b/src/AL2DBML.CLI/Strategies/SingleFileInputStrategy.cs @@ -0,0 +1,35 @@ +using AL2DBML.Application.Interfaces; +using AL2DBML.Core.Enums; +using AL2DBML.Core.Models; + +namespace AL2DBML.CLI.Strategies; + +public class SingleFileInputStrategy : IInputStrategy +{ + private readonly IAlParser _alParser; + public SingleFileInputStrategy(IAlParser alParser) + { + _alParser = alParser; + } + public OutputSchema Execute(string inputPath) + { + var content = File.ReadAllText(inputPath); + switch (_alParser.DetectFileType(content)) + { + case AlFileType.Enum: + _alParser.ParseEnum(content); + break; + case AlFileType.EnumExtension: + _alParser.ParseEnumExtension(content); + break; + case AlFileType.Table: + _alParser.ParseTable(content); + break; + case AlFileType.TableExtension: + _alParser.ParseTableExtension(content); + break; + // Unknown: skip silently (unsupported files in a folder) + } + return _alParser.GetOutputSchema(); + } +} diff --git a/src/AL2DBML.CLI/Strategies/WorkspaceInputStrategy.cs b/src/AL2DBML.CLI/Strategies/WorkspaceInputStrategy.cs new file mode 100644 index 0000000..cedf267 --- /dev/null +++ b/src/AL2DBML.CLI/Strategies/WorkspaceInputStrategy.cs @@ -0,0 +1,19 @@ +using AL2DBML.Application.Interfaces; +using AL2DBML.Core.Models; + +namespace AL2DBML.CLI.Strategies; + +class WorkspaceInputStrategy : IInputStrategy +{ + private readonly IAlParser _alParser; + + public WorkspaceInputStrategy(IAlParser alParser) + { + _alParser = alParser; + } + + public OutputSchema Execute(string inputPath) + { + throw new NotImplementedException(); + } +} diff --git a/src/AL2DBML.DI/ParserServiceExtensions.cs b/src/AL2DBML.DI/ParserServiceExtensions.cs index f3082fd..094b792 100644 --- a/src/AL2DBML.DI/ParserServiceExtensions.cs +++ b/src/AL2DBML.DI/ParserServiceExtensions.cs @@ -8,7 +8,8 @@ public static class ParserServiceExtensions { public static IServiceCollection AddParser(this IServiceCollection services) { - services.AddScoped(); + // Singleton because we want to maintain state across multiple parsing operations + services.AddSingleton(); return services; } } diff --git a/src/AL2DBML.Parser/AlParser.cs b/src/AL2DBML.Parser/AlParser.cs index facc431..e8665b7 100644 --- a/src/AL2DBML.Parser/AlParser.cs +++ b/src/AL2DBML.Parser/AlParser.cs @@ -180,4 +180,10 @@ public OutputSchema GetOutputSchema() { return OutputSchemaHelper.DeepCopy(_outputSchema); } + + public void ClearOutputSchema() + { + _outputSchema.Enums.Clear(); + _outputSchema.Tables.Clear(); + } } diff --git a/src/AL2DBML.Tests/TestBase.cs b/src/AL2DBML.Tests/TestBase.cs index 4beeb0b..2c5b985 100644 --- a/src/AL2DBML.Tests/TestBase.cs +++ b/src/AL2DBML.Tests/TestBase.cs @@ -21,7 +21,7 @@ protected TestBase() protected void ResetParser() { - _parser = Services.GetRequiredService(); + _parser.ClearOutputSchema(); } protected static string LoadFixture(string path) From c215963ffd9a32d88e00006d216a61dd71775502 Mon Sep 17 00:00:00 2001 From: OGR-67 Date: Sat, 21 Mar 2026 06:40:17 +0100 Subject: [PATCH 2/4] feat: Implement workspace file parsing to extract project paths and handle folder entries --- .../Strategies/WorkspaceInputStrategy.cs | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/AL2DBML.CLI/Strategies/WorkspaceInputStrategy.cs b/src/AL2DBML.CLI/Strategies/WorkspaceInputStrategy.cs index cedf267..3f35b9a 100644 --- a/src/AL2DBML.CLI/Strategies/WorkspaceInputStrategy.cs +++ b/src/AL2DBML.CLI/Strategies/WorkspaceInputStrategy.cs @@ -1,5 +1,7 @@ +using System.Text.Json; using AL2DBML.Application.Interfaces; using AL2DBML.Core.Models; +using Spectre.Console; namespace AL2DBML.CLI.Strategies; @@ -14,6 +16,31 @@ public WorkspaceInputStrategy(IAlParser alParser) public OutputSchema Execute(string inputPath) { - throw new NotImplementedException(); + // Read the vscode workspace file to get the list of projects + var workspaceContent = File.ReadAllText(inputPath); + using var workspaceJson = JsonDocument.Parse(workspaceContent); + if (!workspaceJson.RootElement.TryGetProperty("folders", out var folders)) + { + throw new InvalidDataException("Invalid workspace file: 'folders' property not found."); + } + foreach (var folder in folders.EnumerateArray()) + { + if (!folder.TryGetProperty("path", out var path)) + { + AnsiConsole.MarkupLine($"[orange]Warning:[/] A folder entry has no 'path' property. Skipping."); + continue; // Skip if no path property + } + var projectPath = Path.Combine(Path.GetDirectoryName(inputPath) ?? string.Empty, path.GetString() ?? string.Empty); + if (Directory.Exists(projectPath)) + { + var folderStrategy = new FolderInputStrategy(_alParser); + folderStrategy.Execute(projectPath); + } + else + { + AnsiConsole.MarkupLine($"[orange]Warning:[/] Project path '{projectPath}' does not exist. Skipping this entry."); + } + } + return _alParser.GetOutputSchema(); } } From d10f1b2fa16dd9c85112f84852fbea030c0451bf Mon Sep 17 00:00:00 2001 From: OGR-67 Date: Sat, 21 Mar 2026 07:04:20 +0100 Subject: [PATCH 3/4] feat: Enhance parsing functionality with tracking and refactor input strategies --- src/AL2DBML.CLI/Commands/GenerateCommand.cs | 13 +++++++++---- src/AL2DBML.CLI/Program.cs | 8 +++++--- src/AL2DBML.CLI/Services/FileSystemService.cs | 11 ++--------- src/AL2DBML.CLI/Services/IParsingTracker.cs | 8 ++++++++ src/AL2DBML.CLI/Services/ParsingTracker.cs | 13 +++++++++++++ src/AL2DBML.CLI/Strategies/FolderInputStrategy.cs | 6 ++++-- src/AL2DBML.CLI/Strategies/InputStrategyFactory.cs | 11 ++++++----- .../Strategies/SingleFileInputStrategy.cs | 14 ++++++++++++-- .../Strategies/WorkspaceInputStrategy.cs | 9 ++++++--- src/AL2DBML.DI/ParserServiceExtensions.cs | 4 ++-- 10 files changed, 67 insertions(+), 30 deletions(-) create mode 100644 src/AL2DBML.CLI/Services/IParsingTracker.cs create mode 100644 src/AL2DBML.CLI/Services/ParsingTracker.cs diff --git a/src/AL2DBML.CLI/Commands/GenerateCommand.cs b/src/AL2DBML.CLI/Commands/GenerateCommand.cs index ccc87eb..7ed30fb 100644 --- a/src/AL2DBML.CLI/Commands/GenerateCommand.cs +++ b/src/AL2DBML.CLI/Commands/GenerateCommand.cs @@ -14,7 +14,7 @@ public class GenerateSettings : CommandSettings public string InputPath { get; init; } = "."; [CommandOption("-o|--output ")] - [Description("The path to the output file or directory.")] + [Description("The path to the output directory.")] public string OutputPath { get; init; } = "."; [CommandOption("-n|--name ")] @@ -26,11 +26,13 @@ public class GenerateCommand : AsyncCommand { private readonly IAlParser _alParser; private readonly IDBMLWriter _dbmlWriter; + private readonly IParsingTracker _tracker; - public GenerateCommand(IAlParser alParser, IDBMLWriter dbmlWriter) + public GenerateCommand(IAlParser alParser, IDBMLWriter dbmlWriter, IParsingTracker tracker) { _alParser = alParser; _dbmlWriter = dbmlWriter; + _tracker = tracker; } protected override async Task ExecuteAsync(CommandContext context, GenerateSettings settings, CancellationToken cancellationToken) @@ -43,12 +45,15 @@ protected override async Task ExecuteAsync(CommandContext context, Generate } var outputPath = Path.Combine(settings.OutputPath, $"{settings.OutputName}.dbml"); + Directory.CreateDirectory(settings.OutputPath); - var factory = new InputStrategyFactory(inputType, _alParser); + var factory = new InputStrategyFactory(inputType, _alParser, _tracker); var outputSchema = factory.Strategy.Execute(settings.InputPath); var dbmlContent = await _dbmlWriter.WriteDBMLAsync(outputSchema); - File.WriteAllText(outputPath, dbmlContent); + await File.WriteAllTextAsync(outputPath, dbmlContent, cancellationToken); + + AnsiConsole.MarkupLine($"[green]Done:[/] {_tracker.FileCount} file(s) parsed in {_tracker.Elapsed.TotalSeconds:F2}s → {Markup.Escape(outputPath)}"); return 0; } diff --git a/src/AL2DBML.CLI/Program.cs b/src/AL2DBML.CLI/Program.cs index bd28bf6..6a91f4b 100644 --- a/src/AL2DBML.CLI/Program.cs +++ b/src/AL2DBML.CLI/Program.cs @@ -1,4 +1,5 @@ using AL2DBML.CLI.Commands; +using AL2DBML.CLI.Services; using AL2DBML.DI; using Microsoft.Extensions.DependencyInjection; using Spectre.Console.Cli; @@ -9,7 +10,8 @@ var services = new ServiceCollection(); services .AddAL2Dbml() - .AddScoped(); + .AddScoped() + .AddScoped(); var registrar = new TypeRegistrar(services); @@ -26,11 +28,11 @@ public sealed class TypeRegistrar(IServiceCollection services) : ITypeRegistrar { public ITypeResolver Build() => new TypeResolver(services.BuildServiceProvider()); - public void Register(Type service, Type implementation) => services.AddSingleton(service, implementation); + public void Register(Type service, Type implementation) => services.AddScoped(service, implementation); public void RegisterInstance(Type service, object implementation) => services.AddSingleton(service, implementation); - public void RegisterLazy(Type service, Func factory) => services.AddSingleton(service, _ => factory()); + public void RegisterLazy(Type service, Func factory) => services.AddScoped(service, _ => factory()); } public sealed class TypeResolver(IServiceProvider provider) : ITypeResolver diff --git a/src/AL2DBML.CLI/Services/FileSystemService.cs b/src/AL2DBML.CLI/Services/FileSystemService.cs index 3dbe49f..2e38128 100644 --- a/src/AL2DBML.CLI/Services/FileSystemService.cs +++ b/src/AL2DBML.CLI/Services/FileSystemService.cs @@ -23,14 +23,7 @@ public static InputType GetInputType(string path) public static List ScanDirectory(string directoryPath) { - var paths = new List(); - var files = Directory.GetFiles(directoryPath, "*.al", SearchOption.AllDirectories); - - foreach (var file in files) - { - paths.Add(file); - } - - return paths; + return Directory.GetFiles(directoryPath, "*.al", SearchOption.AllDirectories) + .ToList(); } } diff --git a/src/AL2DBML.CLI/Services/IParsingTracker.cs b/src/AL2DBML.CLI/Services/IParsingTracker.cs new file mode 100644 index 0000000..6c85681 --- /dev/null +++ b/src/AL2DBML.CLI/Services/IParsingTracker.cs @@ -0,0 +1,8 @@ +namespace AL2DBML.CLI.Services; + +public interface IParsingTracker +{ + void RecordFile(); + int FileCount { get; } + TimeSpan Elapsed { get; } +} diff --git a/src/AL2DBML.CLI/Services/ParsingTracker.cs b/src/AL2DBML.CLI/Services/ParsingTracker.cs new file mode 100644 index 0000000..dd6f248 --- /dev/null +++ b/src/AL2DBML.CLI/Services/ParsingTracker.cs @@ -0,0 +1,13 @@ +using System.Diagnostics; + +namespace AL2DBML.CLI.Services; + +public class ParsingTracker : IParsingTracker +{ + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + private int _fileCount; + + public void RecordFile() => _fileCount++; + public int FileCount => _fileCount; + public TimeSpan Elapsed => _stopwatch.Elapsed; +} diff --git a/src/AL2DBML.CLI/Strategies/FolderInputStrategy.cs b/src/AL2DBML.CLI/Strategies/FolderInputStrategy.cs index e7f0fcb..cf8bcf2 100644 --- a/src/AL2DBML.CLI/Strategies/FolderInputStrategy.cs +++ b/src/AL2DBML.CLI/Strategies/FolderInputStrategy.cs @@ -7,16 +7,18 @@ namespace AL2DBML.CLI.Strategies; public class FolderInputStrategy : IInputStrategy { private readonly IAlParser _alParser; + private readonly IParsingTracker _tracker; - public FolderInputStrategy(IAlParser alParser) + public FolderInputStrategy(IAlParser alParser, IParsingTracker tracker) { _alParser = alParser; + _tracker = tracker; } public OutputSchema Execute(string inputPath) { var files = FileSystemService.ScanDirectory(inputPath); - var singleFileStrategy = new SingleFileInputStrategy(_alParser); + var singleFileStrategy = new SingleFileInputStrategy(_alParser, _tracker); foreach (var file in files) singleFileStrategy.Execute(file); diff --git a/src/AL2DBML.CLI/Strategies/InputStrategyFactory.cs b/src/AL2DBML.CLI/Strategies/InputStrategyFactory.cs index e9985d9..8346fef 100644 --- a/src/AL2DBML.CLI/Strategies/InputStrategyFactory.cs +++ b/src/AL2DBML.CLI/Strategies/InputStrategyFactory.cs @@ -1,23 +1,24 @@ using AL2DBML.Application.Interfaces; using AL2DBML.CLI.Enums; +using AL2DBML.CLI.Services; namespace AL2DBML.CLI.Strategies; public class InputStrategyFactory { - public IInputStrategy Strategy { get; init; } - public InputStrategyFactory(InputType inputType, IAlParser alParser) + public IInputStrategy Strategy { get; } + public InputStrategyFactory(InputType inputType, IAlParser alParser, IParsingTracker tracker) { switch (inputType) { case InputType.Directory: - Strategy = new FolderInputStrategy(alParser); + Strategy = new FolderInputStrategy(alParser, tracker); break; case InputType.ALFile: - Strategy = new SingleFileInputStrategy(alParser); + Strategy = new SingleFileInputStrategy(alParser, tracker); break; case InputType.WorkspaceFile: - Strategy = new WorkspaceInputStrategy(alParser); + Strategy = new WorkspaceInputStrategy(alParser, tracker); break; default: throw new NotSupportedException($"Input type {inputType} is not supported."); diff --git a/src/AL2DBML.CLI/Strategies/SingleFileInputStrategy.cs b/src/AL2DBML.CLI/Strategies/SingleFileInputStrategy.cs index 17ab891..0abc595 100644 --- a/src/AL2DBML.CLI/Strategies/SingleFileInputStrategy.cs +++ b/src/AL2DBML.CLI/Strategies/SingleFileInputStrategy.cs @@ -1,4 +1,5 @@ using AL2DBML.Application.Interfaces; +using AL2DBML.CLI.Services; using AL2DBML.Core.Enums; using AL2DBML.Core.Models; @@ -7,26 +8,35 @@ namespace AL2DBML.CLI.Strategies; public class SingleFileInputStrategy : IInputStrategy { private readonly IAlParser _alParser; - public SingleFileInputStrategy(IAlParser alParser) + private readonly IParsingTracker _tracker; + + public SingleFileInputStrategy(IAlParser alParser, IParsingTracker tracker) { _alParser = alParser; + _tracker = tracker; } + public OutputSchema Execute(string inputPath) { var content = File.ReadAllText(inputPath); - switch (_alParser.DetectFileType(content)) + var fileType = _alParser.DetectFileType(content); + switch (fileType) { case AlFileType.Enum: _alParser.ParseEnum(content); + _tracker.RecordFile(); break; case AlFileType.EnumExtension: _alParser.ParseEnumExtension(content); + _tracker.RecordFile(); break; case AlFileType.Table: _alParser.ParseTable(content); + _tracker.RecordFile(); break; case AlFileType.TableExtension: _alParser.ParseTableExtension(content); + _tracker.RecordFile(); break; // Unknown: skip silently (unsupported files in a folder) } diff --git a/src/AL2DBML.CLI/Strategies/WorkspaceInputStrategy.cs b/src/AL2DBML.CLI/Strategies/WorkspaceInputStrategy.cs index 3f35b9a..f3d23f7 100644 --- a/src/AL2DBML.CLI/Strategies/WorkspaceInputStrategy.cs +++ b/src/AL2DBML.CLI/Strategies/WorkspaceInputStrategy.cs @@ -1,5 +1,6 @@ using System.Text.Json; using AL2DBML.Application.Interfaces; +using AL2DBML.CLI.Services; using AL2DBML.Core.Models; using Spectre.Console; @@ -8,10 +9,12 @@ namespace AL2DBML.CLI.Strategies; class WorkspaceInputStrategy : IInputStrategy { private readonly IAlParser _alParser; + private readonly IParsingTracker _tracker; - public WorkspaceInputStrategy(IAlParser alParser) + public WorkspaceInputStrategy(IAlParser alParser, IParsingTracker tracker) { _alParser = alParser; + _tracker = tracker; } public OutputSchema Execute(string inputPath) @@ -33,12 +36,12 @@ public OutputSchema Execute(string inputPath) var projectPath = Path.Combine(Path.GetDirectoryName(inputPath) ?? string.Empty, path.GetString() ?? string.Empty); if (Directory.Exists(projectPath)) { - var folderStrategy = new FolderInputStrategy(_alParser); + var folderStrategy = new FolderInputStrategy(_alParser, _tracker); folderStrategy.Execute(projectPath); } else { - AnsiConsole.MarkupLine($"[orange]Warning:[/] Project path '{projectPath}' does not exist. Skipping this entry."); + AnsiConsole.MarkupLine($"[orange]Warning:[/] Project path '{Markup.Escape(projectPath)}' does not exist. Skipping this entry."); } } return _alParser.GetOutputSchema(); diff --git a/src/AL2DBML.DI/ParserServiceExtensions.cs b/src/AL2DBML.DI/ParserServiceExtensions.cs index 094b792..e290986 100644 --- a/src/AL2DBML.DI/ParserServiceExtensions.cs +++ b/src/AL2DBML.DI/ParserServiceExtensions.cs @@ -8,8 +8,8 @@ public static class ParserServiceExtensions { public static IServiceCollection AddParser(this IServiceCollection services) { - // Singleton because we want to maintain state across multiple parsing operations - services.AddSingleton(); + // Scoped because we want to maintain state across multiple parsing operations + services.AddScoped(); return services; } } From e9a1961ef65229c7f7f0b6b0563e4df826e1663a Mon Sep 17 00:00:00 2001 From: OGR-67 Date: Sat, 21 Mar 2026 07:23:46 +0100 Subject: [PATCH 4/4] feat: Enhance workspace input strategy with validation for 'folders' property --- src/AL2DBML.CLI/Program.cs | 2 +- src/AL2DBML.CLI/Strategies/WorkspaceInputStrategy.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/AL2DBML.CLI/Program.cs b/src/AL2DBML.CLI/Program.cs index 6a91f4b..1bcdaf0 100644 --- a/src/AL2DBML.CLI/Program.cs +++ b/src/AL2DBML.CLI/Program.cs @@ -26,7 +26,7 @@ public sealed class TypeRegistrar(IServiceCollection services) : ITypeRegistrar { - public ITypeResolver Build() => new TypeResolver(services.BuildServiceProvider()); + public ITypeResolver Build() => new TypeResolver(services.BuildServiceProvider().CreateScope().ServiceProvider); public void Register(Type service, Type implementation) => services.AddScoped(service, implementation); diff --git a/src/AL2DBML.CLI/Strategies/WorkspaceInputStrategy.cs b/src/AL2DBML.CLI/Strategies/WorkspaceInputStrategy.cs index f3d23f7..bab883d 100644 --- a/src/AL2DBML.CLI/Strategies/WorkspaceInputStrategy.cs +++ b/src/AL2DBML.CLI/Strategies/WorkspaceInputStrategy.cs @@ -26,6 +26,10 @@ public OutputSchema Execute(string inputPath) { throw new InvalidDataException("Invalid workspace file: 'folders' property not found."); } + if (folders.ValueKind != JsonValueKind.Array) + { + throw new InvalidDataException("Invalid workspace file: 'folders' property must be an array."); + } foreach (var folder in folders.EnumerateArray()) { if (!folder.TryGetProperty("path", out var path))