| | | 1 | | using ArturRios.Data.Export.Interfaces; |
| | | 2 | | using ArturRios.Output; |
| | | 3 | | using Microsoft.Extensions.Logging; |
| | | 4 | | |
| | | 5 | | namespace ArturRios.Data.Export.Exporters; |
| | | 6 | | |
| | | 7 | | /// <summary> |
| | | 8 | | /// Base for exporters: handles null-guarding, envelope conversion, cancellation propagation, and |
| | | 9 | | /// file-stream lifetime. Concrete exporters implement <see cref="WriteCoreAsync" />. |
| | | 10 | | /// </summary> |
| | | 11 | | /// <typeparam name="T">The record type.</typeparam> |
| | | 12 | | /// <param name="logger"> |
| | | 13 | | /// Optional logger. Envelopes carry no exception text, so a write failure is otherwise |
| | | 14 | | /// undiagnosable: supply a logger and the full exception, plus the destination path for file |
| | | 15 | | /// writes, is written at <see cref="LogLevel.Error" />. Record contents are never logged. |
| | | 16 | | /// Resolved from DI when logging is registered. |
| | | 17 | | /// </param> |
| | 70 | 18 | | public abstract class ExporterBase<T>(ILogger? logger = null) : IExporter<T> where T : class |
| | | 19 | | { |
| | | 20 | | /// <summary>Message returned when a write fails.</summary> |
| | | 21 | | protected const string ExportFailedMessage = "An export error occurred."; |
| | | 22 | | |
| | | 23 | | /// <summary>Message returned when the caller passes no records.</summary> |
| | | 24 | | protected const string NullDataMessage = "An export error occurred: data is null."; |
| | | 25 | | |
| | | 26 | | /// <summary>Message returned when the caller passes no destination stream.</summary> |
| | | 27 | | protected const string NullDestinationMessage = "An export error occurred: destination is null."; |
| | | 28 | | |
| | | 29 | | /// <summary>Message returned when the caller passes no destination path.</summary> |
| | | 30 | | protected const string EmptyPathMessage = "An export error occurred: path is null or empty."; |
| | | 31 | | |
| | | 32 | | /// <inheritdoc /> |
| | | 33 | | public Task<ProcessOutput> WriteAsync(IEnumerable<T> data, Stream destination, CancellationToken ct = default) => |
| | 84 | 34 | | GuardedWriteAsync(data, destination, stream => WriteCoreAsync(data, stream, ct)); |
| | | 35 | | |
| | | 36 | | /// <inheritdoc /> |
| | | 37 | | public Task<ProcessOutput> WriteToFileAsync(IEnumerable<T> data, string path, CancellationToken ct = default) => |
| | 8 | 38 | | GuardedFileAsync(data, path, stream => WriteCoreAsync(data, stream, ct)); |
| | | 39 | | |
| | | 40 | | /// <summary>Guards a stream write: null checks, envelope conversion, cancellation propagation.</summary> |
| | | 41 | | protected async Task<ProcessOutput> GuardedWriteAsync(IEnumerable<T> data, Stream destination, |
| | | 42 | | Func<Stream, Task> write) |
| | | 43 | | { |
| | 52 | 44 | | if (data is null) return ProcessOutput.New.WithError(NullDataMessage); |
| | 46 | 45 | | if (destination is null) return ProcessOutput.New.WithError(NullDestinationMessage); |
| | | 46 | | |
| | | 47 | | try |
| | | 48 | | { |
| | 42 | 49 | | await write(destination).ConfigureAwait(false); |
| | 36 | 50 | | return ProcessOutput.New; |
| | | 51 | | } |
| | 4 | 52 | | catch (OperationCanceledException) { throw; } |
| | 8 | 53 | | catch (Exception ex) { return Fail(ex, destination: null); } |
| | 46 | 54 | | } |
| | | 55 | | |
| | | 56 | | /// <summary>Guards a file write: opens/truncates the file, then delegates to <paramref name="write" />.</summary> |
| | | 57 | | protected async Task<ProcessOutput> GuardedFileAsync(IEnumerable<T> data, string path, Func<Stream, Task> write) |
| | | 58 | | { |
| | 4 | 59 | | if (data is null) return ProcessOutput.New.WithError(NullDataMessage); |
| | 4 | 60 | | if (string.IsNullOrEmpty(path)) return ProcessOutput.New.WithError(EmptyPathMessage); |
| | | 61 | | |
| | | 62 | | try |
| | | 63 | | { |
| | 4 | 64 | | var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); |
| | | 65 | | |
| | 4 | 66 | | await using var streamScope = stream.ConfigureAwait(false); |
| | | 67 | | |
| | 4 | 68 | | await write(stream).ConfigureAwait(false); |
| | 2 | 69 | | return ProcessOutput.New; |
| | 0 | 70 | | } |
| | 0 | 71 | | catch (OperationCanceledException) { throw; } |
| | 4 | 72 | | catch (Exception ex) { return Fail(ex, path); } |
| | 4 | 73 | | } |
| | | 74 | | |
| | | 75 | | /// <summary> |
| | | 76 | | /// Logs the failure when a logger is configured, and returns the caller-safe envelope. |
| | | 77 | | /// Exception text embeds absolute paths and OS error detail, so it goes to the log and |
| | | 78 | | /// never to the caller. |
| | | 79 | | /// </summary> |
| | | 80 | | /// <param name="ex">The exception caught by a guard.</param> |
| | | 81 | | /// <param name="destination">The file path being written, or <see langword="null" /> for a stream write.</param> |
| | | 82 | | protected ProcessOutput Fail(Exception ex, string? destination) |
| | | 83 | | { |
| | 6 | 84 | | logger?.LogError(ex, "Export failed. Exporter: {Exporter}, record: {Record}, destination: {Destination}", |
| | 6 | 85 | | GetType().Name, typeof(T).Name, destination ?? "<stream>"); |
| | | 86 | | |
| | 6 | 87 | | return ProcessOutput.New.WithError(ExportFailedMessage); |
| | | 88 | | } |
| | | 89 | | |
| | | 90 | | /// <summary>Format-specific write. Implementations must honor <paramref name="ct" /> and not dispose the stream.</s |
| | | 91 | | protected abstract Task WriteCoreAsync(IEnumerable<T> data, Stream destination, CancellationToken ct); |
| | | 92 | | } |