From 20a285a6494594bf0c1ed6b3145935777b0886f5 Mon Sep 17 00:00:00 2001 From: ismailbennani Date: Sat, 22 Mar 2025 11:36:13 +0100 Subject: [PATCH 1/4] try commandlineparser --- FacturXDotNet.CLI/Extract/ExtractCommand.cs | 11 ++ FacturXDotNet.CLI/Extract/ExtractOption.cs | 26 ++++ FacturXDotNet.CLI/Program.cs | 138 +++++++------------- FacturXDotNet.sln.DotSettings.user | 1 + FacturXDotNet/FacturXDotNet.csproj | 1 + 5 files changed, 88 insertions(+), 89 deletions(-) create mode 100644 FacturXDotNet.CLI/Extract/ExtractCommand.cs create mode 100644 FacturXDotNet.CLI/Extract/ExtractOption.cs diff --git a/FacturXDotNet.CLI/Extract/ExtractCommand.cs b/FacturXDotNet.CLI/Extract/ExtractCommand.cs new file mode 100644 index 00000000..0de3cfde --- /dev/null +++ b/FacturXDotNet.CLI/Extract/ExtractCommand.cs @@ -0,0 +1,11 @@ +namespace FacturXDotNet.CLI.Extract; + +static class ExtractCommand +{ + public static async Task RunAsync(ExtractOption options) + { + Console.WriteLine($"Extracting {options.Path}..."); + Console.WriteLine($"CII {options.CiiPath ?? "(null)"}..."); + Console.WriteLine($"XMP {options.XmpPath ?? "(null)"}..."); + } +} diff --git a/FacturXDotNet.CLI/Extract/ExtractOption.cs b/FacturXDotNet.CLI/Extract/ExtractOption.cs new file mode 100644 index 00000000..74f99d24 --- /dev/null +++ b/FacturXDotNet.CLI/Extract/ExtractOption.cs @@ -0,0 +1,26 @@ +using CommandLine; + +namespace FacturXDotNet.CLI.Extract; + +[Verb("extract", HelpText = "Extracts the content of a Factur-X PDF.")] +class ExtractOption +{ + const string ExtractionTargetGroupName = "Extraction target"; + + [Value(0, MetaName = "PATH", Required = true, HelpText = "The path to the Factur-X PDF.")] + public string Path { get; set; } = string.Empty; + + [Option( + "cii", + Group = ExtractionTargetGroupName, + HelpText = "Extracts the content of the CII XML. Optionally specify a path, otherwise the CII XML will be saved next to the PDF with the same name." + )] + public string? CiiPath { get; set; } + + [Option( + "xmp", + Group = ExtractionTargetGroupName, + HelpText = "Extracts the content of the XMP metadata. Optionally specify a path, otherwise the XMP metadata will be saved next to the PDF with the same name." + )] + public string? XmpPath { get; set; } +} diff --git a/FacturXDotNet.CLI/Program.cs b/FacturXDotNet.CLI/Program.cs index 07f2d533..d0a54ecf 100644 --- a/FacturXDotNet.CLI/Program.cs +++ b/FacturXDotNet.CLI/Program.cs @@ -1,14 +1,8 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using FacturXDotNet; -using FacturXDotNet.CLI; -using FacturXDotNet.Models; -using FacturXDotNet.Parsing; -using FacturXDotNet.Parsing.CII; -using FacturXDotNet.Parsing.XMP; -using FacturXDotNet.Validation; -using FacturXDotNet.Validation.BusinessRules; -using Microsoft.Extensions.Configuration; +using System.Diagnostics; +using System.Reflection; +using CommandLine; +using CommandLine.Text; +using FacturXDotNet.CLI.Extract; using Microsoft.Extensions.Logging; using Serilog; using Serilog.Extensions.Logging; @@ -21,99 +15,65 @@ try { - SourceGenerationContext sourceGenerationContext = - new(new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true, Converters = { new JsonStringEnumConverter() } }); - - IConfigurationRoot configuration = new ConfigurationBuilder().AddEnvironmentVariables().Build(); - Options options = configuration.Get() ?? new Options(); - string environment = options.Environment ?? string.Empty; - - await using FileStream example = File.OpenRead(@"D:\source\repos\FacturXDotNet\FacturXDotNet.CLI\Examples\Facture_F20220023-LE_FOURNISSEUR-POUR-LE_CLIENT_MINIMUM.pdf"); - - FacturXParser parser = new( - new FacturXParserOptions + Parser parser = new( + settings => { - Xmp = - { - Logger = environment.Equals("development", StringComparison.InvariantCultureIgnoreCase) ? loggerFactory.CreateLogger() : null - }, - Cii = - { - Logger = environment.Equals("development", StringComparison.InvariantCultureIgnoreCase) ? loggerFactory.CreateLogger() : null - } + settings.AutoVersion = true; + settings.AutoHelp = true; + settings.CaseSensitive = false; + settings.CaseInsensitiveEnumValues = true; + settings.HelpWriter = null; } ); - FacturX facturX = await parser.ParseFacturXPdfAsync(example); - logger.LogInformation("-------------"); - logger.LogInformation(" XMP"); - logger.LogInformation("-------------"); - logger.LogInformation("{XMP}", JsonSerializer.Serialize(facturX.XmpMetadata, sourceGenerationContext.XmpMetadata)); - - logger.LogInformation("-------------"); - logger.LogInformation(" CII"); - logger.LogInformation("-------------"); - logger.LogInformation("{CII}", JsonSerializer.Serialize(facturX.CrossIndustryInvoice, sourceGenerationContext.CrossIndustryInvoice)); - - FacturXValidator validator = new(); - FacturXValidationResult validationResult = validator.GetValidationResult(facturX); - - logger.LogInformation("-------------"); - logger.LogInformation(" VALIDATION"); - logger.LogInformation("-------------"); - - logger.LogInformation("Success: {Success}", validationResult.Success); - logger.LogInformation("Actual profile: {Profile}", validationResult.ValidProfiles.GetMaxProfile()); - - logger.LogInformation("Details:"); - - if (validationResult.Fatal.Count > 0) - { - logger.LogError("- Failed: ({FailedCount})", validationResult.Fatal.Count); - } - else - { - logger.LogInformation("- Failed: ({FailedCount})", validationResult.Fatal.Count); - } - foreach (FacturXBusinessRule rule in validationResult.Fatal) - { - logger.LogError(" - KO {Rule}", rule.Format()); - } + ParserResult? parsed = parser.ParseArguments(args); + await parsed.WithParsedAsync(ExtractCommand.RunAsync); + await parsed.WithNotParsedAsync(errs => NotParsedAsync(parsed, errs)); +} +catch (Exception exn) +{ + logger.LogCritical(exn, "Unhandled exception."); +} +return; - logger.LogInformation("- Passed: ({PassedCount})", validationResult.Passed.Count); - foreach (FacturXBusinessRule? rule in validationResult.Passed) +async Task NotParsedAsync(ParserResult parsed1, IEnumerable errs) +{ + IEnumerable enumerable = errs as Error[] ?? errs.ToArray(); + if (enumerable.IsVersion()) { - logger.LogInformation(" - OK {Rule}", rule.Format()); + await Console.Out.WriteLineAsync(GetVersion()); } - - logger.LogInformation("- Expected to fail: ({ExpectedToFailCount})", validationResult.ExpectedToFail.Count); - foreach (FacturXBusinessRule? rule in validationResult.ExpectedToFail) + else if (enumerable.IsHelp()) { - logger.LogInformation(" - KO {Rule}", rule.Format()); + await Console.Out.WriteLineAsync(GetHelp(parsed1)); } - - logger.LogInformation("- Skipped: ({SkippedCount})", validationResult.Skipped.Count); - foreach (FacturXBusinessRule? rule in validationResult.Skipped) + else { - logger.LogInformation(" - ?? {Rule}", rule.Format()); + await Console.Error.WriteLineAsync(GetHelp(parsed1)); } } -catch (Exception exn) + +string GetVersion() { - logger.LogCritical(exn, "Unhandled exception."); + Assembly assembly = Assembly.GetExecutingAssembly(); + FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(assembly.Location); + return fileVersionInfo.ProductVersion ?? "~dev"; } -namespace FacturXDotNet.CLI +HelpText GetHelp(ParserResult parserResult) { - class Options - { - public string? Environment { get; set; } - } + HelpText? helpText = HelpText.AutoBuild( + parserResult, + h => + { + h.AdditionalNewLineAfterOption = false; + h.AddEnumValuesToHelpText = true; + h.Heading = $"FacturX.NET {GetVersion()}"; + h.Copyright = "Copyright (C) 2025 Ismail Bennani"; + return HelpText.DefaultParsingErrorsHandler(parserResult, h); + }, + e => e + ); - [JsonSourceGenerationOptions(WriteIndented = true)] - [JsonSerializable(typeof(CrossIndustryInvoice))] - [JsonSerializable(typeof(XmpMetadata))] - partial class SourceGenerationContext : JsonSerializerContext - { - } + return helpText; } diff --git a/FacturXDotNet.sln.DotSettings.user b/FacturXDotNet.sln.DotSettings.user index 20a78179..15302b90 100644 --- a/FacturXDotNet.sln.DotSettings.user +++ b/FacturXDotNet.sln.DotSettings.user @@ -1,4 +1,5 @@  + ForceIncluded ForceIncluded ForceIncluded ForceIncluded diff --git a/FacturXDotNet/FacturXDotNet.csproj b/FacturXDotNet/FacturXDotNet.csproj index 8c70bd54..ef912604 100644 --- a/FacturXDotNet/FacturXDotNet.csproj +++ b/FacturXDotNet/FacturXDotNet.csproj @@ -19,6 +19,7 @@ + From 321638d44b900d95280fa9ae6bf2a34ce1d42edd Mon Sep 17 00:00:00 2001 From: ismailbennani Date: Sat, 22 Mar 2025 15:53:52 +0100 Subject: [PATCH 2/4] use System.CommandLine instead of CommandLineArgs --- Benchmark/Benchmark.csproj | 1 - Directory.Build.props | 1 - FacturXDotNet.CLI/Extract/CommandBase.cs | 41 +++++++++ FacturXDotNet.CLI/Extract/ExtractCommand.cs | 63 ++++++++++++-- FacturXDotNet.CLI/Extract/ExtractOption.cs | 26 ------ FacturXDotNet.CLI/FacturXDotNet.CLI.csproj | 10 +-- FacturXDotNet.CLI/Program.cs | 84 ++++--------------- FacturXDotNet.sln.DotSettings.user | 5 ++ FacturXDotNet/FacturXDotNet.csproj | 2 +- .../Tests.FacturXDotNet.csproj | 7 +- 10 files changed, 129 insertions(+), 111 deletions(-) create mode 100644 FacturXDotNet.CLI/Extract/CommandBase.cs delete mode 100644 FacturXDotNet.CLI/Extract/ExtractOption.cs diff --git a/Benchmark/Benchmark.csproj b/Benchmark/Benchmark.csproj index 20d2b953..aff03dbc 100644 --- a/Benchmark/Benchmark.csproj +++ b/Benchmark/Benchmark.csproj @@ -10,7 +10,6 @@ latest enable enable - false diff --git a/Directory.Build.props b/Directory.Build.props index c47a908c..56501af9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,6 +3,5 @@ latest enable enable - true diff --git a/FacturXDotNet.CLI/Extract/CommandBase.cs b/FacturXDotNet.CLI/Extract/CommandBase.cs new file mode 100644 index 00000000..eeae1f46 --- /dev/null +++ b/FacturXDotNet.CLI/Extract/CommandBase.cs @@ -0,0 +1,41 @@ +using System.CommandLine; +using System.CommandLine.NamingConventionBinder; +using System.CommandLine.Parsing; + +namespace FacturXDotNet.CLI.Extract; + +abstract class CommandBase(string name, string description, IReadOnlyList? arguments = null, IReadOnlyList