From d4efdc5e2bb82bcdf66ee8e12bceeaf1ddaf6349 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Tue, 14 Jul 2026 15:07:08 +0100 Subject: [PATCH] Refactor line terminator handling with enum type Replaced string-based line terminator properties with a new LineTerminatorType enum in file profile requests. Updated all usages, validation, and tests to use the strongly-typed enum. Added extension methods for conversion and enhanced validation to reject invalid or unknown values. Updated test cases and step definitions to support the new approach. --- .../Requests/CreateFileProfileRequest.cs | 2 +- .../Requests/LineTerminatorType.cs | 14 +++ .../Requests/UpdateFileProfileRequest.cs | 2 +- .../FileProfileAggregateTests.cs | 63 +++++++++++--- .../FileProfileAggregate.cs | 86 ++++++++++++++++--- .../Features/FileProfileSteps.cs | 27 ++++-- 6 files changed, 162 insertions(+), 32 deletions(-) create mode 100644 FileProcessor.DataTransferObjects/Requests/LineTerminatorType.cs diff --git a/FileProcessor.DataTransferObjects/Requests/CreateFileProfileRequest.cs b/FileProcessor.DataTransferObjects/Requests/CreateFileProfileRequest.cs index 2cfe059..53639c6 100644 --- a/FileProcessor.DataTransferObjects/Requests/CreateFileProfileRequest.cs +++ b/FileProcessor.DataTransferObjects/Requests/CreateFileProfileRequest.cs @@ -14,7 +14,7 @@ public class CreateFileProfileRequest public string OperatorName { get; set; } - public string LineTerminator { get; set; } + public LineTerminatorType? LineTerminator { get; set; } public string FileFormatHandler { get; set; } } diff --git a/FileProcessor.DataTransferObjects/Requests/LineTerminatorType.cs b/FileProcessor.DataTransferObjects/Requests/LineTerminatorType.cs new file mode 100644 index 0000000..5805298 --- /dev/null +++ b/FileProcessor.DataTransferObjects/Requests/LineTerminatorType.cs @@ -0,0 +1,14 @@ +using System; +using System.Text.Json.Serialization; + +namespace FileProcessor.DataTransferObjects.Requests; + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum LineTerminatorType +{ + LineFeed = 1, + + CarriageReturnLineFeed = 2, + + CarriageReturn = 3 +} diff --git a/FileProcessor.DataTransferObjects/Requests/UpdateFileProfileRequest.cs b/FileProcessor.DataTransferObjects/Requests/UpdateFileProfileRequest.cs index a7c2905..c09527c 100644 --- a/FileProcessor.DataTransferObjects/Requests/UpdateFileProfileRequest.cs +++ b/FileProcessor.DataTransferObjects/Requests/UpdateFileProfileRequest.cs @@ -10,7 +10,7 @@ public class UpdateFileProfileRequest public string OperatorName { get; set; } - public string LineTerminator { get; set; } + public LineTerminatorType? LineTerminator { get; set; } public string FileFormatHandler { get; set; } } diff --git a/FileProcessor.FileProfileAggregate.Tests/FileProfileAggregateTests.cs b/FileProcessor.FileProfileAggregate.Tests/FileProfileAggregateTests.cs index a87f3d7..49b8c14 100644 --- a/FileProcessor.FileProfileAggregate.Tests/FileProfileAggregateTests.cs +++ b/FileProcessor.FileProfileAggregate.Tests/FileProfileAggregateTests.cs @@ -24,7 +24,7 @@ public void FileProfileAggregate_CreateUpdateAndUniquenessFlow_WorksAsExpected() TestData.SafaricomListeningDirectory, TestData.SafaricomRequestType, TestData.SafaricomOperatorIdentifier, - TestData.SafaricomLineTerminator, + LineTerminatorType.LineFeed, TestData.SafaricomFileFormatHandler)); Result secondCreate = aggregate.CreateProfile(this.CreateProfile( @@ -33,7 +33,7 @@ public void FileProfileAggregate_CreateUpdateAndUniquenessFlow_WorksAsExpected() TestData.VoucherListeningDirectory, TestData.VoucherRequestType, TestData.VoucherOperatorIdentifier, - TestData.VoucherLineTerminator, + LineTerminatorType.LineFeed, TestData.VoucherFileFormatHandler)); Result updateName = aggregate.UpdateProfile(TestData.FileProfileId, new UpdateFileProfileRequest @@ -58,7 +58,7 @@ public void FileProfileAggregate_CreateUpdateAndUniquenessFlow_WorksAsExpected() Result updateLineTerminator = aggregate.UpdateProfile(TestData.FileProfileId, new UpdateFileProfileRequest { - LineTerminator = "\r\n" + LineTerminator = LineTerminatorType.CarriageReturnLineFeed }); Result updateFileFormatHandler = aggregate.UpdateProfile(TestData.FileProfileId, new UpdateFileProfileRequest @@ -72,7 +72,7 @@ public void FileProfileAggregate_CreateUpdateAndUniquenessFlow_WorksAsExpected() "/home/txnproc/bulkfiles/duplicate-name", "GammaRequest", "Gamma Operator", - "\n", + LineTerminatorType.LineFeed, "GammaHandler")); Result duplicateRequestTypeCreate = aggregate.CreateProfile(this.CreateProfile( @@ -81,7 +81,7 @@ public void FileProfileAggregate_CreateUpdateAndUniquenessFlow_WorksAsExpected() "/home/txnproc/bulkfiles/duplicate-request", TestData.VoucherRequestType, "Gamma Operator", - "\n", + LineTerminatorType.LineFeed, "GammaHandler")); List fileProfiles = aggregate.GetAllProfiles(); @@ -128,7 +128,7 @@ public void FileProfileAggregate_Update_WithNoEffectiveChanges_DoesNotChangeAppl TestData.SafaricomListeningDirectory, TestData.SafaricomRequestType, TestData.SafaricomOperatorIdentifier, - TestData.SafaricomLineTerminator, + LineTerminatorType.LineFeed, TestData.SafaricomFileFormatHandler)); Result updateResult = aggregate.UpdateProfile(TestData.FileProfileId, new UpdateFileProfileRequest @@ -137,7 +137,7 @@ public void FileProfileAggregate_Update_WithNoEffectiveChanges_DoesNotChangeAppl RequestType = TestData.SafaricomRequestType, ListeningDirectory = TestData.SafaricomListeningDirectory, OperatorName = TestData.SafaricomOperatorIdentifier, - LineTerminator = TestData.SafaricomLineTerminator, + LineTerminator = LineTerminatorType.LineFeed, FileFormatHandler = TestData.SafaricomFileFormatHandler }); @@ -154,7 +154,7 @@ public void FileProfileAggregate_Update_ListeningDirectoryUpdatesDerivedDirector TestData.SafaricomListeningDirectory, TestData.SafaricomRequestType, TestData.SafaricomOperatorIdentifier, - TestData.SafaricomLineTerminator, + LineTerminatorType.LineFeed, TestData.SafaricomFileFormatHandler)); Result updateResult = aggregate.UpdateProfile(TestData.FileProfileId, new UpdateFileProfileRequest @@ -182,7 +182,7 @@ public void FileProfileAggregate_Create_WithDuplicateName_IsRejected() TestData.SafaricomListeningDirectory, TestData.SafaricomRequestType, TestData.SafaricomOperatorIdentifier, - TestData.SafaricomLineTerminator, + LineTerminatorType.LineFeed, TestData.SafaricomFileFormatHandler)); Result duplicateCreate = aggregate.CreateProfile(this.CreateProfile( @@ -191,7 +191,7 @@ public void FileProfileAggregate_Create_WithDuplicateName_IsRejected() "/tmp/other", "DifferentRequest", "Other", - TestData.SafaricomLineTerminator, + LineTerminatorType.LineFeed, "OtherHandler")); firstCreate.IsSuccess.ShouldBeTrue(); @@ -199,6 +199,43 @@ public void FileProfileAggregate_Create_WithDuplicateName_IsRejected() duplicateCreate.Status.ShouldBe(ResultStatus.Invalid); } + [Theory] + [InlineData(null)] + public void FileProfileAggregate_Create_WithInvalidTerminator_IsRejected(LineTerminatorType? lineTerminator) + { + FileProfileAggregateRoot aggregate = FileProfileAggregateRoot.Create(); + + Result firstCreate = aggregate.CreateProfile(this.CreateProfile( + TestData.FileProfileId, + TestData.SafaricomProfileName, + TestData.SafaricomListeningDirectory, + TestData.SafaricomRequestType, + TestData.SafaricomOperatorIdentifier, + lineTerminator, + TestData.SafaricomFileFormatHandler)); + + firstCreate.IsFailed.ShouldBeTrue(); + firstCreate.Status.ShouldBe(ResultStatus.Invalid); + } + + [Fact] + public void FileProfileAggregate_Create_WithUnknownTerminator_IsRejected() + { + FileProfileAggregateRoot aggregate = FileProfileAggregateRoot.Create(); + + Result firstCreate = aggregate.CreateProfile(this.CreateProfile( + TestData.FileProfileId, + TestData.SafaricomProfileName, + TestData.SafaricomListeningDirectory, + TestData.SafaricomRequestType, + TestData.SafaricomOperatorIdentifier, + (LineTerminatorType?)99, + TestData.SafaricomFileFormatHandler)); + + firstCreate.IsFailed.ShouldBeTrue(); + firstCreate.Status.ShouldBe(ResultStatus.Invalid); + } + [Fact] public void FileProfileAggregate_Create_WithDuplicateRequestType_IsRejected() { @@ -210,7 +247,7 @@ public void FileProfileAggregate_Create_WithDuplicateRequestType_IsRejected() TestData.SafaricomListeningDirectory, TestData.SafaricomRequestType, TestData.SafaricomOperatorIdentifier, - TestData.SafaricomLineTerminator, + LineTerminatorType.LineFeed, TestData.SafaricomFileFormatHandler)); Result duplicateCreate = aggregate.CreateProfile(this.CreateProfile( @@ -219,7 +256,7 @@ public void FileProfileAggregate_Create_WithDuplicateRequestType_IsRejected() "/tmp/other", TestData.SafaricomRequestType, "Other", - TestData.SafaricomLineTerminator, + LineTerminatorType.LineFeed, "OtherHandler")); firstCreate.IsSuccess.ShouldBeTrue(); @@ -232,7 +269,7 @@ private CreateFileProfileRequest CreateProfile(Guid fileProfileId, string listeningDirectory, string requestType, string operatorName, - string lineTerminator, + LineTerminatorType? lineTerminator, string fileFormatHandler) { return new CreateFileProfileRequest diff --git a/FileProcessor.FileProfileAggregate/FileProfileAggregate.cs b/FileProcessor.FileProfileAggregate/FileProfileAggregate.cs index 4d62df1..4605978 100644 --- a/FileProcessor.FileProfileAggregate/FileProfileAggregate.cs +++ b/FileProcessor.FileProfileAggregate/FileProfileAggregate.cs @@ -8,6 +8,7 @@ using Shared.DomainDrivenDesign.EventSourcing; using Shared.EventStore.Aggregate; using Shared.General; +using Shared.Results; using SimpleResults; using FileProfileModel = FileProcessor.Models.FileProfile; @@ -93,13 +94,17 @@ public static Result CreateProfile(this FileProfileAggregate aggregate, CreateFi return Result.Invalid($"File profile [{request.FileProfileId}] already exists"); } + var lineTerminatorResult = request.LineTerminator.Value.ToLineTerminator(); + if (lineTerminatorResult.IsFailed) + return ResultHelpers.CreateFailure(lineTerminatorResult); + FileProfileCreatedEvent fileProfileCreatedEvent = new(aggregate.AggregateId, request.FileProfileId, request.Name.Trim(), request.ListeningDirectory.Trim(), request.RequestType.Trim(), request.OperatorName.Trim(), - request.LineTerminator, + lineTerminatorResult.Data, request.FileFormatHandler.Trim()); aggregate.ApplyAndAppend(fileProfileCreatedEvent); @@ -135,12 +140,19 @@ public static Result UpdateProfile(this FileProfileAggregate aggregate, Guid fil aggregate.ApplyAndAppend(new FileProfileOperatorNameUpdatedEvent(aggregate.AggregateId, fileProfileId, request.OperatorName)); } - if (IsDomainEventRequired(profile.FileFormatHandler, request.FileFormatHandler)) { - aggregate.ApplyAndAppend(new FileProfileFileFormatHandlerUpdatedEvent(aggregate.AggregateId, fileProfileId, request.FileFormatHandler)); + if (request.LineTerminator.HasValue) { + Result lineTerminatorResult = request.LineTerminator.Value.ToLineTerminator(); + if (lineTerminatorResult.IsFailed) { + return ResultHelpers.CreateFailure(lineTerminatorResult); + } + + if (IsDomainEventRequired(profile.LineTerminator, lineTerminatorResult.Data)) { + aggregate.ApplyAndAppend(new FileProfileLineTerminatorUpdatedEvent(aggregate.AggregateId, fileProfileId, lineTerminatorResult.Data)); + } } - if (IsDomainEventRequired(profile.LineTerminator, request.LineTerminator)) { - aggregate.ApplyAndAppend(new FileProfileLineTerminatorUpdatedEvent(aggregate.AggregateId, fileProfileId, request.LineTerminator)); + if (IsDomainEventRequired(profile.FileFormatHandler, request.FileFormatHandler)) { + aggregate.ApplyAndAppend(new FileProfileFileFormatHandlerUpdatedEvent(aggregate.AggregateId, fileProfileId, request.FileFormatHandler)); } return Result.Success(); @@ -270,18 +282,42 @@ private static Boolean IsSameProfile(this FileProfileAggregate aggregate, FilePr Comparer.Equals(profile.ListeningDirectory, request.ListeningDirectory.Trim()) && Comparer.Equals(profile.RequestType, request.RequestType.Trim()) && Comparer.Equals(profile.OperatorName, request.OperatorName.Trim()) && - Comparer.Equals(profile.LineTerminator, request.LineTerminator) && + Comparer.Equals(profile.LineTerminator, request.LineTerminator.Value.ToLineTerminator()) && Comparer.Equals(profile.FileFormatHandler, request.FileFormatHandler.Trim()) && profile.IsArchived == false; } private static Result ValidateRequiredCreateFields(CreateFileProfileRequest request) { - return ValidateStringField(request.ListeningDirectory, "No listening directory provided") ?? - ValidateStringField(request.RequestType, "No request type provided") ?? - ValidateStringField(request.OperatorName, "No operator name provided") ?? - ValidateStringField(request.LineTerminator, "No line terminator provided") ?? - ValidateStringField(request.FileFormatHandler, "No file format handler provided"); + var errors = new List(); + + AddIfInvalid(request.ListeningDirectory, "No listening directory provided", errors); + AddIfInvalid(request.RequestType, "No request type provided", errors); + AddIfInvalid(request.OperatorName, "No operator name provided", errors); + AddLineTerminatorIfInvalid(request.LineTerminator, "No line terminator provided", errors); + AddIfInvalid(request.FileFormatHandler, "No file format handler provided", errors); + + return errors.Count == 0 + ? Result.Success() + : Result.Invalid(errors); + } + + private static void AddIfInvalid(string? value, string errorMessage, ICollection errors) + { + if (string.IsNullOrWhiteSpace(value)) + { + errors.Add(errorMessage); + } + } + + private static void AddLineTerminatorIfInvalid(LineTerminatorType? value, + string errorMessage, + ICollection errors) + { + if (value.HasValue == false || Enum.IsDefined(typeof(LineTerminatorType), value.Value) == false) + { + errors.Add(errorMessage); + } } private static Result ValidateProposedUpdateValues(UpdateFileProfileRequest request) @@ -290,10 +326,22 @@ private static Result ValidateProposedUpdateValues(UpdateFileProfileRequest requ ValidateOptionalStringField(request.ListeningDirectory, "No listening directory provided") ?? ValidateOptionalStringField(request.RequestType, "No request type provided") ?? ValidateOptionalStringField(request.OperatorName, "No operator name provided") ?? - ValidateOptionalStringField(request.LineTerminator, "No line terminator provided") ?? + ValidateOptionalLineTerminatorField(request.LineTerminator, "No line terminator provided") ?? ValidateOptionalStringField(request.FileFormatHandler, "No file format handler provided"); } + private static Result ValidateOptionalLineTerminatorField(LineTerminatorType? value, string errorMessage) + { + if (value.HasValue == false) + { + return Result.Success(); + } + + return Enum.IsDefined(typeof(LineTerminatorType), value.Value) + ? Result.Success() + : Result.Invalid(errorMessage); + } + private static Result ValidateCreateUniqueness(this FileProfileAggregate aggregate, CreateFileProfileRequest request) { String proposedName = request.Name.Trim(); @@ -395,6 +443,20 @@ private static FileProfileModel ToModel(this FileProfileState fileProfile) } } +public static class LineTerminatorTypeExtensions +{ + public static Result ToLineTerminator(this LineTerminatorType lineTerminator) + { + return lineTerminator switch + { + LineTerminatorType.LineFeed => Result.Success("\n"), + LineTerminatorType.CarriageReturnLineFeed => Result.Success("\r\n"), + LineTerminatorType.CarriageReturn => Result.Success("\r"), + _ => Result.Invalid($"Invalid line terminator type {lineTerminator}") + }; + } +} + public record FileProfileAggregate : Aggregate { public static readonly Guid FileProfileCollectionId = Guid.Parse("2E8EAF39-3F51-4C10-9B8B-0D9DE3F80110"); diff --git a/FileProcessor.IntegrationTests/Features/FileProfileSteps.cs b/FileProcessor.IntegrationTests/Features/FileProfileSteps.cs index 61d984c..a8dc6b6 100644 --- a/FileProcessor.IntegrationTests/Features/FileProfileSteps.cs +++ b/FileProcessor.IntegrationTests/Features/FileProfileSteps.cs @@ -57,7 +57,7 @@ public async Task WhenITryToCreateTheFollowingDuplicateFileProfiles(DataTable ta string basedOn = row["BasedOn"]; FileProfile sourceProfile = await this.GetProfileByAlias(basedOn); - CreateFileProfileRequest request = duplicateType.Equals("Name", StringComparison.OrdinalIgnoreCase) + CreateFileProfileRequest request = duplicateType.Equals("Name", StringComparison.OrdinalIgnoreCase) ? new CreateFileProfileRequest { FileProfileId = Guid.NewGuid(), @@ -65,7 +65,7 @@ public async Task WhenITryToCreateTheFollowingDuplicateFileProfiles(DataTable ta ListeningDirectory = $"/tmp/{Guid.NewGuid():N}", RequestType = $"{sourceProfile.RequestType}-duplicate", OperatorName = sourceProfile.OperatorName, - LineTerminator = sourceProfile.LineTerminator, + LineTerminator = ParseLineTerminator(sourceProfile.LineTerminator), FileFormatHandler = sourceProfile.FileFormatHandler } : new CreateFileProfileRequest @@ -75,7 +75,7 @@ public async Task WhenITryToCreateTheFollowingDuplicateFileProfiles(DataTable ta ListeningDirectory = $"/tmp/{Guid.NewGuid():N}", RequestType = sourceProfile.RequestType, OperatorName = sourceProfile.OperatorName, - LineTerminator = sourceProfile.LineTerminator, + LineTerminator = ParseLineTerminator(sourceProfile.LineTerminator), FileFormatHandler = sourceProfile.FileFormatHandler }; @@ -125,7 +125,7 @@ private CreateFileProfileRequest BuildCreateRequest(DataTableRow row) ListeningDirectory = row["ListeningDirectory"], RequestType = row["RequestType"], OperatorName = row["OperatorName"], - LineTerminator = row["LineTerminator"], + LineTerminator = ParseLineTerminator(row["LineTerminator"]), FileFormatHandler = row["FileFormatHandler"] }; } @@ -148,11 +148,28 @@ private UpdateFileProfileRequest BuildUpdateRequest(DataTableRow row) ListeningDirectory = row["ListeningDirectory"], RequestType = row["RequestType"], OperatorName = row["OperatorName"], - LineTerminator = row["LineTerminator"], + LineTerminator = ParseLineTerminator(row["LineTerminator"]), FileFormatHandler = row["FileFormatHandler"] }; } + private static LineTerminatorType? ParseLineTerminator(string lineTerminator) + { + if (string.IsNullOrWhiteSpace(lineTerminator)) + { + return null; + } + + return lineTerminator switch + { + "\n" => LineTerminatorType.LineFeed, + "\r\n" => LineTerminatorType.CarriageReturnLineFeed, + "\r" => LineTerminatorType.CarriageReturn, + _ when Enum.TryParse(lineTerminator, true, out LineTerminatorType parsed) => parsed, + _ => throw new ArgumentOutOfRangeException(nameof(lineTerminator), lineTerminator, "Unsupported line terminator value") + }; + } + private async Task UpdateProfile(string alias, UpdateFileProfileRequest request) { Guid fileProfileId = this.FileProfileIds[alias];