Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Benchmark/Benchmark.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
Expand Down
1 change: 0 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
64 changes: 64 additions & 0 deletions FacturXDotNet.CLI/Extract/CommandBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
using System.CommandLine.Parsing;
using Spectre.Console;

namespace FacturXDotNet.CLI.Extract;

abstract class CommandBase<TOptions>(string name, string description, IReadOnlyList<Argument>? arguments = null, IReadOnlyList<Option>? options = null)
{
public string Name { get; } = name;
public string Description { get; } = description;
public IReadOnlyList<Argument> Arguments => arguments ?? [];
public IReadOnlyList<Option> Options => options ?? [];

public Command GetCommand()
{
Command command = new(Name, Description);

foreach (Argument argument in Arguments)
{
command.Add(argument);
}

foreach (Option option in Options)
{
command.Add(option);
}

command.Action = CommandHandler.Create(
async (ParseResult result, CancellationToken cancellationToken) =>
{
try
{
TOptions opt = ParseOptions(result.CommandResult);
return await RunImplAsync(opt, cancellationToken);
}
catch (Exception exception)
{
AnsiConsole.WriteException(exception, ExceptionFormats.ShortenEverything);
return 1;
}
}
);

command.Validators.Add(
result =>
{
TOptions opt = ParseOptions(result);
ValidateOptions(result, opt);
}
);

return command;
}

protected abstract Task<int> RunImplAsync(TOptions options, CancellationToken cancellationToken = default);
protected abstract TOptions ParseOptions(CommandResult result);
protected virtual void ValidateOptions(CommandResult result, TOptions opt) { }
}

static class CommandBaseExtensions
{
public static void AddCommand<T>(this RootCommand rootCommand, CommandBase<T> command) => rootCommand.Subcommands.Add(command.GetCommand());
}
160 changes: 160 additions & 0 deletions FacturXDotNet.CLI/Extract/ExtractCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using System.CommandLine;
using System.CommandLine.Parsing;
using FacturXDotNet.Parsing;
using Spectre.Console;

namespace FacturXDotNet.CLI.Extract;

class ExtractCommand() : CommandBase<ExtractCommandOptions>("extract", "Extracts the content of a Factur-X PDF.", [PathArgument], [CiiOption, CiiAttachmentOption, XmpOption])
{
static readonly Argument<FileInfo> PathArgument;
static readonly Option<string> CiiOption;
static readonly Option<string> CiiAttachmentOption;
static readonly Option<string> XmpOption;

static ExtractCommand()
{
PathArgument = new Argument<FileInfo>("path")
{
Description = "The path to the Factur-X PDF.",
Validators =
{
result =>
{
FileInfo path = result.GetValueOrDefault<FileInfo>();
if (!path.Exists)
{
result.AddError($"Could not find file '{path.FullName}'.");
}
}
}
};
CiiOption = new Option<string>("--cii")
{
Description = "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.",
HelpName = "path",
Arity = ArgumentArity.ZeroOrOne
};
CiiAttachmentOption = new Option<string>("--cii-attachment")
{
Description = "The name of the CII attachment.",
HelpName = "name",
DefaultValueFactory = _ => "factur-x.xml"
};
XmpOption = new Option<string>("--xmp")
{
Description = "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.",
HelpName = "path",
Arity = ArgumentArity.ZeroOrOne
};
}

protected override ExtractCommandOptions ParseOptions(CommandResult result) =>
new()
{
Path = result.GetValue(PathArgument)!,
Cii = result.GetResult(CiiOption) is not null ? result.GetValue(CiiOption) ?? "" : null,
CiiAttachment = result.GetValue(CiiAttachmentOption),
Xmp = result.GetResult(XmpOption) is not null ? result.GetValue(XmpOption) ?? "" : null
};

protected override void ValidateOptions(CommandResult result, ExtractCommandOptions options)
{
if (options is { ExportCii: false, ExportXmp: false })
{
result.AddError("At least one of --cii or --xmp must be specified.");
}
}

protected override async Task<int> RunImplAsync(ExtractCommandOptions options, CancellationToken cancellationToken = default)
{
if (options.ExportCii)
{
await AnsiConsole.Status()
.Spinner(Spinner.Known.Default)
.StartAsync(
"Exporting CII XML...",
async ctx =>
{
string outputPath = string.IsNullOrWhiteSpace(options.Cii) ? Path.ChangeExtension(options.Path.FullName, ".xml") : options.Cii;
await ExtractCii(options.Path, options.CiiAttachment, outputPath, cancellationToken);

AnsiConsole.MarkupLine("[green]:check_mark:[/] Extracted CII XML to '[bold]{0}[/]'.", outputPath);
}
);
}

if (options.ExportXmp)
{
await AnsiConsole.Status()
.Spinner(Spinner.Known.Default)
.StartAsync(
"Exporting XMP metadata...",
async ctx =>
{
string outputPath = string.IsNullOrWhiteSpace(options.Xmp) ? Path.ChangeExtension(options.Path.FullName, ".xmp") : options.Xmp;
await ExtractXmp(options.Path, outputPath, cancellationToken);

AnsiConsole.MarkupLine("[green]:check_mark:[/] Extracted XMP metadata to '[bold]{0}[/]'.", outputPath);
}
);
}

return 0;
}

static async Task ExtractCii(FileInfo pdfPath, string? ciiAttachment, string outputPath, CancellationToken cancellationToken)
{
await using FileStream stream = pdfPath.OpenRead();

FacturXCrossIndustryInvoiceExtractor extractor = new(new FacturXCrossIndustryInvoiceExtractorOptions { CiiXmlAttachmentName = ciiAttachment });
await using Stream xmpStream = extractor.ExtractCiiAsync(stream);

await using FileStream xmpFile = File.Open(outputPath, FileMode.Create);
await xmpStream.CopyToAsync(xmpFile, cancellationToken);
}

static async Task ExtractXmp(FileInfo pdfPath, string outputPath, CancellationToken cancellationToken)
{
await using FileStream stream = pdfPath.OpenRead();

FacturXXmpExtractor extractor = new();
await using Stream xmpStream = extractor.ExtractXmpAsync(stream);

await using FileStream xmpFile = File.Open(outputPath, FileMode.Create);
await xmpStream.CopyToAsync(xmpFile, cancellationToken);
}
}

public class ExtractCommandOptions
{
/// <summary>
/// The path to the Factur-X PDF.
/// </summary>
public FileInfo Path { get; set; } = null!;

/// <summary>
/// True if the CII XML should be extracted.
/// </summary>
public bool ExportCii => Cii is not null;

/// <summary>
/// The path to extract the CII XML to. If empty, the CII XML will be saved next to the PDF with the same name. If null, the CII XML will not be extracted.
/// </summary>
public string? Cii { get; set; }

/// <summary>
/// The name of the CII attachment.
/// </summary>
public string? CiiAttachment { get; set; }

/// <summary>
/// True if the XMP metadata should be extracted.
/// </summary>
public bool ExportXmp => Xmp is not null;

/// <summary>
/// The path to extract the XMP metadata to. If empty, the XMP metadata will be saved next to the PDF with the same name. If null, the XMP metadata will not be extracted.
/// </summary>
public string? Xmp { get; set; }
}
21 changes: 16 additions & 5 deletions FacturXDotNet.CLI/FacturXDotNet.CLI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,23 @@
<OutputType>exe</OutputType>
</PropertyGroup>

<PropertyGroup>
<Product>FacturX.NET CLI</Product>
<Company>Ismail Bennani</Company>
<Copyright>Copyright © $([System.DateTime]::Now.Year) $(Company)</Copyright>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.3" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0-preview.2.25163.2" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.2.25163.2" />
<PackageReference Include="Spectre.Console" Version="0.49.2-preview.0.76" />
<PackageReference Include="Spectre.Console.Analyzer" Version="1.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta5.25170.1" />
<PackageReference Include="System.CommandLine.NamingConventionBinder" Version="2.0.0-beta5.25170.1" />
<PackageReference Include="System.CommandLine.Rendering" Version="0.4.0-alpha.22272.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading