Skip to content

Commit

Permalink
Add the ability to apply rules to a file
Browse files Browse the repository at this point in the history
  • Loading branch information
wgnf committed Oct 23, 2021
1 parent 8d90dff commit b5c528f
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 50 deletions.
2 changes: 1 addition & 1 deletion src/CsvProc9000/Csv/CsvColumn.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace CsvProc9000.Csv
{
public record CsvColumn(int Index, string Name);
public record CsvColumn(string Name);
}
18 changes: 5 additions & 13 deletions src/CsvProc9000/Csv/CsvFile.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;

namespace CsvProc9000.Csv
{
public class CsvFile
{
private readonly List<CsvColumn> _columns = new();
private readonly List<CsvRow> _rows = new();

public CsvFile([NotNull] IEnumerable<string> columns)
public CsvFile([NotNull] string fileName)
{
if (columns == null) throw new ArgumentNullException(nameof(columns));

var columnsArray = columns.ToArray();
for (var index = 0; index < columnsArray.Length; index++)
{
_columns.Add(new CsvColumn(index, columnsArray[index]));
}
FileName = fileName ?? throw new ArgumentNullException(nameof(fileName));
}

public string FileName { get; }

public IReadOnlyCollection<CsvColumn> Columns => _columns;

public IReadOnlyCollection<CsvRow> Rows => _rows;
public IEnumerable<CsvRow> Rows => _rows;

public void AddRow([NotNull] CsvRow row)
{
Expand Down
30 changes: 16 additions & 14 deletions src/CsvProc9000/Csv/CsvReader.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Abstractions;
Expand Down Expand Up @@ -32,44 +33,45 @@ public async Task<CsvFile> ReadAsync(string fileName, string delimiter)
};
using var csvReader = new CsvHelper.CsvReader(streamReader, configuration);

var csvFile = await CreateCsvFileFromHeader(fileName, csvReader);
await ProcessCsvRows(csvReader, csvFile);
var csvFile = new CsvFile(fileName);
var headers = await GetHeaders(fileName, csvReader);
await ProcessCsvRows(csvReader, csvFile, headers);

return csvFile;
}

private static async Task<CsvFile> CreateCsvFileFromHeader(string fileName, IReader csvReader)
private static async Task<IEnumerable<string>> GetHeaders(string fileName, IReader csvReader)
{
await csvReader.ReadAsync();
if (!csvReader.ReadHeader())
throw new InvalidOperationException($"Could not read header of file {fileName}");

var header = csvReader.HeaderRecord;
if (!header.Any())
var headers = csvReader.HeaderRecord;
if (!headers.Any())
throw new InvalidOperationException($"Did not find any headers for file {fileName}");

var csvFile = new CsvFile(header);
return csvFile;

return headers;
}

private static async Task ProcessCsvRows(IReader csvReader, CsvFile csvFile)
private static async Task ProcessCsvRows(IReader csvReader, CsvFile csvFile, IEnumerable<string> headers)
{
var headersList = headers.ToList();
while (await csvReader.ReadAsync())
{
var row = ProcessCsvRow(csvReader, csvFile);
var row = ProcessCsvRow(csvReader, headersList);
csvFile.AddRow(row);
}
}

private static CsvRow ProcessCsvRow(IReaderRow csvReader, CsvFile csvFile)
private static CsvRow ProcessCsvRow(IReaderRow csvReader, IEnumerable<string> headers)
{
var csvRow = new CsvRow();

foreach (var column in csvFile.Columns)
foreach (var header in headers)
{
if (!csvReader.TryGetField<string>(column.Index, out var fieldValue)) continue;
if (!csvReader.TryGetField<string>(header, out var fieldValue)) continue;

csvRow.AddField(column, fieldValue);
csvRow.AddOrUpdateField(header, fieldValue);
}

return csvRow;
Expand Down
7 changes: 4 additions & 3 deletions src/CsvProc9000/Csv/CsvRow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ public class CsvRow
{
private readonly List<CsvField> _fields = new();

public IReadOnlyCollection<CsvField> Fields => _fields;
public IEnumerable<CsvField> Fields => _fields;

public void AddField([NotNull] CsvColumn column, [NotNull] string fieldValue)
public void AddOrUpdateField([NotNull] string columnName, [NotNull] string fieldValue)
{
if (column == null) throw new ArgumentNullException(nameof(column));
if (columnName == null) throw new ArgumentNullException(nameof(columnName));
if (fieldValue == null) throw new ArgumentNullException(nameof(fieldValue));

var column = new CsvColumn(columnName);
_fields.Add(new CsvField(column, fieldValue));
}
}
Expand Down
13 changes: 13 additions & 0 deletions src/CsvProc9000/Options/Condition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using JetBrains.Annotations;

namespace CsvProc9000.Options
{
public class Condition
{
[UsedImplicitly]
public string Field { get; set; }

[UsedImplicitly]
public string Value { get; set; }
}
}
6 changes: 5 additions & 1 deletion src/CsvProc9000/Options/CsvProcessorOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using JetBrains.Annotations;
using System.Collections.Generic;
using JetBrains.Annotations;

namespace CsvProc9000.Options
{
Expand All @@ -15,5 +16,8 @@ public class CsvProcessorOptions

[UsedImplicitly]
public string OutboxDelimiter { get; set; }

[UsedImplicitly]
public List<Rule> Rules { get; set; }
}
}
14 changes: 14 additions & 0 deletions src/CsvProc9000/Options/Rule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Collections.Generic;
using JetBrains.Annotations;

namespace CsvProc9000.Options
{
public class Rule
{
[UsedImplicitly]
public List<Condition> Conditions { get; set; }

[UsedImplicitly]
public Dictionary<string, string> Steps { get; set; }
}
}
95 changes: 81 additions & 14 deletions src/CsvProc9000/Processors/CsvProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Threading.Tasks;
using CsvProc9000.Csv;
using CsvProc9000.Options;
Expand All @@ -24,29 +25,29 @@ public CsvProcessor(
[NotNull] ICsvReader csvReader)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_processorOptions = processorOptions?.Value ?? throw new ArgumentNullException(nameof(processorOptions));
_processorOptions = processorOptions.Value ?? throw new ArgumentNullException(nameof(processorOptions));
_fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
_csvReader = csvReader ?? throw new ArgumentNullException(nameof(csvReader));
}

public async Task ProcessAsync(IFileInfo file)
{
_logger.LogDebug("Processor: Waiting until file {File} can be processed", file.FullName);
await WaitUntilFileIsUnlockedAsync(file);

if (!CanProcess(file)) return;

await ProcessInternalAsync(file);
}

private bool CanProcess(IFileInfo file)
{
if (!file.Exists)
{
_logger.LogDebug("Cannot process file {File}, because it does not exist", file.FullName);
return false;
}

if (file.IsReadOnly)
{
_logger.LogDebug("Cannot process file {File}, because it is read-only", file.FullName);
Expand All @@ -56,7 +57,8 @@ private bool CanProcess(IFileInfo file)
// ReSharper disable once InvertIf
if (!file.Extension.Equals(".csv"))
{
_logger.LogDebug("Cannot process file {File}, because it has file-extensions '{Extension}' instead of '.csv'",
_logger.LogDebug(
"Cannot process file {File}, because it has file-extensions '{Extension}' instead of '.csv'",
file.FullName, file.Extension);
return false;
}
Expand All @@ -68,19 +70,84 @@ private async Task ProcessInternalAsync(IFileSystemInfo file)
{
_logger.LogInformation("Processor: Starting to process {File}...", file.FullName);

_logger.LogDebug("Reading in file {File}...", file.FullName);
_logger.LogDebug("Processor: Reading in file {File}...", file.FullName);
var csvFile = await _csvReader.ReadAsync(file.FullName, _processorOptions.InboxDelimiter);

_logger.LogDebug("Processor: Applying rules to file {File}...", file.FullName);
ApplyRulesToFile(csvFile);
}

// if (_fileSystem.Directory.Exists(_processorOptions.Outbox))
// _fileSystem.Directory.CreateDirectory(_processorOptions.Outbox);

private void ApplyRulesToFile(CsvFile csvFile)
{
if (_processorOptions.Rules == null || !_processorOptions.Rules.Any())
{
_logger.LogWarning("Processor: Cannot process file {File} because there are no rules defined",
csvFile.FileName);
return;
}

foreach (var rule in _processorOptions.Rules)
ApplyRuleToFile(csvFile, rule);
}

private void ApplyRuleToFile(CsvFile csvFile, Rule rule)
{
if (rule.Conditions == null || !rule.Conditions.Any())
{
_logger.LogWarning("Processor: Skipping Rule at index {Index} because it has no conditions",
_processorOptions.Rules.IndexOf(rule));
return;
}

foreach (var row in csvFile.Rows)
ApplyRuleToRow(row, rule, csvFile);
}

private void ApplyRuleToRow(CsvRow row, Rule rule, CsvFile file)
{
if (!MeetsRowConditions(row, rule, file)) return;

_logger.LogTrace("Processor: File {File} meets rule at index {RuleIndex}. Applying rule...",
file.FileName, _processorOptions.Rules.IndexOf(rule));

foreach (var (columnName, fieldValue) in rule.Steps)
{
_logger.LogTrace("Processor: Row at index {RowIndex}: Adding field '{Field}' with value '{FieldValue}'",
_processorOptions.Rules.IndexOf(rule), columnName, fieldValue);
row.AddOrUpdateField(columnName, fieldValue);
}
}

private void MoveFileToOutput(IFileInfo file)
private bool MeetsRowConditions(CsvRow row, Rule rule, CsvFile file)
{
var fileName = file.Name;
var destinationFileName = _fileSystem.Path.Combine(_processorOptions.Outbox, fileName);
foreach (var condition in rule.Conditions)
{
var field = row.Fields.FirstOrDefault(field => field.Column.Name == condition.Field);
if (field == null)
{
_logger.LogTrace(
"Processor: Row at index {RowIndex} does not meet condition at index {ConditionIndex}, " +
"because the field '{FieldName}' could not be found",
file.Rows.ToList().IndexOf(row), rule.Conditions.IndexOf(condition), condition.Field);
return false;
}

if (_fileSystem.Directory.Exists(_processorOptions.Outbox))
_fileSystem.Directory.CreateDirectory(_processorOptions.Outbox);
if (field.Value == condition.Value) continue;

_logger.LogTrace(
"Processor: Row at index {RowIndex} does not meet condition at index {ConditionIndex}, " +
"because the field '{FieldName}' with the value '{FieldValue}' " +
"does not have the desired value '{ConditionValue}'",
file.Rows.ToList().IndexOf(row), rule.Conditions.IndexOf(condition), condition.Field,
field.Value, condition.Value);
return false;
}

file.MoveTo(destinationFileName, true);
// if we get here, all conditions have been met
return true;
}

private async Task WaitUntilFileIsUnlockedAsync(IFileSystemInfo file)
Expand All @@ -98,7 +165,7 @@ private bool IsFileLocked(IFileSystemInfo file)
{
using var stream =
_fileSystem.File.Open(file.FullName, FileMode.Open, FileAccess.ReadWrite, FileShare.None);

// if this succeeds the file is not locked
return false;
}
Expand Down
6 changes: 3 additions & 3 deletions src/CsvProc9000/Workers/CsvWatcherWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ internal sealed class CsvWatcherWorker : BackgroundService
public CsvWatcherWorker(
[NotNull] ILogger<CsvWatcherWorker> logger,
[NotNull] IOptions<CsvProcessorOptions> processorOptions,
[NotNull] IFileSystem fileSystem,
[NotNull] IFileSystem fileSystem,
[NotNull] ICsvProcessor csvProcessor)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
_csvProcessor = csvProcessor ?? throw new ArgumentNullException(nameof(csvProcessor));
_processorOptions = processorOptions?.Value ?? throw new ArgumentNullException(nameof(processorOptions));
_processorOptions = processorOptions.Value ?? throw new ArgumentNullException(nameof(processorOptions));

_fileSystemWatcher = fileSystem.FileSystemWatcher.CreateNew(_processorOptions.Inbox);

Expand All @@ -39,7 +39,7 @@ public CsvWatcherWorker(

protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Starting to watch for files in {Target}...", _processorOptions.Inbox);
_logger.LogInformation("Watcher: Starting to watch for files in {Target}...", _processorOptions.Inbox);
_fileSystemWatcher.EnableRaisingEvents = true;

return Task.CompletedTask;
Expand Down
21 changes: 20 additions & 1 deletion src/CsvProc9000/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@
"InboxDelimiter": ";",

"Outbox": "X:\\Dev\\__test\\CSV_OUT",
"OutboxDelimiter": ";"
"OutboxDelimiter": ";",

"Rules": [
{
"Conditions": [
{
"Field": "Artikelnummer",
"Value": "16887"
},
{
"Field": "Versandart",
"Value": "Standardversand"
}
],
"Steps": {
"SHIPMENTTYPE": "NP",
"Verpackungsart": 0
}
}
]
}
}

0 comments on commit b5c528f

Please sign in to comment.