diff --git a/Px.Utils.TestingApp/Commands/BenchmarkRunner.cs b/Px.Utils.TestingApp/Commands/BenchmarkRunner.cs index 0982d770..944394f6 100644 --- a/Px.Utils.TestingApp/Commands/BenchmarkRunner.cs +++ b/Px.Utils.TestingApp/Commands/BenchmarkRunner.cs @@ -14,6 +14,7 @@ internal BenchmarkRunner() _benchmarks.Add("data-validation", new DataValidationBenchmark()); _benchmarks.Add("file-validation", new PxFileValidationBenchmark()); _benchmarks.Add("computation", new ComputationBenchmark()); + _benchmarks.Add("database-validation", new DatabaseValidationBenchmark()); } internal override string Help diff --git a/Px.Utils.TestingApp/Commands/DataValidationBenchmark.cs b/Px.Utils.TestingApp/Commands/DataValidationBenchmark.cs index 47dd735b..486f5ce9 100644 --- a/Px.Utils.TestingApp/Commands/DataValidationBenchmark.cs +++ b/Px.Utils.TestingApp/Commands/DataValidationBenchmark.cs @@ -53,17 +53,17 @@ private void ValidateDataBenchmarks() { using Stream stream = new FileStream(TestFilePath, FileMode.Open, FileAccess.Read); stream.Position = start + dataKeyword.Length + readStartOffset; // skip the '=' and linechange - DataValidator validator = new(stream, expectedCols, expectedRows, TestFilePath, 0, encoding); - validator.Validate(); + DataValidator validator = new(expectedCols, expectedRows, 0); + validator.Validate(stream, TestFilePath, encoding); } private async Task ValidateDataBenchmarksAsync() { using Stream stream = new FileStream(TestFilePath, FileMode.Open, FileAccess.Read); stream.Position = start + dataKeyword.Length + readStartOffset; // skip the '=' and linechange - DataValidator validator = new(stream, expectedCols, expectedRows, TestFilePath, 0, encoding); + DataValidator validator = new(expectedCols, expectedRows, 0); - await validator.ValidateAsync(); + await validator.ValidateAsync(stream, TestFilePath, encoding); } protected override void SetRunParameters() diff --git a/Px.Utils.TestingApp/Commands/DatabaseValidationBenchmark.cs b/Px.Utils.TestingApp/Commands/DatabaseValidationBenchmark.cs new file mode 100644 index 00000000..aff5135b --- /dev/null +++ b/Px.Utils.TestingApp/Commands/DatabaseValidationBenchmark.cs @@ -0,0 +1,69 @@ +using Px.Utils.Validation.DatabaseValidation; + +namespace Px.Utils.TestingApp.Commands +{ + internal sealed class DatabaseValidationBenchmark : Benchmark + { + internal override string Help => "Validates a px path database."; + + internal override string Description => "Validates a px path database."; + private static readonly string[] directoryFlags = ["-d", "-directory"]; + + private DatabaseValidator validator; + + internal DatabaseValidationBenchmark() + { + ParameterFlags.Add(directoryFlags); + BenchmarkFunctions = [ValidationBenchmark]; + BenchmarkFunctionsAsync = [ValidationBenchmarkAsync]; + } + + protected override void SetRunParameters() + { + foreach (string key in directoryFlags) + { + if (Parameters.TryGetValue(key, out List? value) && value.Count == 1) + { + TestFilePath = value[0]; + base.SetRunParameters(); + return; + } + } + + throw new ArgumentException("Directory not found."); + } + + protected override void StartInteractiveMode() + { + base.StartInteractiveMode(); + + Console.WriteLine("Enter the path to the PX database root to benchmark"); + string path = Console.ReadLine() ?? ""; + + while (!Directory.Exists(path)) + { + Console.WriteLine("Path provided is not valid, please enter a path to a valid directory."); + path = Console.ReadLine() ?? ""; + } + + TestFilePath = path; + } + + protected override void OneTimeBenchmarkSetup() + { + base.OneTimeBenchmarkSetup(); + + validator = new(TestFilePath, new LocalFileSystem()); + } + + private void ValidationBenchmark() + { + validator.Validate(); + } + + private async Task ValidationBenchmarkAsync() + { + await validator.ValidateAsync(); + } + } +} diff --git a/Px.Utils.TestingApp/Commands/FileBenchmark.cs b/Px.Utils.TestingApp/Commands/FileBenchmark.cs index 6d4cbbd5..5c1ac7e9 100644 --- a/Px.Utils.TestingApp/Commands/FileBenchmark.cs +++ b/Px.Utils.TestingApp/Commands/FileBenchmark.cs @@ -21,7 +21,7 @@ protected override void SetRunParameters() } } - throw new ArgumentException("File path parameter not found for a file based benchmark."); + StartInteractiveMode(); } protected override void StartInteractiveMode() diff --git a/Px.Utils.TestingApp/Commands/MetadataContentValidationBenchmark.cs b/Px.Utils.TestingApp/Commands/MetadataContentValidationBenchmark.cs index 6ae1025a..eab3f699 100644 --- a/Px.Utils.TestingApp/Commands/MetadataContentValidationBenchmark.cs +++ b/Px.Utils.TestingApp/Commands/MetadataContentValidationBenchmark.cs @@ -26,8 +26,8 @@ protected override void OneTimeBenchmarkSetup() using Stream stream = new FileStream(TestFilePath, FileMode.Open, FileAccess.Read); stream.Seek(0, SeekOrigin.Begin); - SyntaxValidator syntaxValidator = new(stream, Encoding.Default, TestFilePath); - SyntaxValidationResult validatorResult = syntaxValidator.Validate(); + SyntaxValidator syntaxValidator = new(); + SyntaxValidationResult validatorResult = syntaxValidator.Validate(stream, TestFilePath, Encoding.Default); _entries = validatorResult.Result; } diff --git a/Px.Utils.TestingApp/Commands/MetadataSyntaxValidationBenchmark.cs b/Px.Utils.TestingApp/Commands/MetadataSyntaxValidationBenchmark.cs index de28eb6f..e283d4d2 100644 --- a/Px.Utils.TestingApp/Commands/MetadataSyntaxValidationBenchmark.cs +++ b/Px.Utils.TestingApp/Commands/MetadataSyntaxValidationBenchmark.cs @@ -4,7 +4,7 @@ namespace Px.Utils.TestingApp.Commands { - internal sealed class MetadataSyntaxValidationBenchmark : Benchmark + internal sealed class MetadataSyntaxValidationBenchmark : FileBenchmark { internal override string Help => "Validates the syntax of the Px file metadata given amount of times." + Environment.NewLine + @@ -13,8 +13,7 @@ internal sealed class MetadataSyntaxValidationBenchmark : Benchmark internal override string Description => "Benchmarks the metadata syntax validation of Px.Utils/Validation/SyntaxValidator."; - private Encoding encoding; - private SyntaxValidator validator; + private Encoding encoding = Encoding.Default; internal MetadataSyntaxValidationBenchmark() { @@ -29,24 +28,22 @@ protected override void OneTimeBenchmarkSetup() using Stream stream = new FileStream(TestFilePath, FileMode.Open, FileAccess.Read); PxFileMetadataReader reader = new(); encoding = reader.GetEncoding(stream); - stream.Seek(0, SeekOrigin.Begin); - validator = new(stream, encoding, TestFilePath); } private void SyntaxValidationBenchmark() { using Stream stream = new FileStream(TestFilePath, FileMode.Open, FileAccess.Read); - stream.Seek(0, SeekOrigin.Begin); - - validator.Validate(); + SyntaxValidator validator = new(); + validator.Validate(stream, TestFilePath, encoding); + stream.Close(); } private async Task SyntaxValidationBenchmarkAsync() { using Stream stream = new FileStream(TestFilePath, FileMode.Open, FileAccess.Read); - stream.Seek(0, SeekOrigin.Begin); - - await validator.ValidateAsync(); + SyntaxValidator validator = new(); + await validator.ValidateAsync(stream, TestFilePath, encoding); + stream.Close(); } } } diff --git a/Px.Utils.TestingApp/Commands/PxFileValidationBenchmark.cs b/Px.Utils.TestingApp/Commands/PxFileValidationBenchmark.cs index 8e8b945b..8fca0196 100644 --- a/Px.Utils.TestingApp/Commands/PxFileValidationBenchmark.cs +++ b/Px.Utils.TestingApp/Commands/PxFileValidationBenchmark.cs @@ -1,10 +1,11 @@ -using Px.Utils.PxFile.Metadata; +using Px.Utils.Exceptions; +using Px.Utils.PxFile.Metadata; using Px.Utils.Validation; using System.Text; namespace Px.Utils.TestingApp.Commands { - internal class PxFileValidationBenchmark : Benchmark + internal sealed class PxFileValidationBenchmark : FileBenchmark { internal override string Help => "Runs through the whole px file validation process (metadata syntax- and contents-, data-) for the given file."; @@ -26,21 +27,29 @@ protected override void OneTimeBenchmarkSetup() using Stream stream = new FileStream(TestFilePath, FileMode.Open, FileAccess.Read); PxFileMetadataReader reader = new(); - encoding = reader.GetEncoding(stream); + try + { + encoding = reader.GetEncoding(stream); + } + catch (InvalidPxFileMetadataException) + { + encoding = Encoding.Default; + throw; + } } private void ValidatePxFileBenchmarks() { using Stream stream = new FileStream(TestFilePath, FileMode.Open, FileAccess.Read); - PxFileValidator validator = new(stream, TestFilePath, encoding); - validator.Validate(); + PxFileValidator validator = new(); + validator.Validate(stream, TestFilePath, encoding); } private async Task ValidatePxFileBenchmarksAsync() { using Stream stream = new FileStream(TestFilePath, FileMode.Open, FileAccess.Read); - PxFileValidator validator = new(stream, TestFilePath, encoding); - await validator.ValidateAsync(); + PxFileValidator validator = new(); + await validator.ValidateAsync(stream, TestFilePath, encoding); } } } diff --git a/Px.Utils.UnitTests/PxFileTests/DataTests/PxFileStreamDataReaderTests/AsyncDataReaderTests.cs b/Px.Utils.UnitTests/PxFileTests/DataTests/PxFileStreamDataReaderTests/AsyncDataReaderTests.cs index 24dc2c6e..c9efbd6b 100644 --- a/Px.Utils.UnitTests/PxFileTests/DataTests/PxFileStreamDataReaderTests/AsyncDataReaderTests.cs +++ b/Px.Utils.UnitTests/PxFileTests/DataTests/PxFileStreamDataReaderTests/AsyncDataReaderTests.cs @@ -165,7 +165,7 @@ public async Task CancelReadDoubleDataValuesAsyncValidIntegersIsCancelled() DataIndexer indexer = new(testMeta, matrixMap); using CancellationTokenSource cts = new(); - cts.Cancel(); + await cts.CancelAsync(); CancellationToken cToken = cts.Token; async Task call() => await reader.ReadDoubleDataValuesAsync(targetBuffer, 0, indexer, cToken); @@ -224,7 +224,7 @@ public async Task CancelReadAddDecimalDataValuesAsyncValidIntegersIsCancelled() DataIndexer indexer = new(testMeta, matrixMap); using CancellationTokenSource cts = new(); - cts.Cancel(); + await cts.CancelAsync(); CancellationToken cToken = cts.Token; async Task call() => await reader.ReadDecimalDataValuesAsync(targetBuffer, 0, indexer, cToken); @@ -283,7 +283,7 @@ public async Task CancelReadUnsafeDoubleValuesAsyncValidIntegersIsCancelled() DataIndexer indexer = new(testMeta, matrixMap); using CancellationTokenSource cts = new(); - cts.Cancel(); + await cts.CancelAsync(); CancellationToken cToken = cts.Token; // Act and Assert diff --git a/Px.Utils.UnitTests/Validation/ContentValidationTests/ContentValidationTests.cs b/Px.Utils.UnitTests/Validation/ContentValidationTests/ContentValidationTests.cs index e8ba1c3a..8711d524 100644 --- a/Px.Utils.UnitTests/Validation/ContentValidationTests/ContentValidationTests.cs +++ b/Px.Utils.UnitTests/Validation/ContentValidationTests/ContentValidationTests.cs @@ -1,5 +1,4 @@ -using Px.Utils.PxFile; -using Px.Utils.UnitTests.Validation.Fixtures; +using Px.Utils.UnitTests.Validation.Fixtures; using Px.Utils.Validation; using Px.Utils.Validation.ContentValidation; using Px.Utils.Validation.SyntaxValidation; @@ -41,10 +40,10 @@ public void ValidatePxFileContentCalledWithMinimalStructuredEntryReturnsValidRes ContentValidator validator = new(filename, encoding, entries); // Act - ValidationFeedbackItem[] feedback = validator.Validate().FeedbackItems; + ContentValidationResult feedback = validator.Validate(); // Assert - Assert.AreEqual(0, feedback.Length); + Assert.AreEqual(0, feedback.FeedbackItems.Count); } [TestMethod] @@ -55,15 +54,15 @@ public void ValidateFindDefaultLanguageWithEmptyStructuredEntryArrayReturnsWithE ContentValidator validator = new(filename, encoding, entries); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateFindDefaultLanguage( + ValidationFeedback? result = ContentValidator.ValidateFindDefaultLanguage( entries, validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(ValidationFeedbackRule.MissingDefaultLanguage, result[0].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.MissingDefaultLanguage, result.First().Key.Rule); } [TestMethod] @@ -74,15 +73,15 @@ public void ValidateFindAvailableLanguagesWithEmptyStructuredArrayReturnsWithWar ContentValidator validator = new(filename, encoding, entries); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateFindAvailableLanguages( + ValidationFeedback? result = ContentValidator.ValidateFindAvailableLanguages( entries, validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(ValidationFeedbackRule.RecommendedKeyMissing, result[0].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.RecommendedKeyMissing, result.First().Key.Rule); } [TestMethod] @@ -95,15 +94,15 @@ public void ValidateDefaultLanguageDefinedInAvailableLanguagesCalledWithUndefine SetValidatorField(validator, "_availableLanguages", availableLanguages); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateDefaultLanguageDefinedInAvailableLanguages( + ValidationFeedback? result = ContentValidator.ValidateDefaultLanguageDefinedInAvailableLanguages( entries, validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(ValidationFeedbackRule.UndefinedLanguageFound, result[0].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.UndefinedLanguageFound, result.First().Key.Rule); } [TestMethod] @@ -116,15 +115,15 @@ public void ValidateFindContentDimensionCalledWithMissingContVariableReturnsWith SetValidatorField(validator, "_availableLanguages", availableLanguages); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateFindContentDimension( + ValidationFeedback? result = ContentValidator.ValidateFindContentDimension( entries, validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(ValidationFeedbackRule.RecommendedKeyMissing, result[0].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.RecommendedKeyMissing, result.First().Key.Rule); } [TestMethod] @@ -135,17 +134,36 @@ public void ValidateFindRequiredCommonKeysCalledWithEmptyStructuredEntryArrayYRe ContentValidator validator = new(filename, encoding, entries); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateFindRequiredCommonKeys( + ValidationFeedback? result = ContentValidator.ValidateFindRequiredCommonKeys( entries, validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(3, result.Length); - Assert.AreEqual(ValidationFeedbackRule.RequiredKeyMissing, result[0].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.RequiredKeyMissing, result[1].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.RequiredKeyMissing, result[2].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(3, result.First().Value.Count); + Assert.AreEqual(ValidationFeedbackRule.RequiredKeyMissing, result.First().Key.Rule); + } + + [TestMethod] + public void ValidateFindStubOrHeadingCalledWithListsOfNamesReturnWithNoErrors() + { + // Arrange + ValidationStructuredEntry[] entries = ContentValidationFixtures.STRUCTURED_ENTRY_ARRAY_WITH_MULTIPLE_DIMENSION_NAMES; + ContentValidator validator = new(filename, encoding, entries); + SetValidatorField(validator, "_defaultLanguage", defaultLanguage); + SetValidatorField(validator, "_availableLanguages", availableLanguages); + + // Act + ValidationFeedback? result = ContentValidator.ValidateFindStubAndHeading( + entries, + validator + ); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(0, result.Count); } [TestMethod] @@ -158,15 +176,36 @@ public void ValidateFindStubOrHeadingCalledWithWithMissingHeadingReturnsWithErro SetValidatorField(validator, "_availableLanguages", availableLanguages); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateFindStubAndHeading( + ValidationFeedback? result = ContentValidator.ValidateFindStubAndHeading( entries, validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(ValidationFeedbackRule.MissingStubAndHeading, result[0].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.MissingStubAndHeading, result.First().Key.Rule); + } + + [TestMethod] + public void ValidateFindStubOrHeadingCalledWithDuplicateDimensionsReturnsWithWarning() + { + // Arrange + ValidationStructuredEntry[] entries = ContentValidationFixtures.STRUCTURED_ENTRY_ARRAY_WITH_DUPLICATE_DIMENSION; + ContentValidator validator = new(filename, encoding, entries); + SetValidatorField(validator, "_defaultLanguage", defaultLanguage); + SetValidatorField(validator, "_availableLanguages", availableLanguages); + + // Act + ValidationFeedback? result = ContentValidator.ValidateFindStubAndHeading( + entries, + validator + ); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.DuplicateDimension, result.First().Key.Rule); } [TestMethod] @@ -180,16 +219,16 @@ public void ValidateFindRecommendedKeysCalledWithMissingDescriptionsReturnsWithW SetValidatorField(validator, "_availableLanguages", availableLanguages); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateFindRecommendedKeys( + ValidationFeedback? result = ContentValidator.ValidateFindRecommendedKeys( entries, validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(2, result.Length); - Assert.AreEqual(ValidationFeedbackRule.RecommendedKeyMissing, result[0].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.RecommendedKeyMissing, result[1].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(2, result.First().Value.Count); + Assert.AreEqual(ValidationFeedbackRule.RecommendedKeyMissing, result.First().Key.Rule); } [TestMethod] @@ -207,21 +246,50 @@ public void ValidateFindDimenionsValuesCalledWithMissingDimensionValuesReturnsEr }); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateFindDimensionValues( + ValidationFeedback? result = ContentValidator.ValidateFindDimensionValues( + entries, + validator + ); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(3, result.First().Value.Count); + Assert.AreEqual(ValidationFeedbackRule.VariableValuesMissing, result.First().Key.Rule); + } + + [TestMethod] + public void ValidateFindDimensionValuesCalledWithDuplicateEntriesReturnsErrors() + { + // Arrange + ValidationStructuredEntry[] entries = ContentValidationFixtures.STRUCTURED_ENTRY_ARRAY_WITH_DUPLICATE_DIMENSION_VALUES; + ContentValidator validator = new(filename, encoding, entries); + SetValidatorField(validator, "_defaultLanguage", defaultLanguage); + SetValidatorField(validator, "_availableLanguages", availableLanguages); + SetValidatorField(validator, "_stubDimensionNames", new Dictionary + { + { "fi", ["bar"] }, + { "en", ["bar-en"] } + }); + SetValidatorField(validator, "_headingDimensionNames", new Dictionary + { + { "fi", ["bar"] }, + }); + + // Act + ValidationFeedback? result = ContentValidator.ValidateFindDimensionValues( entries, validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(3, result.Length); - Assert.AreEqual(ValidationFeedbackRule.VariableValuesMissing, result[0].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.VariableValuesMissing, result[1].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.VariableValuesMissing, result[2].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.DuplicateEntry, result.First().Key.Rule); } [TestMethod] - public void ValidateFindContentDimensionKeysCalledWithInvalidContentValueKeyEntriesSReturnsWithErrors() + public void ValidateFindContentDimensionKeysCalledWithMissingContentValueKeyEntriesReturnsWithErrors() { // Arrange ValidationStructuredEntry[] entries = ContentValidationFixtures.STRUCTURED_ENTRY_ARRAY_WITH_INVALID_CONTENT_VALUE_KEY_ENTRIES; @@ -236,16 +304,48 @@ public void ValidateFindContentDimensionKeysCalledWithInvalidContentValueKeyEntr }); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateFindContentDimensionKeys( + ValidationFeedback? result = ContentValidator.ValidateFindContentDimensionKeys( + entries, + validator + ); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(2, result.First().Value.Count); + Assert.AreEqual(ValidationFeedbackRule.RequiredKeyMissing, result.First().Key.Rule); + } + + [TestMethod] + public void ValidateFindContentDimensionKeysCalledWithMissingRecommendedSpecifiersEntriesReturnsWithWarnings() + { + // Arrange + ValidationStructuredEntry[] entries = ContentValidationFixtures.STRUCTURED_ENTRY_ARRAY_WITH_MISSING_RECIMMENDED_SPECIFIERS; + ContentValidator validator = new(filename, encoding, entries); + SetValidatorField(validator, "_defaultLanguage", defaultLanguage); + SetValidatorField(validator, "_availableLanguages", availableLanguages); + SetValidatorField(validator, "_contentDimensionNames", contentDimensionNames); + SetValidatorField(validator, "_dimensionValueNames", new Dictionary, string[]> + { + { new KeyValuePair( "fi", "bar" ), ["foo"] }, + { new KeyValuePair( "en", "bar-en" ), ["foo-en"] }, + }); + ValidationFeedbackKey recommendedKeyFeedbackKey = new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.RecommendedKeyMissing); + ValidationFeedbackKey recommendedSpecifierFeedbackKey = new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.RecommendedSpecifierDefinitionMissing); + + + // Act + ValidationFeedback? result = ContentValidator.ValidateFindContentDimensionKeys( entries, validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(2, result.Length); - Assert.AreEqual(ValidationFeedbackRule.RequiredKeyMissing, result[0].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.RequiredKeyMissing, result[1].Feedback.Rule); + Assert.AreEqual(2, result.Count); + Assert.IsTrue(result.ContainsKey(recommendedKeyFeedbackKey)); + Assert.IsTrue(result.ContainsKey(recommendedSpecifierFeedbackKey)); + Assert.AreEqual(2, result[recommendedSpecifierFeedbackKey].Count); } [TestMethod] @@ -260,18 +360,16 @@ public void ValidateFindDimensionRecommendedKeysCalledWithIncompleteVariableReco SetValidatorField(validator, "_stubDimensionNames", stubDimensionNames); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateFindDimensionRecommendedKeys( + ValidationFeedback? result = ContentValidator.ValidateFindDimensionRecommendedKeys( entries, validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(4, result.Length); - Assert.AreEqual(ValidationFeedbackRule.RecommendedKeyMissing, result[0].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.RecommendedKeyMissing, result[1].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.RecommendedKeyMissing, result[2].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.RecommendedKeyMissing, result[3].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(4, result.First().Value.Count); + Assert.AreEqual(ValidationFeedbackRule.RecommendedKeyMissing, result.First().Key.Rule); } [TestMethod] @@ -282,15 +380,15 @@ public void ValidateUnexpectedSpecifiersCalledWithStructuredEntryWithIllegalSpec ContentValidator validator = new(filename, encoding, entries); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateUnexpectedSpecifiers( + ValidationFeedback? result = ContentValidator.ValidateUnexpectedSpecifiers( entries[0], validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(ValidationFeedbackRule.IllegalSpecifierDefinitionFound, result[0].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.IllegalSpecifierDefinitionFound, result.First().Key.Rule); } [TestMethod] @@ -301,15 +399,15 @@ public void ValidateUnexpectedSpecifiersCalledWithStructuredEntryWithIllegalLang ContentValidator validator = new(filename, encoding, entries); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateUnexpectedLanguageParams( + ValidationFeedback? result = ContentValidator.ValidateUnexpectedLanguageParams( entries[0], validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(ValidationFeedbackRule.IllegalLanguageDefinitionFound, result[0].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.IllegalLanguageDefinitionFound, result.First().Key.Rule); } [TestMethod] @@ -322,15 +420,15 @@ public void ValidateLanguageParamsCalledWithUndefinedLanguageReturnshWithErrors( SetValidatorField(validator, "_availableLanguages", availableLanguages); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateLanguageParams( + ValidationFeedback? result = ContentValidator.ValidateLanguageParams( entries[0], validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(ValidationFeedbackRule.UndefinedLanguageFound, result[0].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.UndefinedLanguageFound, result.First().Key.Rule); } [TestMethod] @@ -345,15 +443,15 @@ public void ValidateSpecifiersCalledWithUndefinedFirstSpecifierReturnsWithErrors SetValidatorField(validator, "_dimensionValueNames", dimensionValueNames); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateSpecifiers( + ValidationFeedback? result = ContentValidator.ValidateSpecifiers( entries[0], validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(ValidationFeedbackRule.IllegalSpecifierDefinitionFound, result[0].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.IllegalSpecifierDefinitionFound, result.First().Key.Rule); } [TestMethod] @@ -368,15 +466,15 @@ public void ValidateSpecifiersCalledWithUndefinedSecondSpecifierReturnsWithError SetValidatorField(validator, "_dimensionValueNames", dimensionValueNames); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateSpecifiers( + ValidationFeedback? result = ContentValidator.ValidateSpecifiers( entries[0], validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(ValidationFeedbackRule.IllegalSpecifierDefinitionFound, result[0].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.IllegalSpecifierDefinitionFound, result.First().Key.Rule); } [TestMethod] @@ -389,15 +487,15 @@ public void ValidateValueTypesCalledWithStructuredEntryArrayWithInvalidValueType // Act foreach (ValidationStructuredEntry entry in entries) { - ValidationFeedbackItem[]? result = ContentValidator.ValidateValueTypes( + ValidationFeedback? result = ContentValidator.ValidateValueTypes( entry, validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(ValidationFeedbackRule.UnmatchingValueType, result[0].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.UnmatchingValueType, result.First().Key.Rule); } } @@ -411,15 +509,15 @@ public void ValidateValueTypesCalledWithStructuredEntryArrayWithWrongValuesRetur // Act foreach (ValidationStructuredEntry entry in entries) { - ValidationFeedbackItem[]? result = ContentValidator.ValidateValueContents( + ValidationFeedback? result = ContentValidator.ValidateValueContents( entry, validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(ValidationFeedbackRule.InvalidValueFound, result[0].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.InvalidValueFound, result.First().Key.Rule); } } @@ -435,15 +533,15 @@ public void ValidateValueAmountsCalledWithUnmatchingAmountOfElementsReturnsWithE SetValidatorField(validator, "_dimensionValueNames", dimensionValueNames); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateValueAmounts( + ValidationFeedback? result = ContentValidator.ValidateValueAmounts( entries[0], validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(ValidationFeedbackRule.UnmatchingValueAmount, result[0].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.UnmatchingValueAmount, result.First().Key.Rule); } [TestMethod] @@ -454,15 +552,15 @@ public void ValidateValueUppercaseRecommendationsCalledWithLowerCaseEntryReturns ContentValidator validator = new(filename, encoding, entries); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateValueUppercaseRecommendations( + ValidationFeedback? result = ContentValidator.ValidateValueUppercaseRecommendations( entries[0], validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(ValidationFeedbackRule.ValueIsNotInUpperCase, result[0].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.ValueIsNotInUpperCase, result.First().Key.Rule); } [TestMethod] @@ -473,10 +571,10 @@ public void ValidateContentWithCustomFunctionsReturnsValidResult() ContentValidator validator = new(filename, encoding, entries, new MockCustomContentValidationFunctions()); // Act - ValidationFeedbackItem[] feedback = validator.Validate().FeedbackItems; + ValidationFeedback feedback = validator.Validate().FeedbackItems; // Assert - Assert.AreEqual(0, feedback.Length); + Assert.AreEqual(0, feedback.Count); } [TestMethod] @@ -487,15 +585,15 @@ public void ValidateFindDefaultWithMultipleDefaultLanguagesReturnsError() ContentValidator validator = new(filename, encoding, entries); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateFindDefaultLanguage( + ValidationFeedback? result = ContentValidator.ValidateFindDefaultLanguage( entries, validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(ValidationFeedbackRule.MultipleInstancesOfUniqueKey, result[0].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.MultipleInstancesOfUniqueKey, result.First().Key.Rule); } [TestMethod] @@ -506,15 +604,15 @@ public void ValidateFindAvailableLanguagesWithMultipleAvailableLanguagesEntriesR ContentValidator validator = new(filename, encoding, entries); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateFindAvailableLanguages( + ValidationFeedback? result = ContentValidator.ValidateFindAvailableLanguages( entries, validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(ValidationFeedbackRule.MultipleInstancesOfUniqueKey, result[0].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.MultipleInstancesOfUniqueKey, result.First().Key.Rule); } [TestMethod] @@ -525,15 +623,15 @@ public void ValidateFindStubAndHeadingWithMissingStubAndHeadingReturnsError() ContentValidator validator = new(filename, encoding, entries); // Act - ValidationFeedbackItem[]? result = ContentValidator.ValidateFindStubAndHeading( + ValidationFeedback? result = ContentValidator.ValidateFindStubAndHeading( entries, validator ); // Assert Assert.IsNotNull(result); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(ValidationFeedbackRule.MissingStubAndHeading, result[0].Feedback.Rule); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ValidationFeedbackRule.MissingStubAndHeading, result.First().Key.Rule); } private static void SetValidatorField(ContentValidator validator, string fieldName, object value) diff --git a/Px.Utils.UnitTests/Validation/ContentValidationTests/MockCustomContentValidationFunctions.cs b/Px.Utils.UnitTests/Validation/ContentValidationTests/MockCustomContentValidationFunctions.cs index 7ef79e2d..8a18eea9 100644 --- a/Px.Utils.UnitTests/Validation/ContentValidationTests/MockCustomContentValidationFunctions.cs +++ b/Px.Utils.UnitTests/Validation/ContentValidationTests/MockCustomContentValidationFunctions.cs @@ -6,12 +6,12 @@ namespace Px.Utils.UnitTests.Validation.ContentValidationTests { internal sealed class MockCustomContentValidationFunctions : CustomContentValidationFunctions { - internal static ValidationFeedbackItem[]? MockFindKeywordFunction(ValidationStructuredEntry[] entries, ContentValidator validator) + internal static ValidationFeedback? MockFindKeywordFunction(ValidationStructuredEntry[] entries, ContentValidator validator) { return null; } - internal static ValidationFeedbackItem[]? MockEntryFunction(ValidationStructuredEntry entry, ContentValidator validator) + internal static ValidationFeedback? MockEntryFunction(ValidationStructuredEntry entry, ContentValidator validator) { return null; } diff --git a/Px.Utils.UnitTests/Validation/DataValidationTests/DataNumberValueValidatorTest.cs b/Px.Utils.UnitTests/Validation/DataValidationTests/DataNumberValueValidatorTest.cs index 6810cae1..cebf10b9 100644 --- a/Px.Utils.UnitTests/Validation/DataValidationTests/DataNumberValueValidatorTest.cs +++ b/Px.Utils.UnitTests/Validation/DataValidationTests/DataNumberValueValidatorTest.cs @@ -19,12 +19,13 @@ public class DataNumberValueValidatorTest [DataRow("79228162514264337593543950335")] [DataRow("100")] [DataRow("-100")] + [DataRow("-")] public void AllowedNumberValues(string allowedValue) { DataNumberValidator validator = new(); Encoding encoding = Encoding.UTF8; List value = [.. encoding.GetBytes(allowedValue)]; - ValidationFeedback? nullableFeedback = validator.Validate(value, EntryType.DataItem, encoding, 0, 0); + KeyValuePair? nullableFeedback = validator.Validate(value, EntryType.DataItem, encoding, 0, 0, "foo"); Assert.IsNull(nullableFeedback); } @@ -47,13 +48,13 @@ public void NotAllowedNumberValue(string notAllowedValue) Encoding encoding = Encoding.UTF8; List value = [.. encoding.GetBytes(notAllowedValue)]; - ValidationFeedback? nullableFeedback = validator.Validate(value, EntryType.DataItem, encoding, 0, 0); + KeyValuePair? nullableFeedback = validator.Validate(value, EntryType.DataItem, encoding, 0, 0, "foo"); Assert.IsNotNull(nullableFeedback); - ValidationFeedback feedback = (ValidationFeedback)nullableFeedback; - Assert.AreEqual(ValidationFeedbackRule.DataValidationFeedbackInvalidNumber, feedback.Rule); - Assert.AreEqual(notAllowedValue, feedback.AdditionalInfo); - Assert.AreEqual(ValidationFeedbackLevel.Error, feedback.Level); + KeyValuePair feedback = (KeyValuePair)nullableFeedback; + Assert.AreEqual(ValidationFeedbackRule.DataValidationFeedbackInvalidNumber, feedback.Key.Rule); + Assert.AreEqual(notAllowedValue, feedback.Value.AdditionalInfo); + Assert.AreEqual(ValidationFeedbackLevel.Error, feedback.Key.Level); } diff --git a/Px.Utils.UnitTests/Validation/DataValidationTests/DataSeparatorValidatorTest.cs b/Px.Utils.UnitTests/Validation/DataValidationTests/DataSeparatorValidatorTest.cs index 7c3b223f..4820a6de 100644 --- a/Px.Utils.UnitTests/Validation/DataValidationTests/DataSeparatorValidatorTest.cs +++ b/Px.Utils.UnitTests/Validation/DataValidationTests/DataSeparatorValidatorTest.cs @@ -12,7 +12,7 @@ public void FirstSeparatorIsUsedAsReference() { DataSeparatorValidator validator = new(); List separator = [.. Encoding.UTF8.GetBytes(" ")]; - ValidationFeedback? nullableFeedback = validator.Validate(separator, EntryType.DataItemSeparator, Encoding.UTF8, 1, 1); + KeyValuePair? nullableFeedback = validator.Validate(separator, EntryType.DataItemSeparator, Encoding.UTF8, 1, 1, "foo"); Assert.IsNull(nullableFeedback); } @@ -22,14 +22,14 @@ public void InconsistentSeparator() { DataSeparatorValidator validator = new(); List separator = [.. Encoding.UTF8.GetBytes(" ")]; - validator.Validate(separator, EntryType.DataItemSeparator, Encoding.UTF8, 1, 1); + validator.Validate(separator, EntryType.DataItemSeparator, Encoding.UTF8, 1, 1, "foo"); List otherSeparator = [.. Encoding.UTF8.GetBytes("\t")]; - ValidationFeedback? nullableFeedback = validator.Validate(otherSeparator, EntryType.DataItemSeparator, Encoding.UTF8, 1, 1); + KeyValuePair? nullableFeedback = validator.Validate(otherSeparator, EntryType.DataItemSeparator, Encoding.UTF8, 1, 1, "foo"); Assert.IsNotNull(nullableFeedback); - ValidationFeedback feedback = (ValidationFeedback)nullableFeedback; - Assert.AreEqual(ValidationFeedbackRule.DataValidationFeedbackInconsistentSeparator, feedback.Rule); - Assert.AreEqual(ValidationFeedbackLevel.Warning, feedback.Level); + KeyValuePair feedback = (KeyValuePair)nullableFeedback; + Assert.AreEqual(ValidationFeedbackRule.DataValidationFeedbackInconsistentSeparator, feedback.Key.Rule); + Assert.AreEqual(ValidationFeedbackLevel.Warning, feedback.Key.Level); } @@ -39,8 +39,8 @@ public void ConsistentSeparator() { DataSeparatorValidator validator = new(); List separator = [.. Encoding.UTF8.GetBytes(" ")]; - validator.Validate(separator, EntryType.DataItemSeparator, Encoding.UTF8, 1, 1); - ValidationFeedback? nullableFeedback = validator.Validate(separator, EntryType.DataItemSeparator, Encoding.UTF8, 1, 1); + validator.Validate(separator, EntryType.DataItemSeparator, Encoding.UTF8, 1, 1, "foo"); + KeyValuePair? nullableFeedback = validator.Validate(separator, EntryType.DataItemSeparator, Encoding.UTF8, 1, 1, "foo"); Assert.IsNull(nullableFeedback); } } diff --git a/Px.Utils.UnitTests/Validation/DataValidationTests/DataStringValueValidatorTests.cs b/Px.Utils.UnitTests/Validation/DataValidationTests/DataStringValueValidatorTests.cs index 7515ae50..7c106e9f 100644 --- a/Px.Utils.UnitTests/Validation/DataValidationTests/DataStringValueValidatorTests.cs +++ b/Px.Utils.UnitTests/Validation/DataValidationTests/DataStringValueValidatorTests.cs @@ -21,7 +21,7 @@ public void AllowedStrings(string allowedValue) DataStringValidator validator = new(); Encoding encoding = Encoding.UTF8; List value = [.. encoding.GetBytes(allowedValue)]; - ValidationFeedback? nullableFeedback = validator.Validate(value, EntryType.DataItem, encoding, 0, 0); + KeyValuePair? nullableFeedback = validator.Validate(value, EntryType.DataItem, encoding, 0, 0, "foo"); Assert.IsNull(nullableFeedback); } @@ -43,13 +43,13 @@ public void NotAllowedStringValue(string notAllowedValue) Encoding encoding = Encoding.UTF8; List value = [.. encoding.GetBytes(notAllowedValue)]; - ValidationFeedback? nullableFeedback = validator.Validate(value, EntryType.DataItem, encoding, 0, 0); + KeyValuePair? nullableFeedback = validator.Validate(value, EntryType.DataItem, encoding, 0, 0, "foo"); Assert.IsNotNull(nullableFeedback); - ValidationFeedback feedback = (ValidationFeedback)nullableFeedback; - Assert.AreEqual(ValidationFeedbackRule.DataValidationFeedbackInvalidString, feedback.Rule); - Assert.AreEqual(notAllowedValue, feedback.AdditionalInfo); - Assert.AreEqual(ValidationFeedbackLevel.Error, feedback.Level); + KeyValuePair feedback = (KeyValuePair)nullableFeedback; + Assert.AreEqual(ValidationFeedbackRule.DataValidationFeedbackInvalidString, feedback.Key.Rule); + Assert.AreEqual(notAllowedValue, feedback.Value.AdditionalInfo); + Assert.AreEqual(ValidationFeedbackLevel.Error, feedback.Key.Level); } } } \ No newline at end of file diff --git a/Px.Utils.UnitTests/Validation/DataValidationTests/DataStructureValidationTests.cs b/Px.Utils.UnitTests/Validation/DataValidationTests/DataStructureValidationTests.cs index 31728f27..1a45b003 100644 --- a/Px.Utils.UnitTests/Validation/DataValidationTests/DataStructureValidationTests.cs +++ b/Px.Utils.UnitTests/Validation/DataValidationTests/DataStructureValidationTests.cs @@ -21,14 +21,14 @@ public class DataStructureValidatorTest public void AllowedTokenSequences(params EntryType[] tokenSequence) { - List feedbacks = []; + ValidationFeedback feedbacks = []; DataStructureValidator validator = new(); foreach (EntryType tokenType in tokenSequence) { - ValidationFeedback? feedback = validator.Validate([], tokenType, Encoding.UTF8, 1, 1); + KeyValuePair? feedback = validator.Validate([], tokenType, Encoding.UTF8, 1, 1, "foo"); if (feedback is not null) { - feedbacks.Add((ValidationFeedback)feedback); + feedbacks.Add((KeyValuePair)feedback); } } @@ -44,23 +44,23 @@ public void AllowedTokenSequences(params EntryType[] tokenSequence) [DataRow([EntryType.DataItem, EntryType.DataItemSeparator, EntryType.EndOfData])] public void NotAllowedTokenSequences(params EntryType[] tokenSequence) { - List feedbacks = []; + ValidationFeedback feedbacks = []; DataStructureValidator validator = new(); foreach (EntryType tokenType in tokenSequence) { - ValidationFeedback? feedback = validator.Validate([], tokenType, Encoding.UTF8, 1, 1); + KeyValuePair? feedback = validator.Validate([], tokenType, Encoding.UTF8, 1, 1, "foo"); if (feedback is not null) { - feedbacks.Add((ValidationFeedback)feedback); + feedbacks.Add((KeyValuePair)feedback); } } Assert.AreEqual(1, feedbacks.Count); - Assert.AreEqual(ValidationFeedbackRule.DataValidationFeedbackInvalidStructure, feedbacks[0].Rule); - Assert.AreEqual(ValidationFeedbackLevel.Error, feedbacks[0].Level); + Assert.AreEqual(ValidationFeedbackRule.DataValidationFeedbackInvalidStructure, feedbacks.First().Key.Rule); + Assert.AreEqual(ValidationFeedbackLevel.Error, feedbacks.First().Key.Level); List expectedTokens = [ tokenSequence.Length > 1 ? tokenSequence[^2] : EntryType.Unknown, tokenSequence[^1] ]; - Assert.AreEqual(string.Join(",", expectedTokens), feedbacks[0].AdditionalInfo); + Assert.AreEqual(string.Join(",", expectedTokens), feedbacks.First().Value[0].AdditionalInfo); } } } \ No newline at end of file diff --git a/Px.Utils.UnitTests/Validation/DataValidationTests/DataValidationTest.cs b/Px.Utils.UnitTests/Validation/DataValidationTests/DataValidationTest.cs index 8fc50982..e64f3b73 100644 --- a/Px.Utils.UnitTests/Validation/DataValidationTests/DataValidationTest.cs +++ b/Px.Utils.UnitTests/Validation/DataValidationTests/DataValidationTest.cs @@ -15,16 +15,19 @@ public void TestValidateWithoutErrors() { using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(DataStreamContents.SIMPLE_VALID_DATA)); stream.Seek(6, 0); - DataValidator validator = new(stream, 5, 4, "foo", 1, Encoding.Default); + DataValidator validator = new(5, 4, 1); - ValidationFeedbackItem[] validationFeedbacks = validator.Validate().FeedbackItems; + ValidationFeedback validationFeedbacks = validator.Validate(stream, "foo", Encoding.UTF8).FeedbackItems; - foreach (ValidationFeedbackItem validationFeedback in validationFeedbacks) + foreach (KeyValuePair> validationFeedback in validationFeedbacks) { - Logger.LogMessage($"Line {validationFeedback.Feedback.Line}, Char {validationFeedback.Feedback.Character}: " - + $"{validationFeedback.Feedback.Rule} {validationFeedback.Feedback.AdditionalInfo}"); + foreach (ValidationFeedbackValue instance in validationFeedback.Value) + { + Logger.LogMessage($"Line {instance.Line}, Char {instance.Character}: " + + $"{validationFeedback.Key.Rule} {instance.AdditionalInfo}"); + } } - Assert.AreEqual(0, validationFeedbacks.Length); + Assert.AreEqual(0, validationFeedbacks.Count); } [TestMethod] @@ -32,17 +35,20 @@ public async Task TestValidateAsyncWithoutErrors() { using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(DataStreamContents.SIMPLE_VALID_DATA)); stream.Seek(6, 0); - DataValidator validator = new(stream, 5, 4, "foo", 1, Encoding.Default); + DataValidator validator = new(5, 4, 1); - ValidationResult result = await validator.ValidateAsync(); - ValidationFeedbackItem[] validationFeedbacks = result.FeedbackItems; + ValidationResult result = await validator.ValidateAsync(stream, "foo", Encoding.UTF8); + ValidationFeedback validationFeedbacks = result.FeedbackItems; - foreach (ValidationFeedbackItem validationFeedback in validationFeedbacks) + foreach (KeyValuePair> validationFeedback in validationFeedbacks) { - Logger.LogMessage($"Line {validationFeedback.Feedback.Line}, Char {validationFeedback.Feedback.Character}: " - + $"{validationFeedback.Feedback.Rule} {validationFeedback.Feedback.AdditionalInfo}"); + foreach (ValidationFeedbackValue instance in validationFeedback.Value) + { + Logger.LogMessage($"Line {instance.Line}, Char {instance.Character}: " + + $"{validationFeedback.Key.Rule} {instance.AdditionalInfo}"); + } } - Assert.AreEqual(0, validationFeedbacks.Length); + Assert.AreEqual(0, validationFeedbacks.Count); } @@ -51,16 +57,21 @@ public void TestValidateWithErrors() { using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(DataStreamContents.SIMPLE_INVALID_DATA)); stream.Seek(6, 0); - DataValidator validator = new(stream, 5, 4, "foo", 1, Encoding.Default); + DataValidator validator = new(5, 4, 1); - ValidationFeedbackItem[] validationFeedbacks = validator.Validate().FeedbackItems; + ValidationFeedback validationFeedbacks = validator.Validate(stream, "foo", Encoding.UTF8).FeedbackItems; - foreach (ValidationFeedbackItem validationFeedback in validationFeedbacks) + foreach (KeyValuePair> validationFeedback in validationFeedbacks) { - Logger.LogMessage($"Line {validationFeedback.Feedback.Line}, Char {validationFeedback.Feedback.Character}: " - + $"{validationFeedback.Feedback.Rule} {validationFeedback.Feedback.AdditionalInfo}"); + foreach (ValidationFeedbackValue instance in validationFeedback.Value) + { + Logger.LogMessage($"Line {instance.Line}, Char {instance.Character}: " + + $"{validationFeedback.Key.Rule} {instance.AdditionalInfo}"); + } } - Assert.AreEqual(13, validationFeedbacks.Length); + + Assert.AreEqual(7, validationFeedbacks.Count); // Unique feedbacks + Assert.AreEqual(13, validationFeedbacks.Values.SelectMany(f => f).Count()); // Total feedbacks including duplicates } [TestMethod] @@ -68,17 +79,106 @@ public async Task TestValidateAsyncWithErrors() { using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(DataStreamContents.SIMPLE_INVALID_DATA)); stream.Seek(6, 0); - DataValidator validator = new(stream, 5, 4, "foo", 1, Encoding.Default); + DataValidator validator = new(5, 4, 1); + + ValidationResult result = await validator.ValidateAsync(stream, "foo", Encoding.UTF8); + ValidationFeedback validationFeedbacks = result.FeedbackItems; + + foreach (KeyValuePair> validationFeedback in validationFeedbacks) + { + foreach (ValidationFeedbackValue instance in validationFeedback.Value) + { + Logger.LogMessage($"Line {instance.Line}, Char {instance.Character}: " + + $"{validationFeedback.Key.Rule} {instance.AdditionalInfo}"); + } + } + + Assert.AreEqual(7, validationFeedbacks.Count); // Unique feedbacks + Assert.AreEqual(13, validationFeedbacks.Values.SelectMany(f => f).Count()); // Total feedbacks including duplicates + } + + [TestMethod] + public void ValidateWithoutDataReturnsErrors() + { + using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(DataStreamContents.NO_DATA)); + DataValidator validator = new(5, 4, 1); + + ValidationFeedback validationFeedbacks = validator.Validate(stream, "foo", Encoding.UTF8).FeedbackItems; + + foreach (KeyValuePair> validationFeedback in validationFeedbacks) + { + foreach (ValidationFeedbackValue instance in validationFeedback.Value) + { + Logger.LogMessage($"Line {instance.Line}, Char {instance.Character}: " + + $"{validationFeedback.Key.Rule} {instance.AdditionalInfo}"); + } + } + + Assert.AreEqual(2, validationFeedbacks.Count); + } + + [TestMethod] + public async Task ValidateAsyncWithoutDataReturnsErrors() + { + using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(DataStreamContents.NO_DATA)); + DataValidator validator = new(5, 4, 1); - ValidationResult result = await validator.ValidateAsync(); - ValidationFeedbackItem[] validationFeedbacks = result.FeedbackItems; + ValidationResult result = await validator.ValidateAsync(stream, "foo", Encoding.UTF8); + ValidationFeedback validationFeedbacks = result.FeedbackItems; - foreach (ValidationFeedbackItem validationFeedback in validationFeedbacks) + foreach (KeyValuePair> validationFeedback in validationFeedbacks) { - Logger.LogMessage($"Line {validationFeedback.Feedback.Line}, Char {validationFeedback.Feedback.Character}: " - + $"{validationFeedback.Feedback.Rule} {validationFeedback.Feedback.AdditionalInfo}"); + foreach (ValidationFeedbackValue instance in validationFeedback.Value) + { + Logger.LogMessage($"Line {instance.Line}, Char {instance.Character}: " + + $"{validationFeedback.Key.Rule} {instance.AdditionalInfo}"); + } } - Assert.AreEqual(13, validationFeedbacks.Length); + + Assert.AreEqual(2, validationFeedbacks.Count); + } + + [TestMethod] + public void ValidateWithDataOnOnSingleRowReturnsErrors() + { + using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(DataStreamContents.DATA_ON_SINGLE_ROW)); + DataValidator validator = new(5, 4, 1); + + ValidationFeedback validationFeedbacks = validator.Validate(stream, "foo", Encoding.UTF8).FeedbackItems; + + foreach (KeyValuePair> validationFeedback in validationFeedbacks) + { + foreach (ValidationFeedbackValue instance in validationFeedback.Value) + { + Logger.LogMessage($"Line {instance.Line}, Char {instance.Character}: " + + $"{validationFeedback.Key.Rule} {instance.AdditionalInfo}"); + } + } + + Assert.AreEqual(2, validationFeedbacks.Count); // Unique feedbacks + Assert.AreEqual(6, validationFeedbacks.Values.SelectMany(f => f).Count()); // Total feedbacks including duplicates + } + + [TestMethod] + public async Task ValidateAsyncWithDataOnSingleRowReturnsErrors() + { + using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(DataStreamContents.DATA_ON_SINGLE_ROW)); + DataValidator validator = new(5, 4, 1); + + ValidationResult result = await validator.ValidateAsync(stream, "foo", Encoding.UTF8); + ValidationFeedback validationFeedbacks = result.FeedbackItems; + + foreach (KeyValuePair> validationFeedback in validationFeedbacks) + { + foreach (ValidationFeedbackValue instance in validationFeedback.Value) + { + Logger.LogMessage($"Line {instance.Line}, Char {instance.Character}: " + + $"{validationFeedback.Key.Rule} {instance.AdditionalInfo}"); + } + } + + Assert.AreEqual(2, validationFeedbacks.Count);// Unique feedbacks + Assert.AreEqual(6, validationFeedbacks.Values.SelectMany(f => f).Count()); // Total feedbacks including duplicates } } } \ No newline at end of file diff --git a/Px.Utils.UnitTests/Validation/DatabaseValidation/DatabaseValidatorFunctionTests.cs b/Px.Utils.UnitTests/Validation/DatabaseValidation/DatabaseValidatorFunctionTests.cs new file mode 100644 index 00000000..4703b6f6 --- /dev/null +++ b/Px.Utils.UnitTests/Validation/DatabaseValidation/DatabaseValidatorFunctionTests.cs @@ -0,0 +1,151 @@ +using Px.Utils.Validation; +using Px.Utils.Validation.DatabaseValidation; +using System.Text; + +namespace Px.Utils.UnitTests.Validation.DatabaseValidation +{ + [TestClass] + public class DatabaseValidatorFunctionTests + { + [TestMethod] + public void DuplicatePxFileNameWithoutDuplicateNameReturnsNull() + { + // Arrange + List pxFiles = [ + new("foo.px", "path/to/file", ["fi", "en", "sv"], Encoding.UTF8), + new("bar.px", "path/to/file", ["fi", "en", "sv"], Encoding.UTF8) + ]; + DuplicatePxFileName validator = new (pxFiles); + DatabaseFileInfo fileInfo = new("baz.px", "path/to/file", ["fi", "en", "sv"], Encoding.UTF8); + + // Act + KeyValuePair? feedback = validator.Validate(fileInfo); + + // Assert + Assert.IsNull(feedback); + } + + [TestMethod] + public void DuplicatePxFileNameWithDuplicateNameReturnsFeedback() + { + // Arrange + List pxFiles = [ + new("foo.px", "path/to/file", ["fi", "en", "sv"], Encoding.UTF8), + new("bar.px", "path/to/file", ["fi", "en", "sv"], Encoding.UTF8) + ]; + DuplicatePxFileName validator = new (pxFiles); + DatabaseFileInfo fileInfo = new("bar.px", "path/to/file", ["fi", "en", "sv"], Encoding.UTF8); + + // Act + KeyValuePair? feedback = validator.Validate(fileInfo); + + // Assert + Assert.IsNotNull(feedback); + Assert.AreEqual(ValidationFeedbackRule.DuplicateFileNames, feedback.Value.Key.Rule); + } + + [TestMethod] + public void MissingPxFileLanguagesWithoutMissingLanguagesReturnsNull() + { + // Arrange + IEnumerable allLanguages = ["fi", "en", "sv"]; + MissingPxFileLanguages validator = new (allLanguages); + DatabaseFileInfo fileInfo = new("foo.px", "path/to/file", ["fi", "en", "sv"], Encoding.UTF8); + + // Act + KeyValuePair? feedback = validator.Validate(fileInfo); + + // Assert + Assert.IsNull(feedback); + } + + [TestMethod] + public void MissingPxFileLanguagesWithMissingLanguagesReturnsFeedback() + { + // Arrange + IEnumerable allLanguages = ["fi", "en", "sv"]; + MissingPxFileLanguages validator = new (allLanguages); + DatabaseFileInfo fileInfo = new("foo.px", "path/to/file", ["fi", "en"], Encoding.UTF8); + + // Act + KeyValuePair? feedback = validator.Validate(fileInfo); + + // Assert + Assert.IsNotNull(feedback); + Assert.AreEqual(ValidationFeedbackRule.FileLanguageDiffersFromDatabase, feedback.Value.Key.Rule); + } + + [TestMethod] + public void MismatchingEncodingWithMatchingEncodingReturnsNull() + { + // Arrange + Encoding mostCommonEncoding = Encoding.UTF8; + MismatchingEncoding validator = new (mostCommonEncoding); + DatabaseFileInfo fileInfo = new("foo.px", "path/to/file", ["fi", "en", "sv"], Encoding.UTF8); + + // Act + KeyValuePair? feedback = validator.Validate(fileInfo); + + // Assert + Assert.IsNull(feedback); + } + + [TestMethod] + public void MismatchingEncodingWithMismatchingEncodingReturnsFeedback() + { + // Arrange + Encoding mostCommonEncoding = Encoding.UTF8; + MismatchingEncoding validator = new (mostCommonEncoding); + DatabaseFileInfo fileInfo = new("foo.px", "path/to/file", ["fi", "en", "sv"], Encoding.UTF32); + + // Act + KeyValuePair? feedback = validator.Validate(fileInfo); + + // Assert + Assert.IsNotNull(feedback); + Assert.AreEqual(ValidationFeedbackRule.FileEncodingDiffersFromDatabase, feedback.Value.Key.Rule); + } + + [TestMethod] + public void MissingAliasFilesWithoutMissingFilesReturnsNull() + { + // Arrange + string path = "path/to/file"; + List aliasFiles = [ + new("Alias_fi.txt", path, ["fi"], Encoding.UTF8), + new("Alias_en.txt", path, ["en"], Encoding.UTF8), + new("Alias_sv.txt", path, ["sv"], Encoding.UTF8) + ]; + IEnumerable allLanguages = ["fi", "en", "sv"]; + MissingAliasFiles validator = new (aliasFiles, allLanguages); + DatabaseValidationItem directoryInfo = new(path); + + // Act + KeyValuePair? feedback = validator.Validate(directoryInfo); + + // Assert + Assert.IsNull(feedback); + } + + [TestMethod] + public void MissingAliasFilesWithMissingFilesReturnsFeedback() + { + // Arrange + string path = "path/to/file"; + List aliasFiles = [ + new("Alias_fi.txt", path, [], Encoding.UTF8), + new("Alias_en.txt", path, [], Encoding.UTF8) + ]; + IEnumerable allLanguages = ["fi", "en", "sv"]; + MissingAliasFiles validator = new (aliasFiles, allLanguages); + DatabaseValidationItem directoryInfo = new(path); + + // Act + KeyValuePair? feedback = validator.Validate(directoryInfo); + + // Assert + Assert.IsNotNull(feedback); + Assert.AreEqual(ValidationFeedbackRule.AliasFileMissing, feedback.Value.Key.Rule); + } + } +} diff --git a/Px.Utils.UnitTests/Validation/DatabaseValidation/DatabaseValidatorTests.cs b/Px.Utils.UnitTests/Validation/DatabaseValidation/DatabaseValidatorTests.cs new file mode 100644 index 00000000..5ebfb745 --- /dev/null +++ b/Px.Utils.UnitTests/Validation/DatabaseValidation/DatabaseValidatorTests.cs @@ -0,0 +1,234 @@ +using Px.Utils.Validation.DatabaseValidation; +using Px.Utils.Validation; + +namespace Px.Utils.UnitTests.Validation.DatabaseValidation +{ + [TestClass] + public class DatabaseValidatorTests + { + [TestMethod] + public void ValidateDatabaseWithValidDatabaseReturnsValidResult() + { + // Arrange + MockFileSystem fileSystem = new(); + DatabaseValidator validator = new("database", fileSystem: fileSystem); + + // Act + ValidationResult result = validator.Validate(); + + // Assert + Assert.IsNotNull(result, "Validation result should not be null"); + Assert.AreEqual(0, result.FeedbackItems.Count); + } + + [TestMethod] + public async Task ValidateDatabaseAsyncWithValidDatabaseReturnsValidResult() + { + // Arrange + MockFileSystem fileSystem = new(); + DatabaseValidator validator = new("database", fileSystem: fileSystem); + + // Act + ValidationResult result = await validator.ValidateAsync(); + + // Assert + Assert.IsNotNull(result, "Validation result should not be null"); + Assert.AreEqual(0, result.FeedbackItems.Count); + } + + [TestMethod] + public void ValidateDatabaseWithInvalidDatabaseReturnsFeedback() + { + // Arrange + MockFileSystem fileSystem = new(); + DatabaseValidator validator = new("database_invalid", fileSystem: fileSystem); + ValidationFeedbackKey invalidValueFormatKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.InvalidValueFormat); + ValidationFeedbackKey recommendedKeyMissingKey = new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.RecommendedKeyMissing); + ValidationFeedbackKey entryWithMultipleValuesKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.EntryWithMultipleValues); + ValidationFeedbackKey dataValidationFeedbackInvalidStructureKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidStructure); + ValidationFeedbackKey dataValidationFeedbackInvalidRowCountKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidRowCount); + ValidationFeedbackKey dataValidationFeedbackInvalidRowLengthKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidRowLength); + ValidationFeedbackKey unmatchingValueTypeKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.UnmatchingValueType); + ValidationFeedbackKey excessNewLinesInValueKey = new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.ExcessNewLinesInValue); + ValidationFeedbackKey missingStubAndHeadingKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.MissingStubAndHeading); + + // Act + ValidationResult result = validator.Validate(); + + // Assert + Assert.IsNotNull(result, "Validation result should not be null"); + Assert.AreEqual(9, result.FeedbackItems.Count); // Unique feedbacks + Assert.AreEqual(11, result.FeedbackItems.Values.SelectMany(f => f).Count()); // Total feedbacks including duplicates + Assert.IsTrue(result.FeedbackItems.ContainsKey(invalidValueFormatKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(recommendedKeyMissingKey)); + Assert.AreEqual(3, result.FeedbackItems[recommendedKeyMissingKey].Count); // 3 warnings + Assert.IsTrue(result.FeedbackItems.ContainsKey(entryWithMultipleValuesKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(dataValidationFeedbackInvalidStructureKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(dataValidationFeedbackInvalidRowCountKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(dataValidationFeedbackInvalidRowLengthKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(unmatchingValueTypeKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(excessNewLinesInValueKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(missingStubAndHeadingKey)); + } + + [TestMethod] + public async Task ValidateDatabaseAsyncWithInvaliDatabaseReturnsFeedback() + { + // Arrange + MockFileSystem fileSystem = new(); + DatabaseValidator validator = new("database_invalid", fileSystem: fileSystem); + ValidationFeedbackKey invalidValueFormatKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.InvalidValueFormat); + ValidationFeedbackKey recommendedKeyMissingKey = new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.RecommendedKeyMissing); + ValidationFeedbackKey entryWithMultipleValuesKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.EntryWithMultipleValues); + ValidationFeedbackKey dataValidationFeedbackInvalidStructureKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidStructure); + ValidationFeedbackKey dataValidationFeedbackInvalidRowCountKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidRowCount); + ValidationFeedbackKey dataValidationFeedbackInvalidRowLengthKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidRowLength); + ValidationFeedbackKey unmatchingValueTypeKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.UnmatchingValueType); + ValidationFeedbackKey excessNewLinesInValueKey = new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.ExcessNewLinesInValue); + ValidationFeedbackKey missingStubAndHeadingKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.MissingStubAndHeading); + + // Act + ValidationResult result = await validator.ValidateAsync(); + + // Assert + Assert.IsNotNull(result, "Validation result should not be null"); + Assert.AreEqual(9, result.FeedbackItems.Count); // Unique feedbacks + Assert.AreEqual(11, result.FeedbackItems.Values.SelectMany(f => f).Count()); // Total feedbacks including duplicates + Assert.IsTrue(result.FeedbackItems.ContainsKey(invalidValueFormatKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(recommendedKeyMissingKey)); + Assert.AreEqual(3, result.FeedbackItems[recommendedKeyMissingKey].Count); // 3 warnings + Assert.IsTrue(result.FeedbackItems.ContainsKey(entryWithMultipleValuesKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(dataValidationFeedbackInvalidStructureKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(dataValidationFeedbackInvalidRowCountKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(dataValidationFeedbackInvalidRowLengthKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(unmatchingValueTypeKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(excessNewLinesInValueKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(missingStubAndHeadingKey)); + } + + [TestMethod] + public void ValidateDatabaseWithCustomFunctionsReturnsFeedback() + { + // Arrange + MockFileSystem fileSystem = new(); + IDatabaseValidator[] customValidators = + [ + new MockCustomDatabaseValidator() + ]; + DatabaseValidator validator = new( + "database_invalid", + customPxFileValidators: customValidators, + customAliasFileValidators: customValidators, + customDirectoryValidators: customValidators, + fileSystem: fileSystem); + ValidationFeedbackKey invalidValueFormatKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.InvalidValueFormat); + ValidationFeedbackKey aliasFileMissingKey = new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.AliasFileMissing); + ValidationFeedbackKey recommendedKeyMissingKey = new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.RecommendedKeyMissing); + ValidationFeedbackKey entryWithMultipleValuesKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.EntryWithMultipleValues); + ValidationFeedbackKey dataValidationFeedbackInvalidStructureKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidStructure); + ValidationFeedbackKey dataValidationFeedbackInvalidRowCountKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidRowCount); + ValidationFeedbackKey dataValidationFeedbackInvalidRowLengthKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidRowLength); + ValidationFeedbackKey unmatchingValueTypeKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.UnmatchingValueType); + ValidationFeedbackKey excessNewLinesInValueKey = new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.ExcessNewLinesInValue); + ValidationFeedbackKey missingStubAndHeadingKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.MissingStubAndHeading); + + // Act + ValidationResult result = validator.Validate(); + + // Assert + Assert.IsNotNull(result, "Validation result should not be null"); + Assert.AreEqual(10, result.FeedbackItems.Count); // Unique feedbacks + Assert.AreEqual(26, result.FeedbackItems.Values.SelectMany(f => f).Count()); // Total feedbacks including duplicates + Assert.IsTrue(result.FeedbackItems.ContainsKey(invalidValueFormatKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(aliasFileMissingKey)); + Assert.AreEqual(15, result.FeedbackItems[aliasFileMissingKey].Count); // 15 warnings + Assert.IsTrue(result.FeedbackItems.ContainsKey(recommendedKeyMissingKey)); + Assert.AreEqual(3, result.FeedbackItems[recommendedKeyMissingKey].Count); // 3 warnings + Assert.IsTrue(result.FeedbackItems.ContainsKey(entryWithMultipleValuesKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(dataValidationFeedbackInvalidStructureKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(dataValidationFeedbackInvalidRowCountKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(dataValidationFeedbackInvalidRowLengthKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(unmatchingValueTypeKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(excessNewLinesInValueKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(missingStubAndHeadingKey)); + } + + [TestMethod] + public async Task ValidateDatabasAsynceWithCustomFunctionsReturnsFeedback() + { + // Arrange + MockFileSystem fileSystem = new(); + IDatabaseValidator[] customValidators = + [ + new MockCustomDatabaseValidator() + ]; + DatabaseValidator validator = new( + "database_invalid", + customPxFileValidators: customValidators, + customAliasFileValidators: customValidators, + customDirectoryValidators: customValidators, + fileSystem: fileSystem); + ValidationFeedbackKey invalidValueFormatKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.InvalidValueFormat); + ValidationFeedbackKey aliasFileMissingKey = new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.AliasFileMissing); + ValidationFeedbackKey recommendedKeyMissingKey = new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.RecommendedKeyMissing); + ValidationFeedbackKey entryWithMultipleValuesKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.EntryWithMultipleValues); + ValidationFeedbackKey dataValidationFeedbackInvalidStructureKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidStructure); + ValidationFeedbackKey dataValidationFeedbackInvalidRowCountKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidRowCount); + ValidationFeedbackKey dataValidationFeedbackInvalidRowLengthKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidRowLength); + ValidationFeedbackKey unmatchingValueTypeKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.UnmatchingValueType); + ValidationFeedbackKey excessNewLinesInValueKey = new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.ExcessNewLinesInValue); + ValidationFeedbackKey missingStubAndHeadingKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.MissingStubAndHeading); + + + // Act + ValidationResult result = await validator.ValidateAsync(); + + // Assert + Assert.IsNotNull(result, "Validation result should not be null"); + Assert.AreEqual(10, result.FeedbackItems.Count); // Unique feedbacks + Assert.AreEqual(26, result.FeedbackItems.Values.SelectMany(f => f).Count()); // Total feedbacks including duplicates + Assert.IsTrue(result.FeedbackItems.ContainsKey(invalidValueFormatKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(aliasFileMissingKey)); + Assert.AreEqual(15, result.FeedbackItems[aliasFileMissingKey].Count); // 15 warnings + Assert.IsTrue(result.FeedbackItems.ContainsKey(recommendedKeyMissingKey)); + Assert.AreEqual(3, result.FeedbackItems[recommendedKeyMissingKey].Count); // 3 warnings + Assert.IsTrue(result.FeedbackItems.ContainsKey(entryWithMultipleValuesKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(dataValidationFeedbackInvalidStructureKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(dataValidationFeedbackInvalidRowCountKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(dataValidationFeedbackInvalidRowLengthKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(unmatchingValueTypeKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(excessNewLinesInValueKey)); + Assert.IsTrue(result.FeedbackItems.ContainsKey(missingStubAndHeadingKey)); + } + + [TestMethod] + public void ValidateDatabaseWithSingleLanguagePxFileReturnsValidResult() + { + // Arrange + MockFileSystem fileSystem = new(); + DatabaseValidator validator = new("database_single_language", fileSystem: fileSystem); + + // Act + ValidationResult result = validator.Validate(); + + // Assert + Assert.IsNotNull(result, "Validation result should not be null"); + Assert.AreEqual(0, result.FeedbackItems.Count); + } + + [TestMethod] + public async Task ValidateDatabaseAsyncWithSingleLanguagePxFileReturnsValidResult() + { + // Arrange + MockFileSystem fileSystem = new(); + DatabaseValidator validator = new("database_single_language", fileSystem: fileSystem); + + // Act + ValidationResult result = await validator.ValidateAsync(); + + // Assert + Assert.IsNotNull(result, "Validation result should not be null"); + Assert.AreEqual(0, result.FeedbackItems.Count); + } + } +} diff --git a/Px.Utils.UnitTests/Validation/DatabaseValidation/MockCustomValidatorFunctions.cs b/Px.Utils.UnitTests/Validation/DatabaseValidation/MockCustomValidatorFunctions.cs new file mode 100644 index 00000000..6c36cd19 --- /dev/null +++ b/Px.Utils.UnitTests/Validation/DatabaseValidation/MockCustomValidatorFunctions.cs @@ -0,0 +1,15 @@ +using Px.Utils.Validation; +using Px.Utils.Validation.DatabaseValidation; + +namespace Px.Utils.UnitTests.Validation.DatabaseValidation +{ + internal sealed class MockCustomDatabaseValidator : IDatabaseValidator + { + public KeyValuePair? Validate(DatabaseValidationItem item) + { + return new ( + new (ValidationFeedbackLevel.Warning, ValidationFeedbackRule.AliasFileMissing), + new ("test", 0, 0, "Test error")); + } + } +} diff --git a/Px.Utils.UnitTests/Validation/DatabaseValidation/MockDatabaseFileStreams.cs b/Px.Utils.UnitTests/Validation/DatabaseValidation/MockDatabaseFileStreams.cs new file mode 100644 index 00000000..44235375 --- /dev/null +++ b/Px.Utils.UnitTests/Validation/DatabaseValidation/MockDatabaseFileStreams.cs @@ -0,0 +1,37 @@ +using Px.Utils.UnitTests.Validation.Fixtures; + +namespace Px.Utils.UnitTests.Validation.DatabaseValidation +{ + internal static class MockDatabaseFileStreams + { + private readonly static string _databaseAliasFi = "database/Alias_fi.txt"; + private readonly static string _databaseAliasEn = "database/Alias_en.txt"; + private readonly static string _databaseAliasSv = "database/Alias_sv.txt"; + private readonly static string _databaseCategoryAliasFi = "database/category/Alias_fi.txt"; + private readonly static string _databaseCategoryAliasEn = "database/category/Alias_en.txt"; + private readonly static string _databaseCategoryAliasSv = "database/category/Alias_sv.txt"; + private readonly static string _databaseCategoryDirectoryAliasFi = "database/category/directory/Alias_fi.txt"; + private readonly static string _databaseCategoryDirectoryAliasEn = "database/category/directory/Alias_en.txt"; + private readonly static string _databaseCategoryDirectoryAliasSv = "database/category/directory/Alias_sv.txt"; + + internal static Dictionary FileStreams = new() + { + { _databaseAliasFi, _databaseAliasFi }, + { _databaseAliasEn, _databaseAliasEn }, + { _databaseAliasSv, _databaseAliasSv }, + { _databaseCategoryAliasFi, _databaseCategoryAliasFi }, + { _databaseCategoryAliasEn, _databaseCategoryAliasEn }, + { _databaseCategoryAliasSv, _databaseCategoryAliasSv }, + { _databaseCategoryDirectoryAliasFi, _databaseCategoryDirectoryAliasFi }, + { _databaseCategoryDirectoryAliasEn, _databaseCategoryDirectoryAliasEn }, + { _databaseCategoryDirectoryAliasSv, _databaseCategoryDirectoryAliasSv }, + { "database/category/directory/foo.px", PxFileFixtures.MINIMAL_PX_FILE }, + { "database/category/directory/bar.px", PxFileFixtures.MINIMAL_PX_FILE }, + { "database/category/directory/baz.px", PxFileFixtures.MINIMAL_PX_FILE }, + { "database/category/directory/foo_sl.px", PxFileFixtures.MINIMAL_SINGLE_LANGUAGE_PX_FILE }, + { "database/category/directory/bar_sl.px", PxFileFixtures.MINIMAL_SINGLE_LANGUAGE_PX_FILE }, + { "database/category/directory/baz_sl.px", PxFileFixtures.MINIMAL_SINGLE_LANGUAGE_PX_FILE }, + { "database/category/directory/invalid.px", PxFileFixtures.INVALID_PX_FILE } + }; + } +} diff --git a/Px.Utils.UnitTests/Validation/DatabaseValidation/MockFileSystem.cs b/Px.Utils.UnitTests/Validation/DatabaseValidation/MockFileSystem.cs new file mode 100644 index 00000000..7d3738b3 --- /dev/null +++ b/Px.Utils.UnitTests/Validation/DatabaseValidation/MockFileSystem.cs @@ -0,0 +1,104 @@ +using Px.Utils.Validation.DatabaseValidation; +using System.Text; + +namespace Px.Utils.UnitTests.Validation.DatabaseValidation +{ + /// + /// Mock file system used for database validation tests. + /// + public class MockFileSystem : IFileSystem + { + private readonly Dictionary> _directories = new() + { + { "database", ["database/category", "database/category/directory", "database/_INDEX"] }, + { "database_invalid", ["database_invalid/category", "database_invalid/category/directory", "database_invalid/_INDEX"] }, + { "database_single_language", ["database_single_language/category", "database_single_language/category/directory", "database_single_language/_INDEX"] } + }; + + private readonly Dictionary> _files = new() + { + { "database", [ + "database/Alias_fi.txt", + "database/Alias_en.txt", + "database/Alias_sv.txt", + "database/category/Alias_fi.txt", + "database/category/Alias_en.txt", + "database/category/Alias_sv.txt", + "database/category/directory/foo.px", + "database/category/directory/bar.px", + "database/category/directory/baz.px", + "database/category/directory/Alias_fi.txt", + "database/category/directory/Alias_en.txt", + "database/category/directory/Alias_sv.txt", + ] }, + { "database_invalid", [ + "database/Alias_fi.txt", + "database/Alias_en.txt", + "database/Alias_sv.txt", + "database/category/Alias_fi.txt", + "database/category/Alias_en.txt", + "database/category/Alias_sv.txt", + "database/category/directory/foo.px", + "database/category/directory/bar.px", + "database/category/directory/baz.px", + "database/category/directory/invalid.px", + "database/category/directory/Alias_fi.txt", + "database/category/directory/Alias_en.txt", + "database/category/directory/Alias_sv.txt", + ] }, + { "database_single_language", [ + "database/Alias_en.txt", + "database/category/Alias_en.txt", + "database/category/directory/foo_sl.px", + "database/category/directory/bar_sl.px", + "database/category/directory/baz_sl.px", + "database/category/directory/Alias_en.txt", + ] } + }; + + public IEnumerable EnumerateDirectories(string path) + { + return _directories[path]; + } + + public IEnumerable EnumerateFiles(string path, string searchPattern) + { + IEnumerable files = _files[path]; + if (searchPattern == "*.px") + { + return files.Where(f => f.EndsWith(".px", StringComparison.InvariantCultureIgnoreCase)); + } + else + { + return files.Where(f => f.EndsWith(".txt", StringComparison.InvariantCultureIgnoreCase)); + } + } + + public string GetDirectoryName(string path) + { + string? directoryName = Path.GetDirectoryName(path); + return directoryName ?? throw new ArgumentException("Path does not contain a directory."); + } + + public string GetFileName(string path) + { + return Path.GetFileName(path); + } + + public Stream GetFileStream(string path) + { + byte[] data = Encoding.UTF8.GetBytes(MockDatabaseFileStreams.FileStreams[path]); + return new MemoryStream(data); + } + + public Encoding GetEncoding(Stream stream) + { + return Encoding.UTF8; + } + + public async Task GetEncodingAsync(Stream stream, CancellationToken cancellationToken) + { + return await Task.Run(() => GetEncoding(stream)); + } + } +} diff --git a/Px.Utils.UnitTests/Validation/Fixtures/ContentValidationFixtures.cs b/Px.Utils.UnitTests/Validation/Fixtures/ContentValidationFixtures.cs index bdea31f4..47eca8b4 100644 --- a/Px.Utils.UnitTests/Validation/Fixtures/ContentValidationFixtures.cs +++ b/Px.Utils.UnitTests/Validation/Fixtures/ContentValidationFixtures.cs @@ -99,16 +99,52 @@ internal static class ContentValidationFixtures private static readonly ValidationStructuredEntry stubEntry = new(filename, new ValidationStructuredEntryKey("STUB"), - "bar", + "\"bar\"", + 8, + [], + 5, + Utils.Validation.ValueType.ListOfStrings); + + private static readonly ValidationStructuredEntry multipleStubNamesEntry = + new(filename, + new ValidationStructuredEntryKey("STUB"), + "\"bar\", \"baz\"", 8, [], 5, Utils.Validation.ValueType.ListOfStrings); + private static readonly ValidationStructuredEntry multipleHeadingNamesEntry = + new(filename, + new ValidationStructuredEntryKey("HEADING"), + "\"bar, time\", \"baz, time\"", + 10, + [], + 5, + Utils.Validation.ValueType.ListOfStrings); + + private static readonly ValidationStructuredEntry multipleEnStubNamesEntry = + new(filename, + new ValidationStructuredEntryKey("STUB", "en"), + "\"bar-en\", \"baz-en\"", + 8, + [], + 5, + Utils.Validation.ValueType.ListOfStrings); + + private static readonly ValidationStructuredEntry multipleEnHeadingNamesEntry = + new(filename, + new ValidationStructuredEntryKey("HEADING", "en"), + "\"bar, time (en)\", \"baz, time (en)\"", + 10, + [], + 5, + Utils.Validation.ValueType.ListOfStrings); + private static readonly ValidationStructuredEntry stubEnEntry = new(filename, new ValidationStructuredEntryKey("STUB", "en"), - "bar-en", + "\"bar-en\"", 9, [], 9, @@ -117,7 +153,16 @@ internal static class ContentValidationFixtures private static readonly ValidationStructuredEntry headingEntry = new(filename, new ValidationStructuredEntryKey("HEADING"), - "bar-time", + "\"bar-time\"", + 10, + [], + 5, + Utils.Validation.ValueType.ListOfStrings); + + private static readonly ValidationStructuredEntry headingEntryWithBar = + new(filename, + new ValidationStructuredEntryKey("HEADING"), + "\"bar-time\",\"bar\"", 10, [], 5, @@ -126,7 +171,7 @@ internal static class ContentValidationFixtures private static readonly ValidationStructuredEntry headingEnEntry = new(filename, new ValidationStructuredEntryKey("HEADING", "en"), - "bar-time-en", + "\"bar-time-en\"", 11, [], 5, @@ -186,6 +231,15 @@ internal static class ContentValidationFixtures 7, Utils.Validation.ValueType.ListOfStrings); + private static readonly ValidationStructuredEntry moreValuesForBarEntry = + new(filename, + new ValidationStructuredEntryKey("VALUES", null, "bar"), + "bar,baz", + 17, + [], + 7, + Utils.Validation.ValueType.ListOfStrings); + private static readonly ValidationStructuredEntry valuesBarEnEntry = new(filename, new ValidationStructuredEntryKey("VALUES", "en", "bar-en"), @@ -204,6 +258,24 @@ internal static class ContentValidationFixtures 13, Utils.Validation.ValueType.DateTime); + private static readonly ValidationStructuredEntry lastUpdatedBarFooEntryOneSpecifier = + new(filename, + new ValidationStructuredEntryKey("LAST-UPDATED", null, "bar"), + "20230131 08:00", + 19, + [], + 13, + Utils.Validation.ValueType.DateTime); + + private static readonly ValidationStructuredEntry unitsBarFooEnEntryOneSpecifier = + new(filename, + new ValidationStructuredEntryKey("UNITS", "en", "foo-en"), + "unit", + 20, + [], + 13, + Utils.Validation.ValueType.StringValue); + private static readonly ValidationStructuredEntry unitsBarFooEntry = new(filename, new ValidationStructuredEntryKey("UNITS", null, "bar", "foo"), @@ -422,6 +494,22 @@ internal static class ContentValidationFixtures stubEntry, ]; + internal static ValidationStructuredEntry[] STRUCTURED_ENTRY_ARRAY_WITH_DUPLICATE_DIMENSION => + [ + stubEntry, + stubEnEntry, + headingEntryWithBar, + headingEnEntry + ]; + + internal static ValidationStructuredEntry[] STRUCTURED_ENTRY_ARRAY_WITH_MULTIPLE_DIMENSION_NAMES => + [ + multipleStubNamesEntry, + multipleHeadingNamesEntry, + multipleEnStubNamesEntry, + multipleEnHeadingNamesEntry + ]; + internal static ValidationStructuredEntry[] STRUCTURED_ENTRY_ARRAY_WITH_DESCRIPTION => [ descriptionEntry, @@ -432,12 +520,26 @@ internal static class ContentValidationFixtures valuesBarEntry, ]; + internal static ValidationStructuredEntry[] STRUCTURED_ENTRY_ARRAY_WITH_DUPLICATE_DIMENSION_VALUES => + [ + valuesBarEntry, + valuesBarEnEntry, + moreValuesForBarEntry + ]; + internal static ValidationStructuredEntry[] STRUCTURED_ENTRY_ARRAY_WITH_INVALID_CONTENT_VALUE_KEY_ENTRIES => [ unitsBarFooEntry, precisionBarFooEntry, ]; + internal static ValidationStructuredEntry[] STRUCTURED_ENTRY_ARRAY_WITH_MISSING_RECIMMENDED_SPECIFIERS => + [ + lastUpdatedBarFooEntryOneSpecifier, + unitsBarFooEntry, + unitsBarFooEnEntryOneSpecifier, + ]; + internal static ValidationStructuredEntry[] STRUCTURED_ENTRY_ARRAY_WITH_INCOMPLETE_VARIABLE_RECOMMENDED_KEYS => [ timevalEntry, diff --git a/Px.Utils.UnitTests/Validation/Fixtures/DataStreamContents.cs b/Px.Utils.UnitTests/Validation/Fixtures/DataStreamContents.cs index ab71b886..3db547ad 100644 --- a/Px.Utils.UnitTests/Validation/Fixtures/DataStreamContents.cs +++ b/Px.Utils.UnitTests/Validation/Fixtures/DataStreamContents.cs @@ -16,5 +16,11 @@ internal static class DataStreamContents "\".\" \"..\" \"...\" \"....\" \".....\" \r" + "\"dots\" \"-\" \"..123\" -1 1.2 -1.3 \r\n" + "1 2 \0 4 5 \r\n;"; + + internal static string NO_DATA => + "DATA=\n"; + + internal static string DATA_ON_SINGLE_ROW => + "DATA=1 2 3 4 5 6 7 8 9 10;"; } } \ No newline at end of file diff --git a/Px.Utils.UnitTests/Validation/Fixtures/PxFileFixtures.cs b/Px.Utils.UnitTests/Validation/Fixtures/PxFileFixtures.cs index 98f1b652..542a4491 100644 --- a/Px.Utils.UnitTests/Validation/Fixtures/PxFileFixtures.cs +++ b/Px.Utils.UnitTests/Validation/Fixtures/PxFileFixtures.cs @@ -44,6 +44,78 @@ internal static class PxFileFixtures "\r\n18 19; " + "\r\n"; + internal static string PX_FILE_WITHOUT_DATA = + "CHARSET=\"ANSI\";" + + "\r\nAXIS-VERSION=\"2013\";" + + "\r\nCODEPAGE=\"UTF-8\";" + + "\r\nLANGUAGE=\"fi\";" + + "\r\nLANGUAGES=\"fi\";" + + "\r\nNEXT-UPDATE=\"20240201 08:00\";" + + "\r\nTABLEID=\"table-id\";" + + "\r\nSUBJECT-AREA=\"subject-area\";" + + "\r\nCOPYRIGHT=YES;" + + "\r\nDESCRIPTION=\"lorem ipsum\";" + + "\r\nSTUB=\"Vuosi\";" + + "\r\nHEADING=\"Tiedot\";" + + "\r\nCONTVARIABLE=\"Tiedot\";" + + "\r\nVALUES(\"Vuosi\")=\"2015\",\"2016\",\"2017\",\"2018\",\"2019\",\"2020\",\"2021\",\"2022\",\"2023\",\"2024\";" + + "\r\nVALUES(\"Tiedot\")=\"foo-val-a\",\"foo-val-b\";" + + "\r\nTIMEVAL(\"Vuosi\")=TLIST(A1),\"2015\",\"2016\",\"2017\",\"2018\",\"2019\",\"2020\",\"2021\",\"2022\",\"2023\",\"2024\";" + + "\r\nCODES(\"Vuosi\")=\"2015\",\"2016\",\"2017\",\"2018\",\"2019\",\"2020\",\"2021\",\"2022\",\"2023\",\"2024\";" + + "\r\nCODES(\"Tiedot\")=\"code-foo-val-a\",\"code-foo-val-b\";" + + "\r\nVARIABLE-TYPE(\"Vuosi\")=\"Time\";" + + "\r\nVARIABLE-TYPE(\"Tiedot\")=\"Content\";" + + "\r\nPRECISION(\"Tiedot\",\"foo-val-a\")=1;" + + "\r\nPRECISION(\"Tiedot\",\"foo-val-b\")=1;" + + "\r\nLAST-UPDATED(\"Tiedot\",\"foo-val-a\")=\"20231101 08:00\";" + + "\r\nLAST-UPDATED(\"Tiedot\",\"foo-val-b\")=\"20231101 08:00\";" + + "\r\nUNITS(\"Tiedot\",\"foo-val-a\")=\"kpl\";" + + "\r\nUNITS(\"Tiedot\",\"foo-val-b\")=\"%\";" + + "\r\nVARIABLECODE(\"Tiedot\")=\"code-tiedot\";" + + "\r\nVARIABLECODE(\"Vuosi\")=\"code-vuosi\";"; + + internal static string MINIMAL_SINGLE_LANGUAGE_PX_FILE = + "CHARSET=\"ANSI\";" + + "\r\nAXIS-VERSION=\"2013\";" + + "\r\nCODEPAGE=\"UTF-8\";" + + "\r\nLANGUAGE=\"en\";" + + "\r\nLANGUAGES=\"en\";" + + "\r\nNEXT-UPDATE=\"20240201 08:00\";" + + "\r\nTABLEID=\"table-id\";" + + "\r\nSUBJECT-AREA=\"subject-area\";" + + "\r\nCOPYRIGHT=YES;" + + "\r\nDESCRIPTION=\"lorem ipsum\";" + + "\r\nSTUB=\"Vuosi\";" + + "\r\nHEADING=\"Tiedot\";" + + "\r\nCONTVARIABLE=\"Tiedot\";" + + "\r\nVALUES(\"Vuosi\")=\"2015\",\"2016\",\"2017\",\"2018\",\"2019\",\"2020\",\"2021\",\"2022\",\"2023\",\"2024\";" + + "\r\nVALUES(\"Tiedot\")=\"foo-val-a\",\"foo-val-b\";" + + "\r\nTIMEVAL(\"Vuosi\")=TLIST(A1),\"2015\",\"2016\",\"2017\",\"2018\",\"2019\",\"2020\",\"2021\",\"2022\",\"2023\",\"2024\";" + + "\r\nCODES(\"Vuosi\")=\"2015\",\"2016\",\"2017\",\"2018\",\"2019\",\"2020\",\"2021\",\"2022\",\"2023\",\"2024\";" + + "\r\nCODES(\"Tiedot\")=\"code-foo-val-a\",\"code-foo-val-b\";" + + "\r\nVARIABLE-TYPE(\"Vuosi\")=\"Time\";" + + "\r\nVARIABLE-TYPE(\"Tiedot\")=\"Content\";" + + "\r\nPRECISION(\"Tiedot\",\"foo-val-a\")=1;" + + "\r\nPRECISION(\"Tiedot\",\"foo-val-b\")=1;" + + "\r\nLAST-UPDATED(\"Tiedot\",\"foo-val-a\")=\"20231101 08:00\";" + + "\r\nLAST-UPDATED(\"Tiedot\",\"foo-val-b\")=\"20231101 08:00\";" + + "\r\nUNITS(\"Tiedot\",\"foo-val-a\")=\"kpl\";" + + "\r\nUNITS(\"Tiedot\",\"foo-val-b\")=\"%\";" + + "\r\nVARIABLECODE(\"Tiedot\")=\"code-tiedot\";" + + "\r\nVARIABLECODE(\"Vuosi\")=\"code-vuosi\";" + + "\r\nDATA=" + + "\r\n0 1 " + + "\r\n2 3 " + + "\r\n4 5 " + + "\r\n6 7 " + + "\r\n8 9 " + + "\r\n10 11 " + + "\r\n12 13 " + + "\r\n14 15 " + + "\r\n16 17 " + + "\r\n18 19; " + + "\r\n"; + internal static string INVALID_PX_FILE = "CHARSET=ANSI;" + "\r\nAXIS-VERSION=2013;" + diff --git a/Px.Utils.UnitTests/Validation/Fixtures/SyntaxValidationFixtures.cs b/Px.Utils.UnitTests/Validation/Fixtures/SyntaxValidationFixtures.cs index acbecdad..f794205a 100644 --- a/Px.Utils.UnitTests/Validation/Fixtures/SyntaxValidationFixtures.cs +++ b/Px.Utils.UnitTests/Validation/Fixtures/SyntaxValidationFixtures.cs @@ -42,6 +42,7 @@ internal static class SyntaxValidationFixtures "\"mauris\", \"vitae\", \"ultricies\", \"leo\", \"integer\", \"malesuada\", \"nunc\", \"vel\", \"commodo\", \"viverra\",\n" + "\"maecenas\", \"accumsan\", \"lacus\", \"vel\", \"facilisis\", \"volutpat\", \"est\", \"velit\", \"egestas\", \"dui\",\n" + // Excess whitespace "\"id\", \"ornare\";\n" + + "\"ENDOFMETADATA\";\n" + // Missing value "DATA=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20;"; internal static string UTF8_N_WITH_TIMEVALS => @@ -98,7 +99,9 @@ internal static class SyntaxValidationFixtures ]; internal static List KEYVALUEPAIR_WITH_ILLEGAL_SYMBOLS_IN_SPECIFIER_SECTIONS => [ - new("foo", new KeyValuePair("FOO([\"first_specifier\"], [\"second_specifier\"])", "YES\n"), 0, [], 0) + new("foo", new KeyValuePair("FOO([\"first_specifier\"], [\"second_specifier\"])", "YES\n"), 0, [], 0), + new("foo", new KeyValuePair("FOO(\"first_specifier\",, \"second_specifier\")", "YES\n"), 1, [], 0), + new("foo", new KeyValuePair("FOO(\"first_specifier\"-\"second_specifier\")", "YES\n"), 2, [], 0) ]; private const string multilineStringValue = "\"dis parturient montes nascetur ridiculus mus\"\n" + @@ -171,7 +174,7 @@ internal static class SyntaxValidationFixtures ]; private readonly static ValidationStructuredEntryKey illegalSpecifier = new("FOO", "fi", "first\"specifier"); - internal static List STRUCTIRED_ENTRIES_WITH_ILLEGAL_CHARACTERS_IN_SPECIFIERS => [ + internal static List STRUCTIRED_ENTRIES_WITH_ILLEGAL_CHARACTERS_IN_SPECIFIER_PARTS => [ new("foo", illegalSpecifier, "foo", 0, [], 0, Utils.Validation.ValueType.StringValue) ]; diff --git a/Px.Utils.UnitTests/Validation/PxFileValidationTests/MockCustomValidators.cs b/Px.Utils.UnitTests/Validation/PxFileValidationTests/MockCustomValidators.cs index 0bba84a6..b2094366 100644 --- a/Px.Utils.UnitTests/Validation/PxFileValidationTests/MockCustomValidators.cs +++ b/Px.Utils.UnitTests/Validation/PxFileValidationTests/MockCustomValidators.cs @@ -1,14 +1,19 @@ using Px.Utils.Validation; +using Px.Utils.Validation.DatabaseValidation; +using System.Text; namespace Px.Utils.UnitTests.Validation.PxFileValidationTests { - internal sealed class MockCustomValidators : IPxFileValidator, IPxFileValidatorAsync + internal sealed class MockCustomValidator : IValidator { public ValidationResult Validate() { return new ValidationResult([]); } + } + internal sealed class MockCustomAsyncValidator : IValidatorAsync + { public async Task ValidateAsync(CancellationToken cancellationToken = default) { // Simulate async operation @@ -17,4 +22,28 @@ public async Task ValidateAsync(CancellationToken cancellation return new ValidationResult([]); } } + + internal sealed class MockCustomStreamValidator : IPxFileStreamValidator + { + public ValidationResult Validate(Stream stream, string filename, Encoding? encoding = null, IFileSystem? fileSystem = null) + { + return new ValidationResult([]); + } + } + + internal sealed class MockCustomStreamAsyncValidator : IPxFileStreamValidatorAsync + { + public async Task ValidateAsync( + Stream stream, + string filename, + Encoding? encoding = null, + IFileSystem? fileSystem = null, + CancellationToken cancellationToken = default) + { + // Simulate async operation + await Task.Delay(1, cancellationToken); + + return new ValidationResult([]); + } + } } diff --git a/Px.Utils.UnitTests/Validation/PxFileValidationTests/PxFileValidationTests.cs b/Px.Utils.UnitTests/Validation/PxFileValidationTests/PxFileValidationTests.cs index e628c0b9..d503bae3 100644 --- a/Px.Utils.UnitTests/Validation/PxFileValidationTests/PxFileValidationTests.cs +++ b/Px.Utils.UnitTests/Validation/PxFileValidationTests/PxFileValidationTests.cs @@ -2,7 +2,6 @@ using Px.Utils.UnitTests.Validation.Fixtures; using Px.Utils.UnitTests.Validation.SyntaxValidationTests; using Px.Utils.Validation; -using Px.Utils.Validation.SyntaxValidation; using System.Text; namespace Px.Utils.UnitTests.Validation.PxFileValidationTests @@ -15,14 +14,14 @@ public void ValidatePxFileWithMinimalPxFileReturnsValidResult() { // Arrange Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(PxFileFixtures.MINIMAL_PX_FILE)); - PxFileValidator validator = new (stream, "foo", Encoding.UTF8); + PxFileValidator validator = new(); // Act - ValidationResult result = validator.Validate(); + ValidationResult result = validator.Validate(stream, "foo", Encoding.UTF8); // Assert Assert.IsNotNull(result, "Validation result should not be null"); - Assert.AreEqual(0, result.FeedbackItems.Length); + Assert.AreEqual(0, result.FeedbackItems.Count); } [TestMethod] @@ -30,14 +29,14 @@ public async Task ValidatePxFileAsyncWithMinimalPxFileReturnsValidResult() { // Arrange Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(PxFileFixtures.MINIMAL_PX_FILE)); - PxFileValidator validator = new(stream, "foo", Encoding.UTF8); + PxFileValidator validator = new(); // Act - ValidationResult result = await validator.ValidateAsync(); + ValidationResult result = await validator.ValidateAsync(stream, "foo", Encoding.UTF8); // Assert Assert.IsNotNull(result, "Validation result should not be null"); - Assert.AreEqual(0, result.FeedbackItems.Length); + Assert.AreEqual(0, result.FeedbackItems.Count); } [TestMethod] @@ -45,14 +44,15 @@ public async Task ValidatePxFileWithInvalidPxFileReturnsFeedbacks() { // Arrange Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(PxFileFixtures.INVALID_PX_FILE)); - PxFileValidator validator = new(stream, "foo", Encoding.UTF8); + PxFileValidator validator = new(); // Act - ValidationResult result = await validator.ValidateAsync(); + ValidationResult result = await validator.ValidateAsync(stream, "foo", Encoding.UTF8); // Assert Assert.IsNotNull(result, "Validation result should not be null"); - Assert.AreEqual(14, result.FeedbackItems.Length); + Assert.AreEqual(9, result.FeedbackItems.Count); // Unique feedbacks + Assert.AreEqual(11, result.FeedbackItems.Values.SelectMany(f => f).Count()); // Total feedbacks including duplicates } [TestMethod] @@ -60,15 +60,45 @@ public void ValidatePxFileWithCustomValidatorsReturnsValidResult() { // Arrange Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(PxFileFixtures.MINIMAL_PX_FILE)); - PxFileValidator validator = new(stream, "foo", Encoding.UTF8); - validator.SetCustomValidators([new MockCustomValidators()]); + PxFileValidator validator = new(); + validator.SetCustomValidators([new MockCustomStreamValidator()], [new MockCustomStreamAsyncValidator()], [new MockCustomValidator()], [new MockCustomAsyncValidator()]); // Act - ValidationResult result = validator.Validate(); + ValidationResult result = validator.Validate(stream, "foo", Encoding.UTF8); // Assert Assert.IsNotNull(result, "Validation result should not be null"); - Assert.AreEqual(0, result.FeedbackItems.Length); + Assert.AreEqual(0, result.FeedbackItems.Count); + } + + [TestMethod] + public void ValidatePxFileWithoutDataReturnsError() + { + // Arrange + Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(PxFileFixtures.PX_FILE_WITHOUT_DATA)); + PxFileValidator validator = new(); + + // Act + ValidationResult result = validator.Validate(stream, "foo", Encoding.UTF8); + + // Assert + Assert.IsNotNull(result, "Validation result should not be null"); + Assert.AreEqual(1, result.FeedbackItems.Count); + } + + [TestMethod] + public async Task ValidatePxFileAsyncWithoutDataReturnsError() + { + // Arrange + Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(PxFileFixtures.PX_FILE_WITHOUT_DATA)); + PxFileValidator validator = new(); + + // Act + ValidationResult result = await validator.ValidateAsync(stream, "foo", Encoding.UTF8); + + // Assert + Assert.IsNotNull(result, "Validation result should not be null"); + Assert.AreEqual(1, result.FeedbackItems.Count); } [TestMethod] @@ -76,15 +106,15 @@ public async Task ValidatePxFileAsyncWithCustomValidatorsReturnsValidResult() { // Arrange Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(PxFileFixtures.MINIMAL_PX_FILE)); - PxFileValidator validator = new(stream, "foo", Encoding.UTF8); - validator.SetCustomValidators(null, [new MockCustomValidators()]); + PxFileValidator validator = new(); + validator.SetCustomValidators([new MockCustomStreamValidator()], [new MockCustomStreamAsyncValidator()], [new MockCustomValidator()], [new MockCustomAsyncValidator()]); // Act - ValidationResult result = await validator.ValidateAsync(); + ValidationResult result = await validator.ValidateAsync(stream, "foo", Encoding.UTF8); // Assert Assert.IsNotNull(result, "Validation result should not be null"); - Assert.AreEqual(0, result.FeedbackItems.Length); + Assert.AreEqual(0, result.FeedbackItems.Count); } [TestMethod] @@ -92,15 +122,15 @@ public void ValidatePxFileWithCustomValidationFunctionsReturnsValidResult() { // Arrange Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(PxFileFixtures.MINIMAL_PX_FILE)); - PxFileValidator validator = new(stream, "foo", Encoding.UTF8); + PxFileValidator validator = new(); validator.SetCustomValidatorFunctions(new MockCustomSyntaxValidationFunctions(), new MockCustomContentValidationFunctions()); // Act - ValidationResult result = validator.Validate(); + ValidationResult result = validator.Validate(stream, "foo", Encoding.UTF8); // Assert Assert.IsNotNull(result, "Validation result should not be null"); - Assert.AreEqual(0, result.FeedbackItems.Length); + Assert.AreEqual(0, result.FeedbackItems.Count); } } } diff --git a/Px.Utils.UnitTests/Validation/SyntaxValidationTests/MockCustomSyntaxValidationFunctions.cs b/Px.Utils.UnitTests/Validation/SyntaxValidationTests/MockCustomSyntaxValidationFunctions.cs index 119ec8b1..27872e61 100644 --- a/Px.Utils.UnitTests/Validation/SyntaxValidationTests/MockCustomSyntaxValidationFunctions.cs +++ b/Px.Utils.UnitTests/Validation/SyntaxValidationTests/MockCustomSyntaxValidationFunctions.cs @@ -6,17 +6,17 @@ namespace Px.Utils.UnitTests.Validation.SyntaxValidationTests { internal sealed class MockCustomSyntaxValidationFunctions : CustomSyntaxValidationFunctions { - internal static ValidationFeedbackItem? MockEntryValidationFunction(ValidationEntry validationEntry, PxFileSyntaxConf syntaxConf) + internal static KeyValuePair? MockEntryValidationFunction(ValidationEntry validationEntry, PxFileSyntaxConf syntaxConf) { return null; } - internal static ValidationFeedbackItem? MockKeyValuePairValidationFunction(ValidationKeyValuePair validationEntry, PxFileSyntaxConf syntaxConf) + internal static KeyValuePair? MockKeyValuePairValidationFunction(ValidationKeyValuePair validationEntry, PxFileSyntaxConf syntaxConf) { return null; } - internal static ValidationFeedbackItem? MockStructuredValidationFunction(ValidationStructuredEntry validationEntry, PxFileSyntaxConf syntaxConf) + internal static KeyValuePair? MockStructuredValidationFunction(ValidationStructuredEntry validationEntry, PxFileSyntaxConf syntaxConf) { return null; } diff --git a/Px.Utils.UnitTests/Validation/SyntaxValidationTests/StreamSyntaxValidationAsyncTests.cs b/Px.Utils.UnitTests/Validation/SyntaxValidationTests/StreamSyntaxValidationAsyncTests.cs index daf2a379..99fca822 100644 --- a/Px.Utils.UnitTests/Validation/SyntaxValidationTests/StreamSyntaxValidationAsyncTests.cs +++ b/Px.Utils.UnitTests/Validation/SyntaxValidationTests/StreamSyntaxValidationAsyncTests.cs @@ -10,7 +10,7 @@ namespace Px.Utils.UnitTests.SyntaxValidationTests public class StreamSyntaxValidationAsyncTests { private readonly string filename = "foo"; - private readonly List feedback = []; + private readonly ValidationFeedback feedback = []; [TestMethod] public async Task ValidatePxFileSyntaxAsyncCalledWithMinumalUTF8ReturnsValidResult() @@ -21,13 +21,14 @@ public async Task ValidatePxFileSyntaxAsyncCalledWithMinumalUTF8ReturnsValidResu PxFileMetadataReader reader = new(); Encoding encoding = await reader.GetEncodingAsync(stream); stream.Seek(0, SeekOrigin.Begin); - SyntaxValidator validator = new(stream, encoding, filename); + SyntaxValidator validator = new(); // Assert Assert.IsNotNull(encoding, "Encoding should not be null"); // Act - SyntaxValidationResult result = await validator.ValidateAsync(); + SyntaxValidationResult result = await validator.ValidateAsync(stream, filename, encoding); + stream.Close(); Assert.AreEqual(8, result.Result.Count); Assert.AreEqual(0, feedback.Count); } @@ -41,13 +42,14 @@ public async Task ValidatePxFileSyntaxAsyncCalledWithSpecifiersReturnsResultWith PxFileMetadataReader reader = new(); Encoding encoding = await reader.GetEncodingAsync(stream); stream.Seek(0, SeekOrigin.Begin); - SyntaxValidator validator = new(stream, encoding, filename); + SyntaxValidator validator = new(); // Assert Assert.IsNotNull(encoding, "Encoding should not be null"); // Act - SyntaxValidationResult result = await validator.ValidateAsync(); + SyntaxValidationResult result = await validator.ValidateAsync(stream, filename, encoding); + stream.Close(); Assert.AreEqual(10, result.Result.Count); Assert.AreEqual("YES", result.Result[8].Value); Assert.AreEqual("NO", result.Result[9].Value); diff --git a/Px.Utils.UnitTests/Validation/SyntaxValidationTests/StreamSyntaxValidationTests.cs b/Px.Utils.UnitTests/Validation/SyntaxValidationTests/StreamSyntaxValidationTests.cs index ef8293ee..8d3fd0e8 100644 --- a/Px.Utils.UnitTests/Validation/SyntaxValidationTests/StreamSyntaxValidationTests.cs +++ b/Px.Utils.UnitTests/Validation/SyntaxValidationTests/StreamSyntaxValidationTests.cs @@ -12,9 +12,8 @@ namespace Px.Utils.UnitTests.SyntaxValidationTests [TestClass] public class StreamSyntaxValidationTests { - private readonly string filename = "foo"; private readonly PxFileSyntaxConf syntaxConf = PxFileSyntaxConf.Default; - private List feedback = []; + private ValidationFeedback feedback = []; private MethodInfo? entryValidationMethod; private MethodInfo? kvpValidationMethod; private MethodInfo? structuredValidationMethod; @@ -42,13 +41,14 @@ public void ValidatePxFileSyntaxCalledWithMininalUtf8ReturnsValidResult() PxFileMetadataReader reader = new(); Encoding? encoding = reader.GetEncoding(stream); stream.Seek(0, SeekOrigin.Begin); - SyntaxValidator validator = new(stream, encoding, filename); + SyntaxValidator validator = new(); // Assert Assert.IsNotNull(encoding, "Encoding should not be null"); // Act - SyntaxValidationResult result = validator.Validate(); + SyntaxValidationResult result = validator.Validate(stream, "foo", encoding); + stream.Close(); Assert.AreEqual(8, result.Result.Count); Assert.AreEqual(0, feedback.Count); } @@ -61,11 +61,11 @@ public void ValidateObjectsCalledWithMultipleEntriesInSingleLineReturnsWithWarni List functions = [SyntaxValidationFunctions.MultipleEntriesOnLine]; // Act - feedback = entryValidationMethod?.Invoke(null, [entries, functions, syntaxConf]) as List ?? []; + feedback = entryValidationMethod?.Invoke(null, [entries, functions, syntaxConf]) as ValidationFeedback ?? []; - Assert.AreEqual(2, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.MultipleEntriesOnOneLine, feedback[1].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.MultipleEntriesOnOneLine, feedback[0].Feedback.Rule); + Assert.AreEqual(1, feedback.Count); + Assert.AreEqual(2, feedback.First().Value.Count); + Assert.AreEqual(ValidationFeedbackRule.MultipleEntriesOnOneLine, feedback.First().Key.Rule); } [TestMethod] @@ -77,13 +77,14 @@ public void ValidatePxFileSyntaxCalledWithWithSpecifiersSReturnsWithRightStructu PxFileMetadataReader reader = new(); Encoding? encoding = reader.GetEncoding(stream); stream.Seek(0, SeekOrigin.Begin); - SyntaxValidator validator = new(stream, encoding, filename); + SyntaxValidator validator = new(); // Assert Assert.IsNotNull(encoding, "Encoding should not be null"); // Act - SyntaxValidationResult result = validator.Validate(); + SyntaxValidationResult result = validator.Validate(stream, "foo", encoding); + stream.Close(); Assert.AreEqual(10, result.Result.Count); Assert.AreEqual("YES", result.Result[8].Value); Assert.AreEqual("NO", result.Result[9].Value); @@ -103,18 +104,23 @@ public void ValidatePxFileSyntaxCalledWithWithFeedbacksReturnsRightLineAndCharac PxFileMetadataReader reader = new(); Encoding? encoding = reader.GetEncoding(stream); stream.Seek(0, SeekOrigin.Begin); - SyntaxValidator validator = new(stream, encoding, filename); + SyntaxValidator validator = new(); + ValidationFeedbackKey keyWhiteSpaceFeedbackKey = new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.KeyContainsExcessWhiteSpace); + ValidationFeedbackKey valueWhiteSpaceFeedbackKey = new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.ExcessWhitespaceInValue); + ValidationFeedbackKey entryWithoutValueFeedbackKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.EntryWithoutValue); + ValidationFeedbackKey invalidValue = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.InvalidValueFormat); // Assert Assert.IsNotNull(encoding, "Encoding should not be null"); // Act - SyntaxValidationResult result = validator.Validate(); - Assert.AreEqual(2, result.FeedbackItems.Length); - Assert.AreEqual(9, result.FeedbackItems[0].Feedback.Line); - Assert.AreEqual(18, result.FeedbackItems[0].Feedback.Character); - Assert.AreEqual(12, result.FeedbackItems[1].Feedback.Line); - Assert.AreEqual(40, result.FeedbackItems[1].Feedback.Character); + SyntaxValidationResult result = validator.Validate(stream, "foo", encoding); + stream.Close(); + Assert.AreEqual(4, result.FeedbackItems.Count); + Assert.AreEqual(9, result.FeedbackItems[keyWhiteSpaceFeedbackKey][0].Line); + Assert.AreEqual(12, result.FeedbackItems[valueWhiteSpaceFeedbackKey][0].Line); + Assert.AreEqual(14, result.FeedbackItems[entryWithoutValueFeedbackKey][0].Line); + Assert.AreEqual(13, result.FeedbackItems[invalidValue][0].Line); } [TestMethod] @@ -125,10 +131,10 @@ public void ValidateObjectsCalledWithKvpWithMultipleLangParamsReturnsWithError() List functions = [SyntaxValidationFunctions.MoreThanOneLanguageParameter]; // Act - feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as List ?? []; + feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as ValidationFeedback ?? []; Assert.AreEqual(1, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.MoreThanOneLanguageParameterSection, feedback[0].Feedback.Rule); + Assert.AreEqual(ValidationFeedbackRule.MoreThanOneLanguageParameterSection, feedback.First().Key.Rule); } @@ -140,10 +146,10 @@ public void ValidateObjectsCalledWithKvpWithMultipleSpecifierParamsSReturnsWithE List functions = [SyntaxValidationFunctions.MoreThanOneSpecifierParameter]; // Act - feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as List ?? []; + feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as ValidationFeedback ?? []; Assert.AreEqual(1, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.MoreThanOneSpecifierParameterSection, feedback[0].Feedback.Rule); + Assert.AreEqual(ValidationFeedbackRule.MoreThanOneSpecifierParameterSection, feedback.First().Key.Rule); } [TestMethod] @@ -154,12 +160,12 @@ public void ValidateObjectsCalledWithKvpWithWrongOrderAndMissingKeywordReturnsWi List functions = [SyntaxValidationFunctions.WrongKeyOrderOrMissingKeyword]; // Act - feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as List ?? []; + feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as ValidationFeedback ?? []; - Assert.AreEqual(3, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.KeyHasWrongOrder, feedback[0].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.KeyHasWrongOrder, feedback[1].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.MissingKeyword, feedback[2].Feedback.Rule); + Assert.AreEqual(2, feedback.Count); + Assert.IsTrue(feedback.ContainsKey(new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.KeyHasWrongOrder))); + Assert.IsTrue(feedback.ContainsKey(new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.MissingKeyword))); + Assert.AreEqual(2, feedback[new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.KeyHasWrongOrder)].Count); } [TestMethod] @@ -171,15 +177,18 @@ public void ValidateObjectsCalledWithKvpWithInvalidSpecifiersReturnsWithErrors() SyntaxValidationFunctions.MoreThanTwoSpecifierParts, SyntaxValidationFunctions.SpecifierPartNotEnclosed, SyntaxValidationFunctions.NoDelimiterBetweenSpecifierParts]; + ValidationFeedbackKey missingDelimeterFeedbackKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.SpecifierDelimiterMissing); + ValidationFeedbackKey tooManySpecifiersFeedbackKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.TooManySpecifiers); + ValidationFeedbackKey notEnclosedFeedbackKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.SpecifierPartNotEnclosed); // Act - feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as List ?? []; + feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as ValidationFeedback ?? []; - Assert.AreEqual(4, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.TooManySpecifiers, feedback[0].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.SpecifierPartNotEnclosed, feedback[1].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.SpecifierPartNotEnclosed, feedback[2].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.SpecifierDelimiterMissing, feedback[3].Feedback.Rule); + Assert.AreEqual(3, feedback.Count); + Assert.IsTrue(feedback.ContainsKey(missingDelimeterFeedbackKey)); + Assert.IsTrue(feedback.ContainsKey(tooManySpecifiersFeedbackKey)); + Assert.IsTrue(feedback.ContainsKey(notEnclosedFeedbackKey)); + Assert.AreEqual(2, feedback[notEnclosedFeedbackKey].Count); } [TestMethod] @@ -190,12 +199,11 @@ public void ValidateObjectsCalledWithKvpWithIllegalSymbolsInLanguageParamReturns List functions = [SyntaxValidationFunctions.IllegalSymbolsInLanguageParamSection]; // Act - feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as List ?? []; + feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as ValidationFeedback ?? []; - Assert.AreEqual(3, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.IllegalCharactersInLanguageParameter, feedback[0].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.IllegalCharactersInLanguageParameter, feedback[1].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.IllegalCharactersInLanguageParameter, feedback[2].Feedback.Rule); + Assert.AreEqual(1, feedback.Count); + Assert.AreEqual(3, feedback.First().Value.Count); + Assert.AreEqual(ValidationFeedbackRule.IllegalCharactersInLanguageSection, feedback.First().Key.Rule); } [TestMethod] @@ -203,31 +211,29 @@ public void ValidateObjectsCalledWithKvpWithIllegalSymbolsInSpecifierParamReturn { // Arrange List keyValuePairs = SyntaxValidationFixtures.KEYVALUEPAIR_WITH_ILLEGAL_SYMBOLS_IN_SPECIFIER_SECTIONS; - List functions = [SyntaxValidationFunctions.IllegalSymbolsInSpecifierParamSection]; + List functions = [SyntaxValidationFunctions.IllegalCharactersInSpecifierSection]; // Act - feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as List ?? []; + feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as ValidationFeedback ?? []; Assert.AreEqual(1, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.IllegalCharactersInSpecifierParameter, feedback[0].Feedback.Rule); + Assert.AreEqual(3, feedback.First().Value.Count); + Assert.AreEqual(ValidationFeedbackRule.IllegalCharactersInSpecifierSection, feedback.First().Key.Rule); } [TestMethod] public void ValidateObjectsCalledWithKvpWithBadValuesReturnsErrors() { - // Arrange List keyValuePairs = SyntaxValidationFixtures.KEYVALUEPAIRS_WITH_BAD_VALUES; List functions = [SyntaxValidationFunctions.InvalidValueFormat]; // Act - feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as List ?? []; + feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as ValidationFeedback ?? []; - Assert.AreEqual(4, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.InvalidValueFormat, feedback[0].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.InvalidValueFormat, feedback[1].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.InvalidValueFormat, feedback[0].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.InvalidValueFormat, feedback[1].Feedback.Rule); + Assert.AreEqual(1, feedback.Count); + Assert.AreEqual(4, feedback.First().Value.Count); + Assert.AreEqual(ValidationFeedbackRule.InvalidValueFormat, feedback.First().Key.Rule); } [TestMethod] @@ -238,10 +244,10 @@ public void ValidateObjectsCalledWithKvpWithExcessListValueWhitespaceReturnsWith List functions = [SyntaxValidationFunctions.ExcessWhitespaceInValue]; // Act - feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as List ?? []; + feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as ValidationFeedback ?? []; Assert.AreEqual(1, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.ExcessWhitespaceInValue, feedback[0].Feedback.Rule); + Assert.AreEqual(ValidationFeedbackRule.ExcessWhitespaceInValue, feedback.First().Key.Rule); } [TestMethod] @@ -252,10 +258,10 @@ public void ValidateObjectsCalledWithKvpWithExcessKeyWhitespaceReturnsWithWarnin List functions = [SyntaxValidationFunctions.KeyContainsExcessWhiteSpace]; // Act - feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as List ?? []; + feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as ValidationFeedback ?? []; Assert.AreEqual(1, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.KeyContainsExcessWhiteSpace, feedback[0].Feedback.Rule); + Assert.AreEqual(ValidationFeedbackRule.KeyContainsExcessWhiteSpace, feedback.First().Key.Rule); } [TestMethod] @@ -266,11 +272,11 @@ public void ValidateObjectsCalledWithKvpWithShortMultilineValueReturnsWithWarnin List functions = [SyntaxValidationFunctions.ExcessNewLinesInValue]; // Act - feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as List ?? []; + feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as ValidationFeedback ?? []; - Assert.AreEqual(2, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.ExcessNewLinesInValue, feedback[0].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.ExcessNewLinesInValue, feedback[1].Feedback.Rule); + Assert.AreEqual(1, feedback.Count); + Assert.AreEqual(2, feedback.First().Value.Count); + Assert.AreEqual(ValidationFeedbackRule.ExcessNewLinesInValue, feedback.First().Key.Rule); } [TestMethod] @@ -279,14 +285,16 @@ public void ValidateObjectsCalledWithStructuredEntriesWithInvalidKeywordsReturns // Arrange List structuredEntries = SyntaxValidationFixtures.STRUCTURED_ENTRIES_WITH_INVALID_KEYWORDS; List functions = [SyntaxValidationFunctions.KeywordDoesntStartWithALetter, SyntaxValidationFunctions.KeywordContainsIllegalCharacters]; + ValidationFeedbackKey startWithletterFeedbackKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.KeywordDoesntStartWithALetter); + ValidationFeedbackKey illegalCharactersFeedbackKey = new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.IllegalCharactersInKeyword); // Act - feedback = structuredValidationMethod?.Invoke(null, [structuredEntries, functions, syntaxConf]) as List ?? []; + feedback = structuredValidationMethod?.Invoke(null, [structuredEntries, functions, syntaxConf]) as ValidationFeedback ?? []; - Assert.AreEqual(3, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.KeywordDoesntStartWithALetter, feedback[0].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.IllegalCharactersInKeyword, feedback[1].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.IllegalCharactersInKeyword, feedback[2].Feedback.Rule); + Assert.AreEqual(2, feedback.Count); + Assert.IsTrue(feedback.ContainsKey(startWithletterFeedbackKey)); + Assert.IsTrue(feedback.ContainsKey(illegalCharactersFeedbackKey)); + Assert.AreEqual(2, feedback[illegalCharactersFeedbackKey].Count); } [TestMethod] @@ -297,7 +305,7 @@ public void ValidateObjectsCalledWithStructuredEntriesWithValidLanguagesReturnsW List functions = [SyntaxValidationFunctions.IllegalCharactersInLanguageParameter]; // Act - feedback = structuredValidationMethod?.Invoke(null, [structuredEntries, functions, syntaxConf]) as List ?? []; + feedback = structuredValidationMethod?.Invoke(null, [structuredEntries, functions, syntaxConf]) as ValidationFeedback ?? []; Assert.AreEqual(0, feedback.Count); } @@ -310,24 +318,24 @@ public void ValidateObjectsCalledWithStructuredEntriesWithInvalidLanguagesReturn List functions = [SyntaxValidationFunctions.IllegalCharactersInLanguageParameter]; // Act - feedback = structuredValidationMethod?.Invoke(null, [structuredEntries, functions, syntaxConf]) as List ?? []; + feedback = structuredValidationMethod?.Invoke(null, [structuredEntries, functions, syntaxConf]) as ValidationFeedback ?? []; Assert.AreEqual(1, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.IllegalCharactersInLanguageParameter, feedback[0].Feedback.Rule); + Assert.AreEqual(ValidationFeedbackRule.IllegalCharactersInLanguageSection, feedback.First().Key.Rule); } [TestMethod] public void ValidateObjectsCalledWithStructuredEntriesWithIllegalCharactersInSpecifiersReturnsWithErrors() { // Arrange - List structuredEntries = SyntaxValidationFixtures.STRUCTIRED_ENTRIES_WITH_ILLEGAL_CHARACTERS_IN_SPECIFIERS; + List structuredEntries = SyntaxValidationFixtures.STRUCTIRED_ENTRIES_WITH_ILLEGAL_CHARACTERS_IN_SPECIFIER_PARTS; List functions = [SyntaxValidationFunctions.IllegalCharactersInSpecifierParts]; // Act - feedback = structuredValidationMethod?.Invoke(null, [structuredEntries, functions, syntaxConf]) as List ?? []; + feedback = structuredValidationMethod?.Invoke(null, [structuredEntries, functions, syntaxConf]) as ValidationFeedback ?? []; Assert.AreEqual(1, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.IllegalCharactersInSpecifierPart, feedback[0].Feedback.Rule); + Assert.AreEqual(ValidationFeedbackRule.IllegalCharactersInSpecifierPart, feedback.First().Key.Rule); } [TestMethod] @@ -338,10 +346,10 @@ public void ValidateObjectsCalledWithEntryWithoutValueReturnsWithError() List functions = [SyntaxValidationFunctions.EntryWithoutValue]; // Act - feedback = entryValidationMethod?.Invoke(null, [entries, functions, syntaxConf]) as List ?? []; + feedback = entryValidationMethod?.Invoke(null, [entries, functions, syntaxConf]) as ValidationFeedback ?? []; Assert.AreEqual(1, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.EntryWithoutValue, feedback[0].Feedback.Rule); + Assert.AreEqual(ValidationFeedbackRule.EntryWithoutValue, feedback.First().Key.Rule); } [TestMethod] @@ -352,11 +360,11 @@ public void ValidateObjectsCalledWithStructuredEntriesWithIncompliantLanguagesRe List functions = [SyntaxValidationFunctions.IncompliantLanguage]; // Act - feedback = structuredValidationMethod?.Invoke(null, [structuredEntries, functions, syntaxConf] ) as List ?? []; + feedback = structuredValidationMethod?.Invoke(null, [structuredEntries, functions, syntaxConf] ) as ValidationFeedback ?? []; - Assert.AreEqual(2, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.IncompliantLanguage, feedback[0].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.IncompliantLanguage, feedback[1].Feedback.Rule); + Assert.AreEqual(1, feedback.Count); + Assert.AreEqual(2, feedback.First().Value.Count); + Assert.AreEqual(ValidationFeedbackRule.IncompliantLanguage, feedback.First().Key.Rule); } [TestMethod] @@ -367,11 +375,11 @@ public void ValidateObjectsCalledWithStructuredEntriesWithUnrecommendedKeywordNa List functions = [SyntaxValidationFunctions.KeywordContainsUnderscore, SyntaxValidationFunctions.KeywordIsNotInUpperCase]; // Act - feedback = structuredValidationMethod?.Invoke(null, [structuredEntries, functions, syntaxConf] ) as List ?? []; + feedback = structuredValidationMethod?.Invoke(null, [structuredEntries, functions, syntaxConf] ) as ValidationFeedback ?? []; Assert.AreEqual(2, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.KeywordIsNotInUpperCase, feedback[0].Feedback.Rule); - Assert.AreEqual(ValidationFeedbackRule.KeywordContainsUnderscore, feedback[1].Feedback.Rule); + Assert.IsTrue(feedback.ContainsKey(new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.KeywordIsNotInUpperCase))); + Assert.IsTrue(feedback.ContainsKey(new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.KeywordContainsUnderscore))); } [TestMethod] @@ -382,10 +390,31 @@ public void ValidateObjectsCalledWithLongKeywordReturnsWithWarnings() List functions = [SyntaxValidationFunctions.KeywordIsExcessivelyLong]; // Act - feedback = structuredValidationMethod?.Invoke(null, [structuredEntries, functions, syntaxConf]) as List ?? []; + feedback = structuredValidationMethod?.Invoke(null, [structuredEntries, functions, syntaxConf]) as ValidationFeedback ?? []; Assert.AreEqual(1, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.KeywordExcessivelyLong, feedback[0].Feedback.Rule); + Assert.AreEqual(ValidationFeedbackRule.KeywordExcessivelyLong, feedback.First().Key.Rule); + } + + [TestMethod] + [DataRow("\"Item1\", \"Item2\", \"Item3\"")] + [DataRow("\"Item 1\", \"Item 2\", \"Item 3\"")] + [DataRow("\"Item1\",\"Item2\",\"Item3\"")] + [DataRow("\"Item, 1\", \"Item, 2\", \"Item, 3\"")] + [DataRow("\"Item1\",\"Item2\"")] + public void GetValueTypeFromStringCalledWithValidListsValuesCorrectValueType(string list) + { + // Arrange + List keyValuePairs = [new("foo", new KeyValuePair("foo-key", list), 0, [], 0)]; + List functions = [SyntaxValidationFunctions.InvalidValueFormat]; + + // Act + feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as ValidationFeedback ?? []; + Utils.Validation.ValueType? valueType = getValueTypeFromStringMethod?.Invoke(null, [keyValuePairs.First().KeyValuePair.Value, PxFileSyntaxConf.Default]) as Utils.Validation.ValueType?; + + // Assert + Assert.AreEqual(Utils.Validation.ValueType.ListOfStrings, valueType); + Assert.AreEqual(0, feedback.Count); } [TestMethod] @@ -410,8 +439,8 @@ public void CorrectlyDefinedRangeAndSeriesTimeValuesReturnCorrectValueType(strin List functions = [SyntaxValidationFunctions.InvalidValueFormat]; // Act - feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as List ?? []; - Utils.Validation.ValueType? valueType = getValueTypeFromStringMethod?.Invoke(null, [keyValuePairs[0].KeyValuePair.Value, PxFileSyntaxConf.Default]) as Utils.Validation.ValueType?; + feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as ValidationFeedback ?? []; + Utils.Validation.ValueType? valueType = getValueTypeFromStringMethod?.Invoke(null, [keyValuePairs.First().KeyValuePair.Value, PxFileSyntaxConf.Default]) as Utils.Validation.ValueType?; // Assert Assert.AreEqual(0, feedback.Count); @@ -440,14 +469,14 @@ public void IncorrectlyDefinedRangeAndSeriesTimeValuesReturnWithErrors(string ti List functions = [SyntaxValidationFunctions.InvalidValueFormat]; // Act - feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as List ?? []; - Utils.Validation.ValueType? valueType = getValueTypeFromStringMethod?.Invoke(null, [keyValuePairs[0].KeyValuePair.Value, PxFileSyntaxConf.Default]) as Utils.Validation.ValueType?; + feedback = kvpValidationMethod?.Invoke(null, [keyValuePairs, functions, syntaxConf]) as ValidationFeedback ?? []; + Utils.Validation.ValueType? valueType = getValueTypeFromStringMethod?.Invoke(null, [keyValuePairs.First().KeyValuePair.Value, PxFileSyntaxConf.Default]) as Utils.Validation.ValueType?; // Assert if (type is null) { Assert.AreEqual(1, feedback.Count); - Assert.AreEqual(ValidationFeedbackRule.InvalidValueFormat, feedback[0].Feedback.Rule); + Assert.AreEqual(ValidationFeedbackRule.InvalidValueFormat, feedback.First().Key.Rule); } Assert.AreEqual(type, valueType); } @@ -461,14 +490,15 @@ public void ValidatePxFileSyntaxCalledWithTimevalsSRetunrsWithValidResult() PxFileMetadataReader reader = new(); Encoding? encoding = reader.GetEncoding(stream); stream.Seek(0, SeekOrigin.Begin); - SyntaxValidator validator = new(stream, encoding, filename); + SyntaxValidator validator = new(); // Assert Assert.IsNotNull(encoding, "Encoding should not be null"); // Act - SyntaxValidationResult result = validator.Validate(); - Assert.AreEqual(0, result.FeedbackItems.Length); + SyntaxValidationResult result = validator.Validate(stream, "foo", encoding); + stream.Close(); + Assert.AreEqual(0, result.FeedbackItems.Count); } [TestMethod] @@ -480,20 +510,21 @@ public void ValidatePxFileSyntaxCalledWithBadTimevalsReturnsErrors() PxFileMetadataReader reader = new(); Encoding? encoding = reader.GetEncoding(stream); stream.Seek(0, SeekOrigin.Begin); - SyntaxValidator validator = new(stream, encoding, filename); + SyntaxValidator validator = new(); // Assert Assert.IsNotNull(encoding, "Encoding should not be null"); // Act - SyntaxValidationResult result = validator.Validate(); - Assert.AreEqual(2, result.FeedbackItems.Length); - Assert.AreEqual(9, result.FeedbackItems[0].Feedback.Line); - Assert.AreEqual(16, result.FeedbackItems[0].Feedback.Character); - Assert.AreEqual(ValidationFeedbackRule.InvalidValueFormat, result.FeedbackItems[0].Feedback.Rule); - Assert.AreEqual(10, result.FeedbackItems[1].Feedback.Line); - Assert.AreEqual(16, result.FeedbackItems[1].Feedback.Character); - Assert.AreEqual(ValidationFeedbackRule.InvalidValueFormat, result.FeedbackItems[1].Feedback.Rule); + SyntaxValidationResult result = validator.Validate(stream, "foo", encoding); + stream.Close(); + Assert.AreEqual(1, result.FeedbackItems.Count); + Assert.AreEqual(9, result.FeedbackItems.First().Value[0].Line); + Assert.AreEqual(16, result.FeedbackItems.First().Value[0].Character); + Assert.AreEqual(ValidationFeedbackRule.InvalidValueFormat, result.FeedbackItems.First().Key.Rule); + Assert.AreEqual(10, result.FeedbackItems.First().Value[1].Line); + Assert.AreEqual(16, result.FeedbackItems.First().Value[1].Character); + Assert.AreEqual(ValidationFeedbackRule.InvalidValueFormat, result.FeedbackItems.First().Key.Rule); } [TestMethod] @@ -507,11 +538,12 @@ public void TestWithCustomSyntaxValidationFunctions() stream.Seek(0, SeekOrigin.Begin); // Act - SyntaxValidator validator = new(stream, encoding, filename, PxFileSyntaxConf.Default, new MockCustomSyntaxValidationFunctions()); - validator.Validate(); + SyntaxValidator validator = new(customValidationFunctions: new MockCustomSyntaxValidationFunctions()); + SyntaxValidationResult result = validator.Validate(stream, "foo", encoding); + stream.Close(); // Assert - Assert.AreEqual(0, feedback.Count); + Assert.AreEqual(0, result.FeedbackItems.Count); } [TestMethod] @@ -525,11 +557,12 @@ public async Task TestWithCustomSyntaxValidationFunctionsAsync() stream.Seek(0, SeekOrigin.Begin); // Act - SyntaxValidator validator = new(stream, encoding, filename, PxFileSyntaxConf.Default, new MockCustomSyntaxValidationFunctions()); - await validator.ValidateAsync(); + SyntaxValidator validator = new(customValidationFunctions: new MockCustomSyntaxValidationFunctions()); + SyntaxValidationResult result = await validator.ValidateAsync(stream, "foo", encoding); + stream.Close(); // Assert - Assert.AreEqual(0, feedback.Count); + Assert.AreEqual(0, result.FeedbackItems.Count); } } } diff --git a/Px.Utils/PxFile/Metadata/PxFileMetadataReader.cs b/Px.Utils/PxFile/Metadata/PxFileMetadataReader.cs index de164703..f87b1788 100644 --- a/Px.Utils/PxFile/Metadata/PxFileMetadataReader.cs +++ b/Px.Utils/PxFile/Metadata/PxFileMetadataReader.cs @@ -226,7 +226,7 @@ public Encoding GetEncoding(Stream stream, PxFileSyntaxConf? syntaxConf = null) stream.Position = position; - if (GetBom(bom) is Encoding utf) return utf; + if (GetEncodingFromBOM(bom) is Encoding utf) return utf; // Use ASCII because encoding is still unknown, CODEPAGE keyword is readable as ASCII KeyValuePair encoding = ReadMetadata(stream, Encoding.ASCII, syntaxConf, 512) @@ -255,7 +255,7 @@ public async Task GetEncodingAsync(Stream stream, PxFileSyntaxConf? sy stream.Position = position; - if (GetBom(bom) is Encoding utf) return utf; + if (GetEncodingFromBOM(bom) is Encoding utf) return utf; // Use ASCII because encoding is still unknown, CODEPAGE keyword is readable as ASCII KeyValuePair encoding = await ReadMetadataAsync(stream, Encoding.ASCII, syntaxConf, 512, cancellationToken) @@ -264,26 +264,13 @@ public async Task GetEncodingAsync(Stream stream, PxFileSyntaxConf? sy return GetEncodingFromValue(encoding.Value, syntaxConf); } - #region Private Methods - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Append(char[] buffer, int start, int endIndex, bool keyWordMode, StringBuilder keyWordBldr, StringBuilder valueStringBldr) - { - if (endIndex - start > 0) - { - if (keyWordMode) - { - keyWordBldr.Append(buffer, start, endIndex - start); - } - else - { - valueStringBldr.Append(buffer, start, endIndex - start); - } - } - } - + /// + /// Returns encoding based on the Byte Order Mark + /// + /// Array of bytes to determine the BOM from. + /// Encoding if one is found based on the BOM or null if one is not found. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Encoding? GetBom(byte[] buffer) + public static Encoding? GetEncodingFromBOM(byte[] buffer) { if (buffer.Take(CharacterConstants.BOMUTF8.Length).SequenceEqual(CharacterConstants.BOMUTF8)) { @@ -301,6 +288,25 @@ private static void Append(char[] buffer, int start, int endIndex, bool keyWordM return null; } + + #region Private Methods + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Append(char[] buffer, int start, int endIndex, bool keyWordMode, StringBuilder keyWordBldr, StringBuilder valueStringBldr) + { + if (endIndex - start > 0) + { + if (keyWordMode) + { + keyWordBldr.Append(buffer, start, endIndex - start); + } + else + { + valueStringBldr.Append(buffer, start, endIndex - start); + } + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Encoding GetEncodingFromValue(string value, PxFileSyntaxConf syntaxConf) { diff --git a/Px.Utils/PxFile/PxFileSyntaxConf.cs b/Px.Utils/PxFile/PxFileSyntaxConf.cs index 8ffb8d50..06870dad 100644 --- a/Px.Utils/PxFile/PxFileSyntaxConf.cs +++ b/Px.Utils/PxFile/PxFileSyntaxConf.cs @@ -215,6 +215,17 @@ public class DataValueTokens public static DataValueTokens DefaultDataValueTokens=> new(); } + public class DatabaseTokens + { + private const string INDEX = "_INDEX"; + private const char LANGUAGE_SEPARATOR = '_'; + + public string Index { get; } = INDEX; + public char LanguageSeparator { get; } = LANGUAGE_SEPARATOR; + + public static DatabaseTokens DefaultDatabaseTokens => new(); + } + public TimeValue Time { get; set; } public KeyWordTokens KeyWords { get; set; } @@ -223,6 +234,7 @@ public class DataValueTokens public BooleanTokens Booleans { get; set; } public DataValueTokens DataValues { get; set; } + public DatabaseTokens Database { get; set; } private TokenDefinitions() { @@ -231,6 +243,7 @@ private TokenDefinitions() VariableTypes = VariableTypeTokens.DefaultVariableTypeTokens; Booleans = BooleanTokens.DefaultBooleanTokens; DataValues = DataValueTokens.DefaultDataValueTokens; + Database = DatabaseTokens.DefaultDatabaseTokens; } public static TokenDefinitions DefaultTokens => new(); diff --git a/Px.Utils/Validation/ContentValidation/ContentValidationResult.cs b/Px.Utils/Validation/ContentValidation/ContentValidationResult.cs index 6939f7c1..9dbf3b99 100644 --- a/Px.Utils/Validation/ContentValidation/ContentValidationResult.cs +++ b/Px.Utils/Validation/ContentValidation/ContentValidationResult.cs @@ -4,10 +4,10 @@ namespace Px.Utils.Validation.ContentValidation /// /// Represents the result of a px file metadata content validation operation. Contains a validation report and information about the expected dimensions of the data section. /// - /// An array of objects gathered during the metadata content validation process. + /// A object containing information about rule violations gathered during the metadata content validation process. /// Expected length of each data row. /// Expected data column length/amount of data rows. - public sealed class ContentValidationResult(ValidationFeedbackItem[] feedbackItems, int dataRowLength, int dataRowAmount) : ValidationResult(feedbackItems) + public sealed class ContentValidationResult(ValidationFeedback feedbackItems, int dataRowLength, int dataRowAmount) : ValidationResult(feedbackItems) { public int DataRowLength { get; } = dataRowLength; public int DataRowAmount { get; } = dataRowAmount; diff --git a/Px.Utils/Validation/ContentValidation/ContentValidator.UtilityMethods.cs b/Px.Utils/Validation/ContentValidation/ContentValidator.UtilityMethods.cs index abc225ea..4f364c11 100644 --- a/Px.Utils/Validation/ContentValidation/ContentValidator.UtilityMethods.cs +++ b/Px.Utils/Validation/ContentValidation/ContentValidator.UtilityMethods.cs @@ -21,7 +21,7 @@ private static Dictionary, string[]> FindDimensionV ValidationStructuredEntry[] entries, PxFileSyntaxConf syntaxConf, Dictionary? dimensions, - ref List feedbackItems, + ref ValidationFeedback feedbackItems, string filename) { if (dimensions is null) @@ -40,31 +40,25 @@ private static Dictionary, string[]> FindDimensionV e.Key.FirstSpecifier.Equals(dimension, StringComparison.Ordinal)); if (valuesEntry is not null) { - string[] values = valuesEntry.Value.Split(syntaxConf.Symbols.Value.ListSeparator); - for(int i = 0; i < values.Length; i++) - { - values[i] = SyntaxValidationUtilityMethods.CleanString(values[i], syntaxConf); - } + List values = SyntaxValidationUtilityMethods.GetListItemsFromString( + valuesEntry.Value, + syntaxConf.Symbols.Value.ListSeparator, + syntaxConf.Symbols.Value.StringDelimeter); variableValues.Add( new KeyValuePair ( language, dimension ), - values + [.. values.Select(v => SyntaxValidationUtilityMethods.CleanString(v, syntaxConf))] ); } else { - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.VariableValuesMissing, - 0, - 0, - $"{dimension}, {language}" - ); + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.VariableValuesMissing), + new(filename, additionalInfo: $"{dimension}, {language}") + ); - feedbackItems.Add(new ValidationFeedbackItem( - new ValidationObject(filename, 0, []), - feedback - )); + feedbackItems.Add(feedback); } } } @@ -80,9 +74,8 @@ private static Dictionary, string[]> FindDimensionV /// Name of the content dimension value requiring the entry /// Object that stores required information about the validation process /// Optional that indicates if the keyword entry is recommended and should yield a warning if not found - /// Returns a object with an error if required entry is not found - /// or with a warning if the entry specifiers are defined in an unexpected way - private static ValidationFeedbackItem? FindContentVariableKey( + /// Returns a key value pair containing information about the validation violation if required or recommended key is not found. + private static KeyValuePair? FindContentDimensionKey( ValidationStructuredEntry[] entries, string keyword, KeyValuePair languageAndDimensionPair, @@ -101,18 +94,11 @@ private static Dictionary, string[]> FindDimensionV if (entry is null) { - ValidationFeedback feedback = new ( - recommended ? ValidationFeedbackLevel.Warning : ValidationFeedbackLevel.Error, - recommended ? ValidationFeedbackRule.RecommendedKeyMissing : ValidationFeedbackRule.RequiredKeyMissing, - 0, - 0, - $"{keyword}, {language}, {dimensionName}, {dimensionValueName}" - ); - return - new ValidationFeedbackItem( - new ValidationObject(validator._filename, 0, []), - feedback - ); + return new KeyValuePair ( + new(recommended ? ValidationFeedbackLevel.Warning : ValidationFeedbackLevel.Error, + recommended ? ValidationFeedbackRule.RecommendedKeyMissing : ValidationFeedbackRule.RequiredKeyMissing), + new(validator._filename, additionalInfo: $"{keyword}, {language}, {dimensionName}, {dimensionValueName}") + ); } else if (entry.Key.FirstSpecifier is null || entry.Key.SecondSpecifier is null) { @@ -121,19 +107,11 @@ private static Dictionary, string[]> FindDimensionV 0, entry.LineChangeIndexes); - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.UnrecommendedSpecifierDefinitionFound, - feedbackIndexes.Key, - 0, - $"{keyword}, {language}, {dimensionName}, {dimensionValueName}" - ); - - return - new ValidationFeedbackItem( - entry, - feedback - ); + return new( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.RecommendedSpecifierDefinitionMissing), + new(validator._filename, feedbackIndexes.Key, feedbackIndexes.Value, $"{keyword}, {language}, {dimensionName}, {dimensionValueName}") + ); } return null; @@ -147,8 +125,8 @@ private static Dictionary, string[]> FindDimensionV /// Language that the function searches the entry for /// Object that stores required information about the validation process /// Name of the dimension - /// Returns a object with a warning if the recommended entry is not found - private static ValidationFeedbackItem? FindDimensionRecommendedKey( + /// Returns a key value pair containing information about a validation violation warning if the recommended entry is not found + private static KeyValuePair? FindDimensionRecommendedKey( ValidationStructuredEntry[] entries, string keyword, string language, ContentValidator validator, @@ -161,17 +139,11 @@ private static Dictionary, string[]> FindDimensionV if (entry is null) { - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.RecommendedKeyMissing, - 0, - 0, - $"{language}, {keyword}, {dimensionName}"); - - return new ValidationFeedbackItem( - new ValidationObject(validator._filename, 0, []), - feedback - ); + return new( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.RecommendedKeyMissing), + new(validator._filename, additionalInfo: $"{keyword}, {language}, {dimensionName}") + ); } return null; @@ -186,8 +158,8 @@ private static Dictionary, string[]> FindDimensionV /// Object that provides information of the ongoing content validation process /// KeyValuePair that contains the processed language as key and the dimension name as value /// Name of the content dimension value to look entries for - /// Returns a list of objects if required entries are not found - private static List ProcessContentDimensionValue( + /// Returns a object containing information of missing required entries + private static ValidationFeedback ProcessContentDimensionValue( string[] languageSpecificKeywords, string[] commonKeywords, string[] recommendedKeywords, @@ -196,13 +168,13 @@ private static List ProcessContentDimensionValue( KeyValuePair languageAndDimensionPair, string valueName) { - List feedbackItems = []; + ValidationFeedback feedbackItems = []; foreach (string keyword in languageSpecificKeywords) { - ValidationFeedbackItem? issue = FindContentVariableKey(entries, keyword, languageAndDimensionPair, valueName, validator); + KeyValuePair? issue = FindContentDimensionKey(entries, keyword, languageAndDimensionPair, valueName, validator); if (issue is not null) { - feedbackItems.Add((ValidationFeedbackItem)issue); + feedbackItems.Add((KeyValuePair)issue); } } @@ -210,18 +182,18 @@ private static List ProcessContentDimensionValue( { foreach (string keyword in commonKeywords) { - ValidationFeedbackItem? issue = FindContentVariableKey(entries, keyword, languageAndDimensionPair, valueName, validator); + KeyValuePair ? issue = FindContentDimensionKey(entries, keyword, languageAndDimensionPair, valueName, validator); if (issue is not null) { - feedbackItems.Add((ValidationFeedbackItem)issue); + feedbackItems.Add((KeyValuePair)issue); } } foreach (string keyword in recommendedKeywords) { - ValidationFeedbackItem? issue = FindContentVariableKey(entries, keyword, languageAndDimensionPair, valueName, validator, true); + KeyValuePair? issue = FindContentDimensionKey(entries, keyword, languageAndDimensionPair, valueName, validator, true); if (issue is not null) { - feedbackItems.Add((ValidationFeedbackItem)issue); + feedbackItems.Add((KeyValuePair)issue); } } } @@ -238,8 +210,8 @@ private static List ProcessContentDimensionValue( /// Language assigned to the currently processed dimension /// Object that provides information of the ongoing content validation process /// Name of the dimension currently being processed - /// - private static List ProcessDimension( + /// Returns a object containing information of missing dimension entries + private static ValidationFeedback ProcessDimension( string[] keywords, string dimensionTypeKeyword, ValidationStructuredEntry[] entries, @@ -247,25 +219,40 @@ private static List ProcessDimension( ContentValidator validator, string dimensionName) { - List feedbackItems = []; + ValidationFeedback feedbackItems = []; foreach (string keyword in keywords) { - ValidationFeedbackItem? keywordFeedback = FindDimensionRecommendedKey(entries, keyword, language, validator, dimensionName); + KeyValuePair? keywordFeedback = FindDimensionRecommendedKey(entries, keyword, language, validator, dimensionName); if (keywordFeedback is not null) { - feedbackItems.Add((ValidationFeedbackItem)keywordFeedback); + feedbackItems.Add((KeyValuePair)keywordFeedback); } } if (language == validator._defaultLanguage) { - ValidationFeedbackItem? variableTypeFeedback = FindDimensionRecommendedKey(entries, dimensionTypeKeyword, language, validator, dimensionName); + KeyValuePair? variableTypeFeedback = FindDimensionRecommendedKey(entries, dimensionTypeKeyword, language, validator, dimensionName); if (variableTypeFeedback is not null) { - feedbackItems.Add((ValidationFeedbackItem)variableTypeFeedback); + feedbackItems.Add((KeyValuePair)variableTypeFeedback); } } return feedbackItems; } + + private static Dictionary GetDimensionNames( + ValidationStructuredEntry[] entries, + string defaultLanguage, + PxFileSyntaxConf syntaxConf) + { + Dictionary dimensionNames = []; + foreach (ValidationStructuredEntry entry in entries) + { + string language = entry.Key.Language ?? defaultLanguage; + List names = SyntaxValidationUtilityMethods.GetListItemsFromString(entry.Value, syntaxConf.Symbols.Value.ListSeparator, syntaxConf.Symbols.Value.StringDelimeter); + dimensionNames.Add(language, [.. names.Select(n => SyntaxValidationUtilityMethods.CleanString(n, syntaxConf))]); + } + return dimensionNames; + } } } diff --git a/Px.Utils/Validation/ContentValidation/ContentValidator.ValidationEntryFunctions.cs b/Px.Utils/Validation/ContentValidation/ContentValidator.ValidationEntryFunctions.cs new file mode 100644 index 00000000..df69dcb8 --- /dev/null +++ b/Px.Utils/Validation/ContentValidation/ContentValidator.ValidationEntryFunctions.cs @@ -0,0 +1,467 @@ +using Px.Utils.Validation.SyntaxValidation; +using System.Globalization; + +namespace Px.Utils.Validation.ContentValidation +{ + public delegate ValidationFeedback? ContentValidationEntryValidator(ValidationStructuredEntry entry, ContentValidator validator); + + /// + /// Collection of functions for validating Px file metadata contents by processing individual entries + /// + public sealed partial class ContentValidator + { + public List DefaultContentValidationEntryFunctions { get; } = [ + ValidateUnexpectedSpecifiers, + ValidateUnexpectedLanguageParams, + ValidateLanguageParams, + ValidateSpecifiers, + ValidateValueTypes, + ValidateValueContents, + ValidateValueAmounts, + ValidateValueUppercaseRecommendations + ]; + + /// + /// Validates that given entry does not contain specifiers if it is not allowed to have them based on keyword. + /// + /// Px file metadata entries in an array of objects + /// object that stores information that is gathered during the validation process + /// Key value pair containing information about the rule violation is returned if an unexpected specifier is detected. + public static ValidationFeedback? ValidateUnexpectedSpecifiers(ValidationStructuredEntry entry, ContentValidator validator) + { + string[] noSpecifierAllowedKeywords = + [ + validator.SyntaxConf.Tokens.KeyWords.DefaultLanguage, + validator.SyntaxConf.Tokens.KeyWords.AvailableLanguages, + validator.SyntaxConf.Tokens.KeyWords.Charset, + validator.SyntaxConf.Tokens.KeyWords.CodePage, + validator.SyntaxConf.Tokens.KeyWords.StubDimensions, + validator.SyntaxConf.Tokens.KeyWords.HeadingDimensions, + validator.SyntaxConf.Tokens.KeyWords.TableId, + validator.SyntaxConf.Tokens.KeyWords.Description, + validator.SyntaxConf.Tokens.KeyWords.ContentVariableIdentifier, + ]; + + if (!noSpecifierAllowedKeywords.Contains(entry.Key.Keyword)) + { + return null; + } + + if (entry.Key.FirstSpecifier is not null || entry.Key.SecondSpecifier is not null) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + entry.KeyStartLineIndex, + 0, + entry.LineChangeIndexes); + + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.IllegalSpecifierDefinitionFound), + new(validator._filename, + feedbackIndexes.Key, + 0, + $"{entry.Key.Keyword}: {entry.Key.FirstSpecifier}, {entry.Key.SecondSpecifier}") + ); + + return new(feedback); + } + + return null; + } + + /// + /// Finds unexpected language parameters in the Px file metadata entry + /// + /// Entry in the Px file metadata. Represented by a object + /// object that stores information that is gathered during the validation process + /// Key value pair containing information about the rule violation is returned if an illegal or unrecommended language parameter is detected in the entry + public static ValidationFeedback? ValidateUnexpectedLanguageParams(ValidationStructuredEntry entry, ContentValidator validator) + { + string[] noLanguageParameterAllowedKeywords = [ + validator.SyntaxConf.Tokens.KeyWords.Charset, + validator.SyntaxConf.Tokens.KeyWords.CodePage, + validator.SyntaxConf.Tokens.KeyWords.TableId, + validator.SyntaxConf.Tokens.KeyWords.DefaultLanguage, + validator.SyntaxConf.Tokens.KeyWords.AvailableLanguages, + ]; + + string[] noLanguageParameterRecommendedKeywords = [ + validator.SyntaxConf.Tokens.KeyWords.LastUpdated, + validator.SyntaxConf.Tokens.KeyWords.DimensionType, + validator.SyntaxConf.Tokens.KeyWords.Precision + ]; + + if (!noLanguageParameterAllowedKeywords.Contains(entry.Key.Keyword) && !noLanguageParameterRecommendedKeywords.Contains(entry.Key.Keyword)) + { + return null; + } + + if (entry.Key.Language is not null) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + entry.KeyStartLineIndex, + 0, + entry.LineChangeIndexes); + + if (noLanguageParameterAllowedKeywords.Contains(entry.Key.Keyword)) + { + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.IllegalLanguageDefinitionFound), + new(validator._filename, + feedbackIndexes.Key, + 0, + $"{entry.Key.Keyword}, {entry.Key.Language}, {entry.Key.FirstSpecifier}, {entry.Key.SecondSpecifier}") + ); + + return new(feedback); + } + else if (noLanguageParameterRecommendedKeywords.Contains(entry.Key.Keyword)) + { + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.UnrecommendedLanguageDefinitionFound), + new(validator._filename, + feedbackIndexes.Key, + 0, + $"{entry.Key.Keyword}, {entry.Key.Language}, {entry.Key.FirstSpecifier}, {entry.Key.SecondSpecifier}") + ); + + return new(feedback); + } + } + + return null; + } + + /// + /// Validates that the language defined in the Px file metadata entry is defined in the available languages entry + /// + /// Entry in the Px file metadata. Represented by a object + /// object that stores information that is gathered during the validation process + /// Key value pair containing information about the rule violation is returned if an undefined language is found from the entry + public static ValidationFeedback? ValidateLanguageParams(ValidationStructuredEntry entry, ContentValidator validator) + { + if (entry.Key.Language is null) + { + return null; + } + + if (validator._availableLanguages is not null && !validator._availableLanguages.Contains(entry.Key.Language)) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + entry.KeyStartLineIndex, + 0, + entry.LineChangeIndexes); + + + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.UndefinedLanguageFound), + new(validator._filename, + feedbackIndexes.Key, + 0, + entry.Key.Language) + ); + + return new(feedback); + } + + return null; + } + + /// + /// Validates that specifiers are defined properly in the Px file metadata entry. + /// Content dimension specifiers are allowed to be defined using only the first specifier at value level and are checked separately + /// + /// Entry in the Px file metadata. Represented by a object + /// object that stores information that is gathered during the validation process + /// Key value pair containing information about the rule violation is returned if the entry's specifier is defined in an unexpected way + public static ValidationFeedback? ValidateSpecifiers(ValidationStructuredEntry entry, ContentValidator validator) + { + ValidationFeedback feedbackItems = []; + if ((entry.Key.FirstSpecifier is null && entry.Key.SecondSpecifier is null) || + validator._dimensionValueNames is null) + { + return null; + } + + // Content dimension specifiers are allowed to be defined using only the first specifier at value level and are checked separately + string[] excludeKeywords = + [ + validator.SyntaxConf.Tokens.KeyWords.Precision, + validator.SyntaxConf.Tokens.KeyWords.Units, + validator.SyntaxConf.Tokens.KeyWords.LastUpdated, + validator.SyntaxConf.Tokens.KeyWords.Contact, + validator.SyntaxConf.Tokens.KeyWords.ValueNote + ]; + + if (excludeKeywords.Contains(entry.Key.Keyword)) + { + return null; + } + + if ((validator._stubDimensionNames is null || !validator._stubDimensionNames.Values.Any(v => v.Contains(entry.Key.FirstSpecifier))) && + (validator._headingDimensionNames is null || !validator._headingDimensionNames.Values.Any(v => v.Contains(entry.Key.FirstSpecifier)))) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + entry.KeyStartLineIndex, + 0, + entry.LineChangeIndexes); + + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.IllegalSpecifierDefinitionFound), + new(validator._filename, + feedbackIndexes.Key, + 0, + entry.Key.FirstSpecifier) + ); + feedbackItems.Add(feedback); + } + else if (entry.Key.SecondSpecifier is not null && + !validator._dimensionValueNames.Values.Any(v => v.Contains(entry.Key.SecondSpecifier))) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + entry.KeyStartLineIndex, + 0, + entry.LineChangeIndexes); + + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.IllegalSpecifierDefinitionFound), + new(validator._filename, + feedbackIndexes.Key, + 0, + entry.Key.SecondSpecifier) + ); + + feedbackItems.Add(feedback); + } + + return feedbackItems; + } + + /// + /// Validates that certain keywords are defined with the correct value types in the Px file metadata entry + /// + /// Entry in the Px file metadata. Represented by a object + /// object that stores information that is gathered during the validation process + /// Key value pair containing information about the rule violation is returned if an unexpected value type is detected + public static ValidationFeedback? ValidateValueTypes(ValidationStructuredEntry entry, ContentValidator validator) + { + string[] stringTypes = + [ + validator.SyntaxConf.Tokens.KeyWords.Charset, + validator.SyntaxConf.Tokens.KeyWords.CodePage, + validator.SyntaxConf.Tokens.KeyWords.DefaultLanguage, + validator.SyntaxConf.Tokens.KeyWords.Units, + validator.SyntaxConf.Tokens.KeyWords.Description, + validator.SyntaxConf.Tokens.KeyWords.TableId, + validator.SyntaxConf.Tokens.KeyWords.ContentVariableIdentifier, + validator.SyntaxConf.Tokens.KeyWords.DimensionCode + ]; + + string[] listOfStringTypes = + [ + validator.SyntaxConf.Tokens.KeyWords.AvailableLanguages, + validator.SyntaxConf.Tokens.KeyWords.StubDimensions, + validator.SyntaxConf.Tokens.KeyWords.HeadingDimensions, + validator.SyntaxConf.Tokens.KeyWords.VariableValues, + validator.SyntaxConf.Tokens.KeyWords.VariableValueCodes + ]; + + string[] dateTimeTypes = + [ + validator.SyntaxConf.Tokens.KeyWords.LastUpdated + ]; + + string[] numberTypes = + [ + validator.SyntaxConf.Tokens.KeyWords.Precision + ]; + + string[] timeval = + [ + validator.SyntaxConf.Tokens.KeyWords.TimeVal + ]; + + if ((stringTypes.Contains(entry.Key.Keyword) && entry.ValueType != ValueType.StringValue) || + (listOfStringTypes.Contains(entry.Key.Keyword) && (entry.ValueType != ValueType.ListOfStrings && entry.ValueType != ValueType.StringValue)) || + (dateTimeTypes.Contains(entry.Key.Keyword) && entry.ValueType != ValueType.DateTime) || + (numberTypes.Contains(entry.Key.Keyword) && entry.ValueType != ValueType.Number) || + (timeval.Contains(entry.Key.Keyword) && (entry.ValueType != ValueType.TimeValRange && entry.ValueType != ValueType.TimeValSeries))) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + entry.KeyStartLineIndex, + entry.ValueStartIndex, + entry.LineChangeIndexes); + + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.UnmatchingValueType), + new(validator._filename, + feedbackIndexes.Key, + feedbackIndexes.Value, + $"{entry.Key.Keyword}: {entry.ValueType}") + ); + + return new(feedback); + } + + return null; + } + + /// + /// Validates that entries with specific keywords have valid values in the Px file metadata entry + /// + /// Entry in the Px file metadata. Represented by a object + /// object that stores information that is gathered during the validation process + /// Key value pair containing information about the rule violation is returned if an unexpected value is detected + public static ValidationFeedback? ValidateValueContents(ValidationStructuredEntry entry, ContentValidator validator) + { + string[] allowedCharsets = ["ANSI", "Unicode"]; + + string[] dimensionTypes = [ + validator.SyntaxConf.Tokens.VariableTypes.Content, + validator.SyntaxConf.Tokens.VariableTypes.Time, + validator.SyntaxConf.Tokens.VariableTypes.Geographical, + validator.SyntaxConf.Tokens.VariableTypes.Ordinal, + validator.SyntaxConf.Tokens.VariableTypes.Nominal, + validator.SyntaxConf.Tokens.VariableTypes.Other, + validator.SyntaxConf.Tokens.VariableTypes.Unknown, + validator.SyntaxConf.Tokens.VariableTypes.Classificatory + ]; + + string value = SyntaxValidationUtilityMethods.CleanString(entry.Value, validator.SyntaxConf); + if ((entry.Key.Keyword == validator.SyntaxConf.Tokens.KeyWords.Charset && !allowedCharsets.Contains(value)) || + (entry.Key.Keyword == validator.SyntaxConf.Tokens.KeyWords.CodePage && !value.Equals(validator._encoding.BodyName, StringComparison.OrdinalIgnoreCase)) || + (entry.Key.Keyword == validator.SyntaxConf.Tokens.KeyWords.DimensionType && !dimensionTypes.Contains(value))) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + entry.KeyStartLineIndex, + entry.ValueStartIndex, + entry.LineChangeIndexes); + + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.InvalidValueFound), + new(validator._filename, + feedbackIndexes.Key, + feedbackIndexes.Value, + $"{entry.Key.Keyword}: {entry.Value}") + ); + + return new(feedback); + } + else if (entry.Key.Keyword == validator.SyntaxConf.Tokens.KeyWords.ContentVariableIdentifier) + { + string defaultLanguage = validator._defaultLanguage ?? string.Empty; + string lang = entry.Key.Language ?? defaultLanguage; + if (validator._stubDimensionNames is not null && validator._stubDimensionNames.TryGetValue(lang, out string[]? stubValues) && + !Array.Exists(stubValues, d => d == value) && + (validator._headingDimensionNames is not null && validator._headingDimensionNames.TryGetValue(lang, out string[]? headingValues) && + !Array.Exists(headingValues, d => d == value))) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + entry.KeyStartLineIndex, + entry.ValueStartIndex, + entry.LineChangeIndexes); + + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.InvalidValueFound), + new(validator._filename, + feedbackIndexes.Key, + feedbackIndexes.Value, + $"{entry.Key.Keyword}: {entry.Value}") + ); + + return new(feedback); + } + } + + return null; + } + + /// + /// Validates that entries with specific keywords have the correct amount of values in the Px file metadata entry + /// + /// Entry in the Px file metadata. Represented by a object + /// object that stores information that is gathered during the validation process + /// Key value pair containing information about the rule violation is returned if an unexpected amount of values is detected + public static ValidationFeedback? ValidateValueAmounts(ValidationStructuredEntry entry, ContentValidator validator) + { + if (entry.Key.Keyword != validator.SyntaxConf.Tokens.KeyWords.VariableValueCodes || + validator._dimensionValueNames is null || + entry.Key.FirstSpecifier is null) + { + return null; + } + + string[] codes = entry.Value.Split(validator.SyntaxConf.Symbols.Value.ListSeparator); + string defaultLanguage = validator._defaultLanguage ?? string.Empty; + string lang = entry.Key.Language ?? defaultLanguage; + if (codes.Length != validator._dimensionValueNames[new(lang, entry.Key.FirstSpecifier)].Length) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + entry.KeyStartLineIndex, + entry.ValueStartIndex, + entry.LineChangeIndexes); + + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.UnmatchingValueAmount), + new(validator._filename, + feedbackIndexes.Key, + feedbackIndexes.Value, + $"{entry.Key.Keyword}: {entry.Value}") + ); + + return new(feedback); + } + + return null; + } + + /// + /// Validates that entries with specific keywords have their values written in upper case + /// + /// Entry in the Px file metadata. Represented by a object + /// object that stores information that is gathered during the validation process + /// Key value pair containing information about the rule violation is returned if the found value is not written in upper case + public static ValidationFeedback? ValidateValueUppercaseRecommendations(ValidationStructuredEntry entry, ContentValidator validator) + { + string[] recommendedUppercaseValueKeywords = [ + validator.SyntaxConf.Tokens.KeyWords.CodePage + ]; + + if (!recommendedUppercaseValueKeywords.Contains(entry.Key.Keyword)) + { + return null; + } + + // Check if entry.value is in upper case + string valueUppercase = entry.Value.ToUpper(CultureInfo.InvariantCulture); + if (entry.Value != valueUppercase) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + entry.KeyStartLineIndex, + entry.ValueStartIndex, + entry.LineChangeIndexes); + + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.ValueIsNotInUpperCase), + new(validator._filename, + feedbackIndexes.Key, + entry.ValueStartIndex, + $"{entry.Key.Keyword}: {entry.Value}") + ); + + return new(feedback); + } + return null; + } + } +} diff --git a/Px.Utils/Validation/ContentValidation/ContentValidator.ValidationFindKeywordFunctions.cs b/Px.Utils/Validation/ContentValidation/ContentValidator.ValidationFindKeywordFunctions.cs new file mode 100644 index 00000000..39517c63 --- /dev/null +++ b/Px.Utils/Validation/ContentValidation/ContentValidator.ValidationFindKeywordFunctions.cs @@ -0,0 +1,528 @@ +using Px.Utils.Validation.SyntaxValidation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Px.Utils.Validation.ContentValidation +{ + public delegate ValidationFeedback? ContentValidationFindKeywordValidator(ValidationStructuredEntry[] entries, ContentValidator validator); + + /// + /// Collection of functions for validating Px file metadata contents by trying to find specific keywords from all entries. + /// + public sealed partial class ContentValidator + { + public List DefaultContentValidationFindKeywordFunctions { get; } = [ + ValidateFindDefaultLanguage, + ValidateFindAvailableLanguages, + ValidateDefaultLanguageDefinedInAvailableLanguages, + ValidateFindContentDimension, + ValidateFindRequiredCommonKeys, + ValidateFindStubAndHeading, + ValidateFindRecommendedKeys, + ValidateFindDimensionValues, + ValidateFindContentDimensionKeys, + ValidateFindDimensionRecommendedKeys + ]; + + /// + /// Validates that default language is defined properly in the Px file metadata + /// + /// Px file metadata entries in an array of objects + /// object that stores information that is gathered during the validation process + /// Null if no issues are found. + /// Key value pairs containing information about rule violations are returned if entry defining default language is not found or if more than one are found. + public static ValidationFeedback? ValidateFindDefaultLanguage(ValidationStructuredEntry[] entries, ContentValidator validator) + { + ValidationStructuredEntry[] langEntries = entries.Where( + e => e.Key.Keyword.Equals(validator.SyntaxConf.Tokens.KeyWords.DefaultLanguage, StringComparison.Ordinal)).ToArray(); + + if (langEntries.Length > 1) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + entries[0].KeyStartLineIndex, + 0, + entries[0].LineChangeIndexes); + + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.MultipleInstancesOfUniqueKey), + new(validator._filename, + feedbackIndexes.Key, + 0, + $"{validator.SyntaxConf.Tokens.KeyWords.DefaultLanguage}: " + string.Join(", ", entries.Select(e => e.Value).ToArray()) + )); + + return new(feedback); + } + else if (langEntries.Length == 0) + { + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.MissingDefaultLanguage), + new(validator._filename, 0, 0) + ); + + return new(feedback); + } + + validator._defaultLanguage = SyntaxValidationUtilityMethods.CleanString(langEntries[0].Value, validator.SyntaxConf); + return null; + } + + /// + /// Finds an entry that defines available languages in the Px file + /// + /// Px file metadata entries in an array of objects + /// object that stores information that is gathered during the validation process + /// Null if no issues are found. Key value pair containing information about the rule violation with a warning is returned if available languages entry is not found. + /// Error is returned if multiple entries are found. + public static ValidationFeedback? ValidateFindAvailableLanguages(ValidationStructuredEntry[] entries, ContentValidator validator) + { + ValidationStructuredEntry[] availableLanguageEntries = entries + .Where(e => e.Key.Keyword.Equals(validator.SyntaxConf.Tokens.KeyWords.AvailableLanguages, StringComparison.Ordinal)) + .ToArray(); + + if (availableLanguageEntries.Length > 1) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + entries[0].KeyStartLineIndex, + 0, + entries[0].LineChangeIndexes); + + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.MultipleInstancesOfUniqueKey), + new(validator._filename, + feedbackIndexes.Key, + 0, + $"{validator.SyntaxConf.Tokens.KeyWords.AvailableLanguages}: " + string.Join(", ", entries.Select(e => e.Value).ToArray())) + ); + + return new(feedback); + } + + if (availableLanguageEntries.Length == 1) + { + List languages = availableLanguageEntries[0].Value.Split(validator.SyntaxConf.Symbols.Value.ListSeparator).ToList(); + validator._availableLanguages = [.. languages.Select(lang => SyntaxValidationUtilityMethods.CleanString(lang, validator.SyntaxConf))]; + return null; + } + else + { + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.RecommendedKeyMissing), + new(validator._filename, + 0, + 0, + validator.SyntaxConf.Tokens.KeyWords.AvailableLanguages) + ); + + return new(feedback); + } + } + + /// + /// Validates that default language is defined in the available languages entry + /// + /// Px file metadata entries in an array of objects + /// object that stores information that is gathered during the validation process + /// Null if no issues are found. + /// Key value pair containing information about the rule violation is returned if default language is not defined in the available languages entry + public static ValidationFeedback? ValidateDefaultLanguageDefinedInAvailableLanguages(ValidationStructuredEntry[] entries, ContentValidator validator) + { + if (validator._availableLanguages is not null && !validator._availableLanguages.Contains(validator._defaultLanguage)) + { + ValidationStructuredEntry? defaultLanguageEntry = Array.Find(entries, e => e.Key.Keyword.Equals(validator.SyntaxConf.Tokens.KeyWords.DefaultLanguage, StringComparison.Ordinal)); + + if (defaultLanguageEntry is null) + { + return null; + } + + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + defaultLanguageEntry.KeyStartLineIndex, + 0, + defaultLanguageEntry.LineChangeIndexes); + + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.UndefinedLanguageFound), + new(validator._filename, + feedbackIndexes.Key, + 0, + validator._defaultLanguage) + ); + + return new(feedback); + } + + return null; + } + + /// + /// Finds a content dimension and validates that it exists for all available languages + /// + /// Px file metadata entries in an array of objects + /// object that stores information that is gathered during the validation process + /// Key value pairs containing information about the rule violation are returned if content dimension entry is not found for any available language + public static ValidationFeedback? ValidateFindContentDimension(ValidationStructuredEntry[] entries, ContentValidator validator) + { + ValidationFeedback feedbackItems = []; + ValidationStructuredEntry[] contentDimensionEntries = entries + .Where(e => e.Key.Keyword.Equals(validator.SyntaxConf.Tokens.KeyWords.ContentVariableIdentifier, StringComparison.Ordinal)) + .ToArray(); + + string defaultLanguage = validator._defaultLanguage ?? string.Empty; + string[] languages = validator._availableLanguages ?? [defaultLanguage]; + foreach (string language in languages) + { + ValidationStructuredEntry? contentDimension = Array.Find(contentDimensionEntries, c => c.Key.Language == language || (c.Key.Language is null && language == defaultLanguage)); + if (contentDimension is null) + { + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.RecommendedKeyMissing), + new(validator._filename, + 0, + 0, + $"{validator.SyntaxConf.Tokens.KeyWords.ContentVariableIdentifier}, {language}") + ); + + feedbackItems.Add(feedback); + } + } + + validator._contentDimensionNames = contentDimensionEntries.ToDictionary( + e => e.Key.Language ?? defaultLanguage, + e => e.Value); + + return feedbackItems; + } + + /// + /// Validates that entries with required keys are found in the Px file metadata + /// + /// Px file metadata entries in an array of objects + /// object that stores information that is gathered during the validation process + /// Key value pairs containing information about the rule violation are returned if entries required keywords are not found + public static ValidationFeedback? ValidateFindRequiredCommonKeys(ValidationStructuredEntry[] entries, ContentValidator validator) + { + ValidationFeedback feedbackItems = []; + string[] alwaysRequiredKeywords = + [ + validator.SyntaxConf.Tokens.KeyWords.Charset, + validator.SyntaxConf.Tokens.KeyWords.CodePage, + validator.SyntaxConf.Tokens.KeyWords.DefaultLanguage, + ]; + + foreach (string keyword in alwaysRequiredKeywords) + { + if (!Array.Exists(entries, e => e.Key.Keyword.Equals(keyword, StringComparison.Ordinal))) + { + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.RequiredKeyMissing), + new(validator._filename, + 0, + 0, + keyword) + ); + + feedbackItems.Add(feedback); + } + } + + return feedbackItems; + } + + /// + /// Finds stub and heading dimensions in the Px file metadata and validates that they are defined for all available languages + /// + /// Px file metadata entries in an array of objects + /// object that stores information that is gathered during the validation process + /// Key value pairs containing information about the rule violation are returned if stub and heading dimension entries are not found for any available language + public static ValidationFeedback? ValidateFindStubAndHeading(ValidationStructuredEntry[] entries, ContentValidator validator) + { + ValidationStructuredEntry[] stubEntries = entries.Where(e => e.Key.Keyword.Equals(validator.SyntaxConf.Tokens.KeyWords.StubDimensions, StringComparison.Ordinal)).ToArray(); + ValidationStructuredEntry[] headingEntries = entries.Where(e => e.Key.Keyword.Equals(validator.SyntaxConf.Tokens.KeyWords.HeadingDimensions, StringComparison.Ordinal)).ToArray(); + + if (stubEntries.Length == 0 && headingEntries.Length == 0) + { + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.MissingStubAndHeading), + new(validator._filename, 0, 0) + ); + + return new(feedback); + } + + string defaultLanguage = validator._defaultLanguage ?? string.Empty; + + validator._stubDimensionNames = GetDimensionNames(stubEntries, defaultLanguage, validator.SyntaxConf); + + validator._headingDimensionNames = GetDimensionNames(headingEntries, defaultLanguage, validator.SyntaxConf); + + ValidationFeedback feedbackItems = []; + + string[] languages = validator._availableLanguages ?? [defaultLanguage]; + + foreach (string language in languages) + { + if ((validator._stubDimensionNames is null || !validator._stubDimensionNames.ContainsKey(language)) + && + (validator._headingDimensionNames is null || !validator._headingDimensionNames.ContainsKey(language))) + { + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.MissingStubAndHeading), + new(validator._filename, + 0, + 0, + $"{language}") + ); + + feedbackItems.Add(feedback); + } + // Check if any of the heading names are also in the stub names + else if (validator._stubDimensionNames is not null && + validator._headingDimensionNames is not null && + validator._stubDimensionNames.TryGetValue(language, out string[]? stubValue) && + validator._headingDimensionNames.TryGetValue(language, out string[]? headingValue) && + stubValue.Intersect(headingValue).Any()) + { + string[] duplicates = stubValue.Intersect(headingValue).ToArray(); + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.DuplicateDimension), + new(validator._filename, + 0, + 0, + $"{language}, {string.Join(", ", duplicates)}") + ); + + feedbackItems.Add(feedback); + } + } + + return feedbackItems; + } + + /// + /// Finds recommended languages dependent or language independent keys in the Px file metadata + /// + /// Px file metadata entries in an array of objects + /// object that stores information that is gathered during the validation process + /// Key value pairs containing information about the rule violation are returned if recommended keys are missing + public static ValidationFeedback? ValidateFindRecommendedKeys(ValidationStructuredEntry[] entries, ContentValidator validator) + { + ValidationFeedback feedbackItems = []; + + string[] commonKeys = + [ + validator.SyntaxConf.Tokens.KeyWords.TableId + ]; + string[] languageSpecific = + [ + validator.SyntaxConf.Tokens.KeyWords.Description + ]; + + foreach (string keyword in commonKeys) + { + if (!Array.Exists(entries, e => e.Key.Keyword.Equals(keyword, StringComparison.Ordinal))) + { + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.RecommendedKeyMissing), + new(validator._filename, + 0, + 0, + keyword) + ); + + feedbackItems.Add(feedback); + } + } + + string defaultLanguage = validator._defaultLanguage ?? string.Empty; + string[] languages = validator._availableLanguages ?? [defaultLanguage]; + + foreach (string language in languages) + { + foreach (string keyword in languageSpecific) + { + if (!Array.Exists(entries, e => e.Key.Keyword.Equals(keyword, StringComparison.Ordinal) && + (e.Key.Language == language || (language == defaultLanguage && e.Key.Language is null)))) + { + KeyValuePair feedback = new( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.RecommendedKeyMissing), + new(validator._filename, + 0, + 0, + $"{language}, {keyword}") + ); + + feedbackItems.Add(feedback); + } + } + } + + return feedbackItems; + } + + /// + /// Finds and validates values for stub and heading dimensions in the Px file metadata + /// + /// Px file metadata entries in an array of objects + /// object that stores information that is gathered during the validation process + /// Key value pairs containing information about the rule violation are returned if values are missing for any dimension + public static ValidationFeedback? ValidateFindDimensionValues(ValidationStructuredEntry[] entries, ContentValidator validator) + { + ValidationFeedback feedbackItems = []; + ValidationStructuredEntry[] dimensionEntries = entries.Where( + e => e.Key.Keyword.Equals(validator.SyntaxConf.Tokens.KeyWords.VariableValues, StringComparison.Ordinal)).ToArray(); + + IEnumerable, string[]>> dimensionValues = []; + dimensionValues = dimensionValues.Concat( + FindDimensionValues( + dimensionEntries, + validator.SyntaxConf, + validator._stubDimensionNames, + ref feedbackItems, + validator._filename) + ); + dimensionValues = dimensionValues.Concat( + FindDimensionValues( + dimensionEntries, + validator.SyntaxConf, + validator._headingDimensionNames, + ref feedbackItems, + validator._filename) + ); + + validator._dimensionValueNames = []; + foreach (KeyValuePair, string[]> item in dimensionValues) + { + if (!validator._dimensionValueNames.TryAdd(item.Key, item.Value)) + { + var originalValue = validator._dimensionValueNames[item.Key]; + validator._dimensionValueNames[item.Key] = [.. originalValue, .. item.Value]; + + feedbackItems.Add(new( + new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.DuplicateEntry), + new(validator._filename, additionalInfo: $"{item.Key.Key}, {item.Key.Value}") + )); + } + } + + return feedbackItems; + } + + /// + /// Finds and validates that entries with required keys related to the content dimension and its values are found in the Px file metadata + /// + /// Px file metadata entries in an array of objects + /// object that stores information that is gathered during the validation process + /// Key value pairs containing information about the rule violation are returned if required entries are missing. + /// Warnings are returned if entries are defined in an unrecommended way + public static ValidationFeedback? ValidateFindContentDimensionKeys(ValidationStructuredEntry[] entries, ContentValidator validator) + { + ValidationFeedback feedbackItems = []; + + string[] languageSpecificKeywords = + [ + validator.SyntaxConf.Tokens.KeyWords.Units, + ]; + + string[] requiredKeywords = + [ + validator.SyntaxConf.Tokens.KeyWords.LastUpdated, + ]; + + string[] recommendedKeywords = + [ + validator.SyntaxConf.Tokens.KeyWords.Precision + ]; + + string[] contentDimensionNames = + validator._contentDimensionNames is not null ? + [.. validator._contentDimensionNames.Values] : []; + + Dictionary, string[]> dimensionValueNames = + validator._dimensionValueNames ?? []; + + Dictionary, string[]> contentDimensionValueNames = dimensionValueNames + .Where(e => contentDimensionNames.Contains(e.Key.Value)) + .ToDictionary(e => e.Key, e => e.Value); + + foreach (KeyValuePair, string[]> kvp in contentDimensionValueNames) + { + foreach (string dimensionValueName in kvp.Value) + { + ValidationFeedback items = ProcessContentDimensionValue(languageSpecificKeywords, requiredKeywords, recommendedKeywords, entries, validator, kvp.Key, dimensionValueName); + feedbackItems.AddRange(items); + } + } + + return feedbackItems; + } + + /// + /// Validates that recommended entries related to dimensions are found in the Px file metadata + /// + /// Px file metadata entries in an array of objects + /// object that stores information that is gathered during the validation process + /// Key value pairs containing information about the rule violation are returned if any dimensions are missing recommended entries related to them + public static ValidationFeedback? ValidateFindDimensionRecommendedKeys(ValidationStructuredEntry[] entries, ContentValidator validator) + { + ValidationFeedback feedbackItems = []; + + string[] dimensionRecommendedKeywords = + [ + validator.SyntaxConf.Tokens.KeyWords.DimensionCode, + validator.SyntaxConf.Tokens.KeyWords.VariableValueCodes + ]; + + Dictionary stubDimensions = validator._stubDimensionNames ?? []; + Dictionary headingDimensions = validator._headingDimensionNames ?? []; + Dictionary allDimensions = stubDimensions + .Concat(headingDimensions) + .GroupBy(kvp => kvp.Key) + .ToDictionary(g => g.Key, g => g.SelectMany(kvp => kvp.Value).ToArray()); + + foreach (KeyValuePair languageDimensions in allDimensions) + { + KeyValuePair? timeValFeedback = FindDimensionRecommendedKey( + entries, + validator.SyntaxConf.Tokens.KeyWords.TimeVal, + languageDimensions.Key, + validator); + + if (timeValFeedback is not null) + { + feedbackItems.Add((KeyValuePair)timeValFeedback); + } + + foreach (string dimension in languageDimensions.Value) + { + ValidationFeedback dimensionFeedback = ProcessDimension( + dimensionRecommendedKeywords, + validator.SyntaxConf.Tokens.KeyWords.DimensionType, + entries, + languageDimensions.Key, + validator, + dimension); + feedbackItems.AddRange(dimensionFeedback); + } + } + + return feedbackItems; + } + + } +} diff --git a/Px.Utils/Validation/ContentValidation/ContentValidator.ValidationFunctions.cs b/Px.Utils/Validation/ContentValidation/ContentValidator.ValidationFunctions.cs deleted file mode 100644 index c01e06cd..00000000 --- a/Px.Utils/Validation/ContentValidation/ContentValidator.ValidationFunctions.cs +++ /dev/null @@ -1,1024 +0,0 @@ -using Px.Utils.PxFile; -using Px.Utils.Validation.SyntaxValidation; -using System.Globalization; - -namespace Px.Utils.Validation.ContentValidation -{ - public delegate ValidationFeedbackItem[]? ContentValidationEntryValidator(ValidationStructuredEntry entry, ContentValidator validator); - public delegate ValidationFeedbackItem[]? ContentValidationFindKeywordValidator(ValidationStructuredEntry[] entries, ContentValidator validator); - - /// - /// Collection of functions for validating Px file metadata contents - /// - public sealed partial class ContentValidator - { - public List DefaultContentValidationEntryFunctions { get; } = [ - ValidateUnexpectedSpecifiers, - ValidateUnexpectedLanguageParams, - ValidateLanguageParams, - ValidateSpecifiers, - ValidateValueTypes, - ValidateValueContents, - ValidateValueAmounts, - ValidateValueUppercaseRecommendations - ]; - - public List DefaultContentValidationFindKeywordFunctions { get; } = [ - ValidateFindDefaultLanguage, - ValidateFindAvailableLanguages, - ValidateDefaultLanguageDefinedInAvailableLanguages, - ValidateFindContentDimension, - ValidateFindRequiredCommonKeys, - ValidateFindStubAndHeading, - ValidateFindRecommendedKeys, - ValidateFindDimensionValues, - ValidateFindContentDimensionKeys, - ValidateFindDimensionRecommendedKeys - ]; - - /// - /// Validates that default language is defined properly in the Px file metadata - /// - /// Px file metadata entries in an array of objects - /// object that stores information that is gathered during the validation process - /// Null if no issues are found. objects are returned if entry defining default language is not found or if more than one are found. - public static ValidationFeedbackItem[]? ValidateFindDefaultLanguage(ValidationStructuredEntry[] entries, ContentValidator validator) - { - ValidationStructuredEntry[] langEntries = entries.Where( - e => e.Key.Keyword.Equals(validator.SyntaxConf.Tokens.KeyWords.DefaultLanguage, StringComparison.Ordinal)).ToArray(); - - if (langEntries.Length > 1) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - entries[0].KeyStartLineIndex, - 0, - entries[0].LineChangeIndexes); - - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.MultipleInstancesOfUniqueKey, - feedbackIndexes.Key, - 0, - $"{validator.SyntaxConf.Tokens.KeyWords.DefaultLanguage}: " + string.Join(", ", entries.Select(e => e.Value).ToArray()) - ); - - return [ - new ValidationFeedbackItem( - entries[0], - feedback - ), - ]; - } - else if (langEntries.Length == 0) - { - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.MissingDefaultLanguage, - 0, - 0 - ); - - return [ - new ValidationFeedbackItem( - new ValidationObject(validator._filename, 0, []), - feedback - ), - ]; - } - - validator._defaultLanguage = langEntries[0].Value; - return null; - } - - /// - /// Finds an entry that defines available languages in the Px file - /// - /// Px file metadata entries in an array of objects - /// object that stores information that is gathered during the validation process - /// Null if no issues are found. object with a warning is returned if available languages entry is not found. - /// Error is returned if multiple entries are found. - public static ValidationFeedbackItem[]? ValidateFindAvailableLanguages(ValidationStructuredEntry[] entries, ContentValidator validator) - { - ValidationStructuredEntry[] availableLanguageEntries = entries - .Where(e => e.Key.Keyword.Equals(validator.SyntaxConf.Tokens.KeyWords.AvailableLanguages, StringComparison.Ordinal)) - .ToArray(); - - if (availableLanguageEntries.Length > 1) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - entries[0].KeyStartLineIndex, - 0, - entries[0].LineChangeIndexes); - - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.MultipleInstancesOfUniqueKey, - feedbackIndexes.Key, - 0, - $"{validator.SyntaxConf.Tokens.KeyWords.AvailableLanguages}: " + string.Join(", ", entries.Select(e => e.Value).ToArray()) - ); - - return [ - new ValidationFeedbackItem( - entries[0], - feedback - ) - ]; - } - - if (availableLanguageEntries.Length == 1) - { - validator._availableLanguages = availableLanguageEntries[0].Value.Split(validator.SyntaxConf.Symbols.Value.ListSeparator); - return null; - } - else - { - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.RecommendedKeyMissing, - 0, - 0, - validator.SyntaxConf.Tokens.KeyWords.AvailableLanguages - ); - - return [ - new ValidationFeedbackItem( - new ValidationObject(validator._filename, 0, []), - feedback - ) - ]; - } - } - - /// - /// Validates that default language is defined in the available languages entry - /// - /// Px file metadata entries in an array of objects - /// object that stores information that is gathered during the validation process - /// Null of no issues are found. object is returned if default language is not defined in the available languages entry - public static ValidationFeedbackItem[]? ValidateDefaultLanguageDefinedInAvailableLanguages(ValidationStructuredEntry[] entries, ContentValidator validator) - { - if (validator._availableLanguages is not null && !validator._availableLanguages.Contains(validator._defaultLanguage)) - { - ValidationStructuredEntry? defaultLanguageEntry = Array.Find(entries, e => e.Key.Keyword.Equals(validator.SyntaxConf.Tokens.KeyWords.DefaultLanguage, StringComparison.Ordinal)); - - if (defaultLanguageEntry is null) - { - return null; - } - - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - defaultLanguageEntry.KeyStartLineIndex, - 0, - defaultLanguageEntry.LineChangeIndexes); - - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.UndefinedLanguageFound, - feedbackIndexes.Key, - 0, - validator._defaultLanguage - ); - - return [ - new ValidationFeedbackItem( - defaultLanguageEntry, - feedback - ) - ]; - } - - return null; - } - - /// - /// Finds a content dimension and validates that it exists for all available languages - /// - /// Px file metadata entries in an array of objects - /// object that stores information that is gathered during the validation process - /// objects are returned if content dimension entry is not found for any available language - public static ValidationFeedbackItem[]? ValidateFindContentDimension(ValidationStructuredEntry[] entries, ContentValidator validator) - { - List feedbackItems = []; - ValidationStructuredEntry[] contentDimensionEntries = entries - .Where(e => e.Key.Keyword.Equals(validator.SyntaxConf.Tokens.KeyWords.ContentVariableIdentifier, StringComparison.Ordinal)) - .ToArray(); - - string defaultLanguage = validator._defaultLanguage ?? string.Empty; - string[] languages = validator._availableLanguages ?? [defaultLanguage]; - foreach (string language in languages) - { - ValidationStructuredEntry? contentDimension = Array.Find(contentDimensionEntries, c => c.Key.Language == language || (c.Key.Language is null && language == defaultLanguage)); - if (contentDimension is null) - { - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.RecommendedKeyMissing, - 0, - 0, - $"{validator.SyntaxConf.Tokens.KeyWords.ContentVariableIdentifier}, {language}" - ); - - feedbackItems.Add( - new ValidationFeedbackItem( - new ValidationObject(validator._filename, 0, []), - feedback - )); - } - } - - validator._contentDimensionNames = contentDimensionEntries.ToDictionary( - e => e.Key.Language ?? defaultLanguage, - e => e.Value); - - return [.. feedbackItems]; - } - - /// - /// Validates that entries with required keys are found in the Px file metadata - /// - /// Px file metadata entries in an array of objects - /// object that stores information that is gathered during the validation process - /// objects are returned if entries required keywords are not found - public static ValidationFeedbackItem[]? ValidateFindRequiredCommonKeys(ValidationStructuredEntry[] entries, ContentValidator validator) - { - List feedbackItems = []; - string[] alwaysRequiredKeywords = - [ - validator.SyntaxConf.Tokens.KeyWords.Charset, - validator.SyntaxConf.Tokens.KeyWords.CodePage, - validator.SyntaxConf.Tokens.KeyWords.DefaultLanguage, - ]; - - foreach (string keyword in alwaysRequiredKeywords) - { - if (!Array.Exists(entries, e => e.Key.Keyword.Equals(keyword, StringComparison.Ordinal))) - { - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.RequiredKeyMissing, - 0, - 0, - keyword - ); - - feedbackItems.Add( - new ValidationFeedbackItem( - new ValidationObject(validator._filename, 0, []), - feedback - ) - ); - } - } - - return [.. feedbackItems]; - } - - /// - /// Finds stub and heading dimensions in the Px file metadata and validates that they are defined for all available languages - /// - /// Px file metadata entries in an array of objects - /// object that stores information that is gathered during the validation process - /// objects are returned if stub and heading dimension entries are not found for any available language - public static ValidationFeedbackItem[]? ValidateFindStubAndHeading(ValidationStructuredEntry[] entries, ContentValidator validator) - { - ValidationStructuredEntry[] stubEntries = entries.Where(e => e.Key.Keyword.Equals(validator.SyntaxConf.Tokens.KeyWords.StubDimensions, StringComparison.Ordinal)).ToArray(); - ValidationStructuredEntry[] headingEntries = entries.Where(e => e.Key.Keyword.Equals(validator.SyntaxConf.Tokens.KeyWords.HeadingDimensions, StringComparison.Ordinal)).ToArray(); - - if (stubEntries.Length == 0 && headingEntries.Length == 0) - { - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.MissingStubAndHeading, - 0, - 0 - ); - - return [ - new( - new ValidationObject(validator._filename, 0, []), - feedback - ) - ]; - } - - string defaultLanguage = validator._defaultLanguage ?? string.Empty; - - validator._stubDimensionNames = stubEntries.ToDictionary( - e => e.Key.Language ?? defaultLanguage, - e => e.Value.Split(validator.SyntaxConf.Symbols.Key.ListSeparator)); - - validator._headingDimensionNames = headingEntries.ToDictionary( - e => e.Key.Language ?? defaultLanguage, - e => e.Value.Split(validator.SyntaxConf.Symbols.Key.ListSeparator)); - - List feedbackItems = []; - - string[] languages = validator._availableLanguages ?? [defaultLanguage]; - - foreach (string language in languages) - { - if ((validator._stubDimensionNames is null || !validator._stubDimensionNames.ContainsKey(language)) - && - (validator._headingDimensionNames is null || !validator._headingDimensionNames.ContainsKey(language))) - { - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.MissingStubAndHeading, - 0, - 0, - $"{language}" - ); - - feedbackItems.Add( - new ValidationFeedbackItem( - new ValidationObject(validator._filename, 0, []), - feedback - ) - ); - } - } - - return [.. feedbackItems]; - } - - /// - /// Finds recommended languages dependent or language independent keys in the Px file metadata - /// - /// Px file metadata entries in an array of objects - /// object that stores information that is gathered during the validation process - /// objects with warning are returned if recommended keys are missing - public static ValidationFeedbackItem[]? ValidateFindRecommendedKeys(ValidationStructuredEntry[] entries, ContentValidator validator) - { - List feedbackItems = []; - - string[] commonKeys = - [ - validator.SyntaxConf.Tokens.KeyWords.TableId - ]; - string[] languageSpecific = - [ - validator.SyntaxConf.Tokens.KeyWords.Description - ]; - - foreach (string keyword in commonKeys) - { - if (!Array.Exists(entries, e => e.Key.Keyword.Equals(keyword, StringComparison.Ordinal))) - { - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.RecommendedKeyMissing, - 0, - 0, - keyword - ); - - feedbackItems.Add( - new ValidationFeedbackItem( - new ValidationObject(validator._filename, 0, []), - feedback - ) - ); - } - } - - string defaultLanguage = validator._defaultLanguage ?? string.Empty; - string[] languages = validator._availableLanguages ?? [defaultLanguage]; - - foreach(string language in languages) - { - foreach(string keyword in languageSpecific) - { - if (!Array.Exists(entries, e => e.Key.Keyword.Equals(keyword, StringComparison.Ordinal) && - (e.Key.Language == language || (language == defaultLanguage && e.Key.Language is null)))) - { - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.RecommendedKeyMissing, - 0, - 0, - $"{language}, {keyword}" - ); - - feedbackItems.Add( - new ValidationFeedbackItem( - new ValidationObject(validator._filename, 0, []), - feedback - ) - ); - } - } - } - - return [.. feedbackItems]; - } - - /// - /// Finds and validates values for stub and heading dimensions in the Px file metadata - /// - /// Px file metadata entries in an array of objects - /// object that stores information that is gathered during the validation process - /// objects are returned if values are missing for any dimension - public static ValidationFeedbackItem[]? ValidateFindDimensionValues(ValidationStructuredEntry[] entries, ContentValidator validator) - { - List feedbackItems = []; - ValidationStructuredEntry[] dimensionEntries = entries.Where( - e => e.Key.Keyword.Equals(validator.SyntaxConf.Tokens.KeyWords.VariableValues, StringComparison.Ordinal)).ToArray(); - - IEnumerable, string[]>> dimensionValues = []; - dimensionValues = dimensionValues.Concat( - FindDimensionValues( - dimensionEntries, - validator.SyntaxConf, - validator._stubDimensionNames, - ref feedbackItems, - validator._filename) - ); - dimensionValues = dimensionValues.Concat( - FindDimensionValues( - dimensionEntries, - validator.SyntaxConf, - validator._headingDimensionNames, - ref feedbackItems, - validator._filename) - ); - - validator._dimensionValueNames = []; - foreach (KeyValuePair, string[]> item in dimensionValues) - { - validator._dimensionValueNames.Add(item.Key, item.Value); - } - - return [.. feedbackItems]; - } - - /// - /// Finds and validates that entries with required keys related to the content dimension and its values are found in the Px file metadata - /// - /// Px file metadata entries in an array of objects - /// object that stores information that is gathered during the validation process - /// objects are returned if required entries are missing. Warnings are returned if entries are defined in an unrecommended way - public static ValidationFeedbackItem[]? ValidateFindContentDimensionKeys(ValidationStructuredEntry[] entries, ContentValidator validator) - { - List? feedbackItems = []; - - string[] languageSpecificKeywords = - [ - validator.SyntaxConf.Tokens.KeyWords.Units, - ]; - - string[] requiredKeywords = - [ - validator.SyntaxConf.Tokens.KeyWords.LastUpdated, - ]; - - string[] recommendedKeywords = - [ - validator.SyntaxConf.Tokens.KeyWords.Precision - ]; - - string[] contentDimensionNames = - validator._contentDimensionNames is not null ? - [.. validator._contentDimensionNames.Values] : []; - - Dictionary, string[]> dimensionValueNames = - validator._dimensionValueNames ?? []; - - Dictionary, string[]> contentDimensionValueNames = dimensionValueNames - .Where(e => contentDimensionNames.Contains(e.Key.Value)) - .ToDictionary(e => e.Key, e => e.Value); - - foreach (KeyValuePair, string[]> kvp in contentDimensionValueNames) - { - foreach (string dimensionValueName in kvp.Value) - { - List items = ProcessContentDimensionValue(languageSpecificKeywords, requiredKeywords, recommendedKeywords, entries, validator, kvp.Key, dimensionValueName); - feedbackItems.AddRange(items); - } - } - - return [.. feedbackItems]; - } - - /// - /// Validates that recommended entries related to dimensions are found in the Px file metadata - /// - /// Px file metadata entries in an array of objects - /// object that stores information that is gathered during the validation process - /// objects with warnings are returned if any dimensions are missing recommended entries related to them - public static ValidationFeedbackItem[]? ValidateFindDimensionRecommendedKeys(ValidationStructuredEntry[] entries, ContentValidator validator) - { - List feedbackItems = []; - - string[] dimensionRecommendedKeywords = - [ - validator.SyntaxConf.Tokens.KeyWords.DimensionCode, - validator.SyntaxConf.Tokens.KeyWords.VariableValueCodes - ]; - - Dictionary stubDimensions = validator._stubDimensionNames ?? []; - Dictionary headingDimensions = validator._headingDimensionNames ?? []; - Dictionary allDimensions = stubDimensions - .Concat(headingDimensions) - .GroupBy(kvp => kvp.Key) - .ToDictionary(g => g.Key, g => g.SelectMany(kvp => kvp.Value).ToArray()); - - foreach (KeyValuePair languageDimensions in allDimensions) - { - ValidationFeedbackItem? timeValFeedback = FindDimensionRecommendedKey(entries, validator.SyntaxConf.Tokens.KeyWords.TimeVal, languageDimensions.Key, validator); - if (timeValFeedback is not null) - { - feedbackItems.Add((ValidationFeedbackItem)timeValFeedback); - } - - foreach (string dimension in languageDimensions.Value) - { - List dimensionFeedback = ProcessDimension( - dimensionRecommendedKeywords, - validator.SyntaxConf.Tokens.KeyWords.DimensionType, - entries, - languageDimensions.Key, - validator, - dimension); - feedbackItems.AddRange(dimensionFeedback); - } - } - - return [.. feedbackItems]; - } - - /// - /// - /// - /// Px file metadata entries in an array of objects - /// object that stores information that is gathered during the validation process - /// objects are returned if - public static ValidationFeedbackItem[]? ValidateUnexpectedSpecifiers(ValidationStructuredEntry entry, ContentValidator validator) - { - string[] noSpecifierAllowedKeywords = - [ - validator.SyntaxConf.Tokens.KeyWords.DefaultLanguage, - validator.SyntaxConf.Tokens.KeyWords.AvailableLanguages, - validator.SyntaxConf.Tokens.KeyWords.Charset, - validator.SyntaxConf.Tokens.KeyWords.CodePage, - validator.SyntaxConf.Tokens.KeyWords.StubDimensions, - validator.SyntaxConf.Tokens.KeyWords.HeadingDimensions, - validator.SyntaxConf.Tokens.KeyWords.TableId, - validator.SyntaxConf.Tokens.KeyWords.Description, - validator.SyntaxConf.Tokens.KeyWords.ContentVariableIdentifier, - ]; - - if (!noSpecifierAllowedKeywords.Contains(entry.Key.Keyword)) - { - return null; - } - - if (entry.Key.FirstSpecifier is not null || entry.Key.SecondSpecifier is not null) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - entry.KeyStartLineIndex, - 0, - entry.LineChangeIndexes); - - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.IllegalSpecifierDefinitionFound, - feedbackIndexes.Key, - 0, - $"{entry.Key.Keyword}: {entry.Key.FirstSpecifier}, {entry.Key.SecondSpecifier}" - ); - - return [ - new ValidationFeedbackItem( - entry, - feedback - ) - ]; - } - - return null; - } - - /// - /// Finds unexpected language parameters in the Px file metadata entry - /// - /// Entry in the Px file metadata. Represented by a object - /// object that stores information that is gathered during the validation process - /// objects are returned if an illegal or unrecommended language parameter is detected in the entry - public static ValidationFeedbackItem[]? ValidateUnexpectedLanguageParams(ValidationStructuredEntry entry, ContentValidator validator) - { - string[] noLanguageParameterAllowedKeywords = [ - validator.SyntaxConf.Tokens.KeyWords.Charset, - validator.SyntaxConf.Tokens.KeyWords.CodePage, - validator.SyntaxConf.Tokens.KeyWords.TableId, - validator.SyntaxConf.Tokens.KeyWords.DefaultLanguage, - validator.SyntaxConf.Tokens.KeyWords.AvailableLanguages, - ]; - - string[] noLanguageParameterRecommendedKeywords = [ - validator.SyntaxConf.Tokens.KeyWords.LastUpdated, - validator.SyntaxConf.Tokens.KeyWords.DimensionType, - validator.SyntaxConf.Tokens.KeyWords.Precision - ]; - - if (!noLanguageParameterAllowedKeywords.Contains(entry.Key.Keyword) && !noLanguageParameterRecommendedKeywords.Contains(entry.Key.Keyword)) - { - return null; - } - - if (entry.Key.Language is not null) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - entry.KeyStartLineIndex, - 0, - entry.LineChangeIndexes); - - if (noLanguageParameterAllowedKeywords.Contains(entry.Key.Keyword)) - { - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.IllegalLanguageDefinitionFound, - feedbackIndexes.Key, - 0, - $"{entry.Key.Keyword}, {entry.Key.Language}, {entry.Key.FirstSpecifier}, {entry.Key.SecondSpecifier}"); - - return [ - new ValidationFeedbackItem( - entry, - feedback - ) - ]; - } - else if (noLanguageParameterRecommendedKeywords.Contains(entry.Key.Keyword)) - { - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.UnrecommendedLanguageDefinitionFound, - feedbackIndexes.Key, - 0, - $"{entry.Key.Keyword}, {entry.Key.Language}, {entry.Key.FirstSpecifier}, {entry.Key.SecondSpecifier}"); - - return [ - new ValidationFeedbackItem( - entry, - feedback - ) - ]; - } - } - - return null; - } - - /// - /// Validates that the language defined in the Px file metadata entry is defined in the available languages entry - /// - /// Entry in the Px file metadata. Represented by a object - /// object that stores information that is gathered during the validation process - /// object is returned if an undefined language is found from the entry - public static ValidationFeedbackItem[]? ValidateLanguageParams(ValidationStructuredEntry entry, ContentValidator validator) - { - if (entry.Key.Language is null) - { - return null; - } - - if (validator._availableLanguages is not null && !validator._availableLanguages.Contains(entry.Key.Language)) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - entry.KeyStartLineIndex, - 0, - entry.LineChangeIndexes); - - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.UndefinedLanguageFound, - feedbackIndexes.Key, - 0, - entry.Key.Language); - - return [ - new ValidationFeedbackItem( - entry, - feedback - ) - ]; - } - - return null; - } - - /// - /// Validates that specifiers are defined properly in the Px file metadata entry. - /// Content dimension specifiers are allowed to be defined using only the first specifier at value level and are checked separately - /// - /// Entry in the Px file metadata. Represented by a object - /// object that stores information that is gathered during the validation process - /// object is returned if the entry's specifier is defined in an unexpected way - public static ValidationFeedbackItem[]? ValidateSpecifiers(ValidationStructuredEntry entry, ContentValidator validator) - { - List feedbackItems = []; - if ((entry.Key.FirstSpecifier is null && entry.Key.SecondSpecifier is null) || - validator._dimensionValueNames is null) - { - return null; - } - - // Content dimension specifiers are allowed to be defined using only the first specifier at value level and are checked separately - string[] excludeKeywords = - [ - validator.SyntaxConf.Tokens.KeyWords.Precision, - validator.SyntaxConf.Tokens.KeyWords.Units, - validator.SyntaxConf.Tokens.KeyWords.LastUpdated, - validator.SyntaxConf.Tokens.KeyWords.Contact, - validator.SyntaxConf.Tokens.KeyWords.ValueNote - ]; - - if (excludeKeywords.Contains(entry.Key.Keyword)) - { - return null; - } - - if ((validator._stubDimensionNames is null || !validator._stubDimensionNames.Values.Any(v => v.Contains(entry.Key.FirstSpecifier))) && - (validator._headingDimensionNames is null || !validator._headingDimensionNames.Values.Any(v => v.Contains(entry.Key.FirstSpecifier)))) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - entry.KeyStartLineIndex, - 0, - entry.LineChangeIndexes); - - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.IllegalSpecifierDefinitionFound, - feedbackIndexes.Key, - 0, - entry.Key.FirstSpecifier); - - feedbackItems.Add( - new ValidationFeedbackItem( - entry, - feedback - ) - ); - } - else if (entry.Key.SecondSpecifier is not null && - !validator._dimensionValueNames.Values.Any(v => v.Contains(entry.Key.SecondSpecifier))) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - entry.KeyStartLineIndex, - 0, - entry.LineChangeIndexes); - - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.IllegalSpecifierDefinitionFound, - feedbackIndexes.Key, - 0, - entry.Key.SecondSpecifier); - - feedbackItems.Add( - new ValidationFeedbackItem( - entry, - feedback - ) - ); - } - - return [.. feedbackItems]; - } - - /// - /// Validates that certain keywords are defined with the correct value types in the Px file metadata entry - /// - /// Entry in the Px file metadata. Represented by a object - /// object that stores information that is gathered during the validation process - /// object is returned if an unexpected value type is detected - public static ValidationFeedbackItem[]? ValidateValueTypes(ValidationStructuredEntry entry, ContentValidator validator) - { - string[] stringTypes = - [ - validator.SyntaxConf.Tokens.KeyWords.Charset, - validator.SyntaxConf.Tokens.KeyWords.CodePage, - validator.SyntaxConf.Tokens.KeyWords.DefaultLanguage, - validator.SyntaxConf.Tokens.KeyWords.Units, - validator.SyntaxConf.Tokens.KeyWords.Description, - validator.SyntaxConf.Tokens.KeyWords.TableId, - validator.SyntaxConf.Tokens.KeyWords.ContentVariableIdentifier, - validator.SyntaxConf.Tokens.KeyWords.DimensionCode - ]; - - string[] listOfStringTypes = - [ - validator.SyntaxConf.Tokens.KeyWords.AvailableLanguages, - validator.SyntaxConf.Tokens.KeyWords.StubDimensions, - validator.SyntaxConf.Tokens.KeyWords.HeadingDimensions, - validator.SyntaxConf.Tokens.KeyWords.VariableValues, - validator.SyntaxConf.Tokens.KeyWords.VariableValueCodes - ]; - - string[] dateTimeTypes = - [ - validator.SyntaxConf.Tokens.KeyWords.LastUpdated - ]; - - string[] numberTypes = - [ - validator.SyntaxConf.Tokens.KeyWords.Precision - ]; - - string[] timeval = - [ - validator.SyntaxConf.Tokens.KeyWords.TimeVal - ]; - - if ((stringTypes.Contains(entry.Key.Keyword) && entry.ValueType != ValueType.StringValue) || - (listOfStringTypes.Contains(entry.Key.Keyword) && (entry.ValueType != ValueType.ListOfStrings && entry.ValueType != ValueType.StringValue)) || - (dateTimeTypes.Contains(entry.Key.Keyword) && entry.ValueType != ValueType.DateTime) || - (numberTypes.Contains(entry.Key.Keyword) && entry.ValueType != ValueType.Number) || - (timeval.Contains(entry.Key.Keyword) && (entry.ValueType != ValueType.TimeValRange && entry.ValueType != ValueType.TimeValSeries))) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - entry.KeyStartLineIndex, - entry.ValueStartIndex, - entry.LineChangeIndexes); - - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.UnmatchingValueType, - feedbackIndexes.Key, - feedbackIndexes.Value, - $"{entry.Key.Keyword}: {entry.ValueType}"); - - return [ - new ValidationFeedbackItem( - entry, - feedback - ) - ]; - } - - return null; - } - - /// - /// Validates that entries with specific keywords have valid values in the Px file metadata entry - /// - /// Entry in the Px file metadata. Represented by a object - /// object that stores information that is gathered during the validation process - /// object is returned if an unexpected value is detected - public static ValidationFeedbackItem[]? ValidateValueContents(ValidationStructuredEntry entry, ContentValidator validator) - { - string[] allowedCharsets = ["ANSI", "Unicode"]; - - string[] dimensionTypes = [ - validator.SyntaxConf.Tokens.VariableTypes.Content, - validator.SyntaxConf.Tokens.VariableTypes.Time, - validator.SyntaxConf.Tokens.VariableTypes.Geographical, - validator.SyntaxConf.Tokens.VariableTypes.Ordinal, - validator.SyntaxConf.Tokens.VariableTypes.Nominal, - validator.SyntaxConf.Tokens.VariableTypes.Other, - validator.SyntaxConf.Tokens.VariableTypes.Unknown, - validator.SyntaxConf.Tokens.VariableTypes.Classificatory - ]; - - if ((entry.Key.Keyword == validator.SyntaxConf.Tokens.KeyWords.Charset && !allowedCharsets.Contains(entry.Value)) || - (entry.Key.Keyword == validator.SyntaxConf.Tokens.KeyWords.CodePage && !entry.Value.Equals(validator._encoding.BodyName, StringComparison.OrdinalIgnoreCase)) || - (entry.Key.Keyword == validator.SyntaxConf.Tokens.KeyWords.DimensionType && !dimensionTypes.Contains(entry.Value))) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - entry.KeyStartLineIndex, - entry.ValueStartIndex, - entry.LineChangeIndexes); - - ValidationFeedback feedback = new( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.InvalidValueFound, - feedbackIndexes.Key, - feedbackIndexes.Value, - $"{entry.Key.Keyword}: {entry.Value}"); - - return [ - new ValidationFeedbackItem( - entry, - feedback - ) - ]; - } - else if (entry.Key.Keyword == validator.SyntaxConf.Tokens.KeyWords.ContentVariableIdentifier) - { - string defaultLanguage = validator._defaultLanguage ?? string.Empty; - string lang = entry.Key.Language ?? defaultLanguage; - if (validator._stubDimensionNames is not null && !Array.Exists(validator._stubDimensionNames[lang], d => d == entry.Value) && - (validator._headingDimensionNames is not null && !Array.Exists(validator._headingDimensionNames[lang], d => d == entry.Value))) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - entry.KeyStartLineIndex, - entry.ValueStartIndex, - entry.LineChangeIndexes); - - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.InvalidValueFound, - feedbackIndexes.Key, - feedbackIndexes.Value, - $"{entry.Key.Keyword}: {entry.Value}"); - - return [ - new ValidationFeedbackItem( - entry, - feedback - ) - ]; - } - } - - return null; - } - - /// - /// Validates that entries with specific keywords have the correct amount of values in the Px file metadata entry - /// - /// Entry in the Px file metadata. Represented by a object - /// object that stores information that is gathered during the validation process - /// object is returned if an unexpected amount of values is detected - public static ValidationFeedbackItem[]? ValidateValueAmounts(ValidationStructuredEntry entry, ContentValidator validator) - { - if (entry.Key.Keyword != validator.SyntaxConf.Tokens.KeyWords.VariableValueCodes || - validator._dimensionValueNames is null || - entry.Key.FirstSpecifier is null) - { - return null; - } - - string[] codes = entry.Value.Split(validator.SyntaxConf.Symbols.Value.ListSeparator); - string defaultLanguage = validator._defaultLanguage ?? string.Empty; - string lang = entry.Key.Language ?? defaultLanguage; - if (codes.Length != validator._dimensionValueNames[new(lang, entry.Key.FirstSpecifier)].Length) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - entry.KeyStartLineIndex, - entry.ValueStartIndex, - entry.LineChangeIndexes); - - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.UnmatchingValueAmount, - feedbackIndexes.Key, - feedbackIndexes.Value, - $"{entry.Key.Keyword}: {entry.Value}"); - - return [ - new ValidationFeedbackItem( - entry, - feedback - ) - ]; - } - - return null; - } - - /// - /// Validates that entries with specific keywords have their values written in upper case - /// - /// Entry in the Px file metadata. Represented by a object - /// object that stores information that is gathered during the validation process - /// object with warning is returned if the found value is not written in upper case - public static ValidationFeedbackItem[]? ValidateValueUppercaseRecommendations(ValidationStructuredEntry entry, ContentValidator validator) - { - string[] recommendedUppercaseValueKeywords = [ - validator.SyntaxConf.Tokens.KeyWords.CodePage - ]; - - if (!recommendedUppercaseValueKeywords.Contains(entry.Key.Keyword)) - { - return null; - } - - // Check if entry.value is in upper case - string valueUppercase = entry.Value.ToUpper(CultureInfo.InvariantCulture); - if (entry.Value != valueUppercase) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - entry.KeyStartLineIndex, - entry.ValueStartIndex, - entry.LineChangeIndexes); - - ValidationFeedback feedback = new ( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.ValueIsNotInUpperCase, - feedbackIndexes.Key, - entry.ValueStartIndex, - $"{entry.Key.Keyword}: {entry.Value}"); - - return [ - new ValidationFeedbackItem( - entry, - feedback - ) - ]; - } - return null; - } - } -} diff --git a/Px.Utils/Validation/ContentValidation/ContentValidator.cs b/Px.Utils/Validation/ContentValidation/ContentValidator.cs index 50d1b9aa..d7ecab7d 100644 --- a/Px.Utils/Validation/ContentValidation/ContentValidator.cs +++ b/Px.Utils/Validation/ContentValidation/ContentValidator.cs @@ -1,4 +1,5 @@ using Px.Utils.PxFile; +using Px.Utils.Validation.DatabaseValidation; using Px.Utils.Validation.SyntaxValidation; using System.Text; @@ -17,7 +18,7 @@ public sealed partial class ContentValidator( Encoding encoding, ValidationStructuredEntry[] entries, CustomContentValidationFunctions? customContentValidationFunctions = null, - PxFileSyntaxConf? syntaxConf = null) : IPxFileValidator + PxFileSyntaxConf? syntaxConf = null) : IValidator { private readonly string _filename = filename; private readonly Encoding _encoding = encoding; @@ -56,7 +57,6 @@ public sealed partial class ContentValidator( /// object that contains the feedback gathered during the validation process. public ContentValidationResult Validate() { - IEnumerable contentValidationEntryFunctions = DefaultContentValidationEntryFunctions; IEnumerable contentValidationFindKeywordFunctions = DefaultContentValidationFindKeywordFunctions; @@ -66,11 +66,11 @@ public ContentValidationResult Validate() contentValidationFindKeywordFunctions = contentValidationFindKeywordFunctions.Concat(customContentValidationFunctions.CustomContentValidationFindKeywordFunctions); } - List feedbackItems = []; + ValidationFeedback feedbackItems = []; foreach (ContentValidationFindKeywordValidator findingFunction in contentValidationFindKeywordFunctions) { - ValidationFeedbackItem[]? feedback = findingFunction(entries, this); + ValidationFeedback? feedback = findingFunction(entries, this); if (feedback is not null) { feedbackItems.AddRange(feedback); @@ -80,43 +80,41 @@ public ContentValidationResult Validate() { foreach (ValidationStructuredEntry entry in entries) { - ValidationFeedbackItem[]? feedback = entryFunction(entry, this); + ValidationFeedback? feedback = entryFunction(entry, this); if (feedback is not null) { feedbackItems.AddRange(feedback); } } } - int lengthOfDataRows = _headingDimensionNames is not null ? GetProductOfDimensionValues(_headingDimensionNames) : 0; int amountOfDataRows = _stubDimensionNames is not null ? GetProductOfDimensionValues(_stubDimensionNames) : 0; - ResetFields(); - return new ContentValidationResult([.. feedbackItems], lengthOfDataRows, amountOfDataRows); + return new ContentValidationResult(feedbackItems, lengthOfDataRows, amountOfDataRows); } #region Interface implementation - ValidationResult IPxFileValidator.Validate() + ValidationResult IValidator.Validate() => Validate(); #endregion private int GetProductOfDimensionValues(Dictionary dimensions) { - string? lang = _defaultLanguage ?? _availableLanguages?[0]; - if (lang is null) + string? lang = _defaultLanguage ?? _availableLanguages?[0] ?? string.Empty; + if (lang is null || dimensions.Count == 0) { return 0; } - string[] headingDimensionNames = dimensions[lang]; - if (headingDimensionNames is null || headingDimensionNames.Length == 0 || _dimensionValueNames is null) + string[] dimensionNames = dimensions[lang]; + if (dimensionNames is null || dimensionNames.Length == 0 || _dimensionValueNames is null || _dimensionValueNames.Count == 0) { return 0; } return _dimensionValueNames - .Where(kvp => headingDimensionNames + .Where(kvp => dimensionNames .Contains(kvp.Key.Value)).Select(kvp => kvp.Value.Length) .Aggregate((a, b) => a * b); } diff --git a/Px.Utils/Validation/DataValidation/DataValidator.cs b/Px.Utils/Validation/DataValidation/DataValidator.cs index 005e8133..542b75d1 100644 --- a/Px.Utils/Validation/DataValidation/DataValidator.cs +++ b/Px.Utils/Validation/DataValidation/DataValidator.cs @@ -1,24 +1,21 @@ using System.Text; using Px.Utils.PxFile; +using Px.Utils.PxFile.Metadata; +using Px.Utils.Validation.DatabaseValidation; namespace Px.Utils.Validation.DataValidation { /// /// The DataValidator class is used to validate the data section of a Px file. - /// Px file stream to be validated /// Length of one row of Px file data /// Amount of rows of Px file data - /// Name of the file being validated /// The row number where the data section starts - /// Encoding of the stream /// Syntax configuration for the Px file /// - public class DataValidator(Stream stream, int rowLen, int numOfRows, string filename, - int startRow, Encoding? streamEncoding, PxFileSyntaxConf? conf = null) : IPxFileValidator, IPxFileValidatorAsync + public class DataValidator(int rowLen, int numOfRows, int startRow, PxFileSyntaxConf? conf = null) : IPxFileStreamValidator, IPxFileStreamValidatorAsync { private const int _streamBufferSize = 4096; - private readonly Encoding _encoding = streamEncoding ?? Encoding.Default; private readonly PxFileSyntaxConf _conf = conf ?? PxFileSyntaxConf.Default; private readonly List _commonValidators = []; @@ -33,52 +30,98 @@ public class DataValidator(Stream stream, int rowLen, int numOfRows, string file private int _charPosition; private EntryType _currentCharacterType; private long _currentRowLength; + private Encoding _encoding = Encoding.Default; + private string _filename = string.Empty; /// /// Validates the data in the stream according to the specified parameters and returns a collection of validation feedback items. /// Assumes that the stream is at the start of the data section (after 'DATA='-keyword) at the first data item. /// + /// Px file stream to be validated + /// Name of the file being validated + /// Encoding of the stream. If not provided, validator tries to find encoding. + /// File system used for file operations. If not provided, default file system is used. /// /// object that contains a collection of - /// ValidationFeedbackItem objects representing the feedback for the data validation. + /// validation feedback key value pairs representing the feedback for the data validation. /// - public ValidationResult Validate() + public ValidationResult Validate( + Stream stream, + string filename, + Encoding? encoding = null, + IFileSystem? fileSystem = null) { - SetValidationParameters(); + fileSystem ??= new LocalFileSystem(); + encoding ??= fileSystem.GetEncoding(stream); + SetValidationParameters(encoding, filename); - List validationFeedbacks = []; - stream.Position = GetStreamIndexOfFirstDataValue(ref validationFeedbacks); - ValidationFeedbackItem[] dataStreamFeedbacks = ValidateDataStream(stream); + ValidationFeedback validationFeedbacks = []; + int dataStartIndex = GetStreamIndexOfFirstDataValue(stream, ref validationFeedbacks); + if (dataStartIndex == -1) + { + KeyValuePair feedback = + new(new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.StartOfDataSectionNotFound), + new(filename, 0, 0)); + validationFeedbacks.Add(feedback); + + return new (validationFeedbacks); + } + stream.Position = dataStartIndex; + ValidationFeedback dataStreamFeedbacks = ValidateDataStream(stream); validationFeedbacks.AddRange(dataStreamFeedbacks); ResetValidator(); - return new ValidationResult([..validationFeedbacks]); + return new (validationFeedbacks); } /// /// Validates the data in the specified stream asynchronously. /// Assumes that the stream is at the start of the data section (after 'DATA='-keyword) at the first data item. /// + /// Px file stream to be validated + /// Encoding of the stream + /// Name of the file being validated. If not provided, validator tries to find the encoding. + /// File system used for file operations. If not provided, default file system is used. + /// Cancellation token for cancelling the validation process /// object that contains a collection of - /// ValidationFeedbackItem objects representing the feedback for the data validation. + /// validation feedback key value pairs representing the feedback for the data validation. /// - public async Task ValidateAsync(CancellationToken cancellationToken = default) + public async Task ValidateAsync( + Stream stream, + string filename, + Encoding? encoding = null, + IFileSystem? fileSystem = null, + CancellationToken cancellationToken = default) { - SetValidationParameters(); + fileSystem ??= new LocalFileSystem(); + encoding ??= await fileSystem.GetEncodingAsync(stream, cancellationToken); + SetValidationParameters(encoding, filename); - List validationFeedbacks = []; - stream.Position = GetStreamIndexOfFirstDataValue(ref validationFeedbacks); - ValidationFeedbackItem[] dataStreamFeedbacks = await Task.Factory.StartNew(() => + ValidationFeedback validationFeedbacks = []; + int dataStartIndex = GetStreamIndexOfFirstDataValue(stream, ref validationFeedbacks); + if (dataStartIndex == -1) + { + KeyValuePair feedback = + new(new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.StartOfDataSectionNotFound), + new(filename, 0, 0)); + validationFeedbacks.Add(feedback); + + return new (validationFeedbacks); + } + stream.Position = dataStartIndex; + ValidationFeedback dataStreamFeedbacks = await Task.Factory.StartNew(() => ValidateDataStream(stream, cancellationToken), cancellationToken); validationFeedbacks.AddRange(dataStreamFeedbacks); ResetValidator(); - return new ValidationResult([.. validationFeedbacks]); + return new (validationFeedbacks); } - private void SetValidationParameters() + private void SetValidationParameters(Encoding encoding, string filename) { _commonValidators.Add(new DataStructureValidator()); _dataNumValidators.AddRange(_commonValidators); @@ -87,21 +130,13 @@ private void SetValidationParameters() _dataStringValidators.Add(new DataStringValidator()); _dataSeparatorValidators.AddRange(_commonValidators); _dataSeparatorValidators.Add(new DataSeparatorValidator()); + _encoding = encoding; + _filename = filename; } - #region Interface implementation - - ValidationResult IPxFileValidator.Validate() - => Validate(); - - async Task IPxFileValidatorAsync.ValidateAsync(CancellationToken cancellationToken) - => await ValidateAsync(cancellationToken); - - #endregion - - private ValidationFeedbackItem[] ValidateDataStream(Stream stream, CancellationToken? cancellationToken = null) + private ValidationFeedback ValidateDataStream(Stream stream, CancellationToken? cancellationToken = null) { - List validationFeedbacks = []; + ValidationFeedback validationFeedbacks = []; byte endOfData = (byte)_conf.Symbols.EntrySeparator; _stringDelimeter = (byte)_conf.Symbols.Value.StringDelimeter; _currentEntry = new(_streamBufferSize); @@ -142,26 +177,23 @@ private ValidationFeedbackItem[] ValidateDataStream(Stream stream, CancellationT if (numOfRows != _lineNumber - 1) { validationFeedbacks.Add(new( - new(filename, _lineNumber + startRow, []), - new( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.DataValidationFeedbackInvalidRowCount, - _lineNumber + startRow, - _charPosition, - $" Expected {numOfRows} rows, got {_lineNumber - 1} rows." - ))); + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.DataValidationFeedbackInvalidRowCount), + new(_filename, _lineNumber + startRow, _charPosition, $" Expected {numOfRows} rows, got {_lineNumber - 1} rows.")) + ); } - return [..validationFeedbacks]; + return validationFeedbacks; } - private void HandleEntryTypeChange(ref List validationFeedbacks) + private void HandleEntryTypeChange(ref ValidationFeedback validationFeedbacks) { if (_currentEntryType == EntryType.Unknown && (_lineNumber > 1 || _charPosition > 0)) { validationFeedbacks.Add(new( - new(filename, _lineNumber + startRow, []), - new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidChar, _lineNumber + startRow, _charPosition))); + new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidChar), + new(_filename, _lineNumber + startRow, _charPosition)) + ); } else { @@ -174,18 +206,22 @@ private void HandleEntryTypeChange(ref List validationFe foreach (IDataValidator validator in validators) { - ValidationFeedback? feedback = validator.Validate(_currentEntry, _currentEntryType, _encoding, _lineNumber + startRow, _charPosition); + KeyValuePair? feedback = validator.Validate( + _currentEntry, + _currentEntryType, + _encoding, + _lineNumber + startRow, + _charPosition, + _filename); if (feedback is not null) { - validationFeedbacks.Add(new - (new(filename, _lineNumber + startRow, []), - (ValidationFeedback)feedback)); + validationFeedbacks.Add((KeyValuePair)feedback); } } } } - private void HandleNonSeparatorType(ref List validationFeedbacks) + private void HandleNonSeparatorType(ref ValidationFeedback validationFeedbacks) { if (_currentCharacterType == EntryType.DataItem) { @@ -196,11 +232,11 @@ private void HandleNonSeparatorType(ref List validationF if (_currentRowLength != rowLen) { validationFeedbacks.Add(new( - new(filename, _lineNumber + startRow, []), - new ValidationFeedback(ValidationFeedbackLevel.Error, - ValidationFeedbackRule.DataValidationFeedbackInvalidRowLength, - _lineNumber + startRow, - _charPosition))); + new (ValidationFeedbackLevel.Error, + ValidationFeedbackRule.DataValidationFeedbackInvalidRowLength), + new(_filename, _lineNumber + startRow, _charPosition, + $"Expected {rowLen}, got row length of {_currentRowLength}.")) + ); } _lineNumber++; _currentRowLength = 0; @@ -221,7 +257,7 @@ private void ResetValidator() _currentRowLength = 0; } - private int GetStreamIndexOfFirstDataValue(ref List feedbacks) + private int GetStreamIndexOfFirstDataValue(Stream stream, ref ValidationFeedback feedbacks) { byte[] buffer = new byte[_streamBufferSize]; int bytesRead; @@ -236,12 +272,11 @@ private int GetStreamIndexOfFirstDataValue(ref List feed } else if (!CharacterConstants.WhitespaceCharacters.Contains((char)buffer[i])) { - feedbacks.Add(new ValidationFeedbackItem( - new(filename, _lineNumber + startRow, []), + feedbacks.Add(new( new(ValidationFeedbackLevel.Error, - ValidationFeedbackRule.DataValidationFeedbackInvalidChar, - _lineNumber + startRow, - _charPosition))); + ValidationFeedbackRule.DataValidationFeedbackInvalidChar), + new(_filename, _lineNumber + startRow, _charPosition)) + ); } } } while (bytesRead > 0); @@ -264,6 +299,6 @@ public enum EntryType internal interface IDataValidator { - internal ValidationFeedback? Validate(List entry, EntryType entryType, Encoding encoding, int lineNumber, int charPos); + internal KeyValuePair? Validate(List entry, EntryType entryType, Encoding encoding, int lineNumber, int charPos, string filename); } } \ No newline at end of file diff --git a/Px.Utils/Validation/DataValidation/DataValidatorFunctions.cs b/Px.Utils/Validation/DataValidation/DataValidatorFunctions.cs index c410d49d..51370520 100644 --- a/Px.Utils/Validation/DataValidation/DataValidatorFunctions.cs +++ b/Px.Utils/Validation/DataValidation/DataValidatorFunctions.cs @@ -25,14 +25,15 @@ public class DataStringValidator : IDataValidator /// Encoding format of the Px file. /// Line number for the validation item. /// Represents the position relative to the line for the validation item. - /// object if the entry is not a missing value string sequence, otherwise null. - public ValidationFeedback? Validate(List entry, EntryType entryType, Encoding encoding, int lineNumber, int charPos) + /// Key value pair containing information about the rule violation if the entry is not a missing value string sequence, otherwise null. + public KeyValuePair? Validate(List entry, EntryType entryType, Encoding encoding, int lineNumber, int charPos, string filename) { string value = encoding.GetString(entry.ToArray()); if (!ValidStringDataItems.Contains(value)) { - return new ValidationFeedback(ValidationFeedbackLevel.Error, - ValidationFeedbackRule.DataValidationFeedbackInvalidString, lineNumber, charPos, $"{value}"); + return new( + new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidString), + new(filename, lineNumber, charPos, $"{value}")); } else { @@ -55,17 +56,15 @@ public class DataNumberValidator : IDataValidator /// Encoding format of the Px file. /// Line number for the validation item. /// Represents the position relative to the line for the validation item. - /// object if the entry is not a valid number, otherwise null. - public ValidationFeedback?Validate(List entry, EntryType entryType, Encoding encoding, int lineNumber, int charPos) + /// Key value pair containing information about the rule violation if the entry is not a valid number, otherwise null. + public KeyValuePair? Validate(List entry, EntryType entryType, Encoding encoding, int lineNumber, int charPos, string filename) { if (entry.Count >= MaxLength && !decimal.TryParse(entry.ToArray(), out _)) { - return new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.DataValidationFeedbackInvalidNumber, - lineNumber, - charPos, - encoding.GetString(entry.ToArray())); + return new( + new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidNumber), + new(filename, lineNumber, charPos, encoding.GetString(entry.ToArray())) + ); } int decimalSeparatorIndex = entry.IndexOf(0x2E); @@ -73,12 +72,10 @@ public class DataNumberValidator : IDataValidator { if (!IsValidIntegerPart(entry, true)) { - return new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.DataValidationFeedbackInvalidNumber, - lineNumber, - charPos, - encoding.GetString(entry.ToArray())); + return new( + new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidNumber), + new(filename, lineNumber, charPos, encoding.GetString(entry.ToArray())) + ); } else { @@ -87,12 +84,11 @@ public class DataNumberValidator : IDataValidator } else if (decimalSeparatorIndex == 0 || !IsValidIntegerPart(entry[0..decimalSeparatorIndex], false) || !IsValidDecimalPart(entry[decimalSeparatorIndex..])) { - return new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.DataValidationFeedbackInvalidNumber, - lineNumber, - charPos, - encoding.GetString(entry.ToArray())); + + return new( + new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidNumber), + new(filename, lineNumber, charPos, encoding.GetString(entry.ToArray())) + ); } else { @@ -103,7 +99,6 @@ public class DataNumberValidator : IDataValidator private static bool IsValidIntegerPart(List entry, bool isInteger) { bool isNegative = entry[0] == 0x2D; - bool startsWithZero = isNegative ? entry[1] == zero : entry[0] == zero; List digits = isNegative ? entry.Skip(1).ToList() : entry; if (digits.Count > 1) { @@ -111,12 +106,13 @@ private static bool IsValidIntegerPart(List entry, bool isInteger) { return false; } + bool startsWithZero = isNegative ? entry[1] == zero : entry[0] == zero; if (startsWithZero && isInteger) { return false; } } - if (isNegative && isInteger && digits[0] == zero) + if (isNegative && isInteger && digits.Count > 0 && digits[0] == zero) { return false; } @@ -142,8 +138,8 @@ public class DataSeparatorValidator : IDataValidator /// Encoding format of the Px file. /// Line number for the validation item. /// Represents the position relative to the line for the validation item. - /// object if the entry is not a valid item separator, otherwise null. - public ValidationFeedback? Validate(List entry, EntryType entryType, Encoding encoding, int lineNumber, int charPos) + /// Key value pair containing information about the rule violation if the entry is not a valid item separator, otherwise null. + public KeyValuePair? Validate(List entry, EntryType entryType, Encoding encoding, int lineNumber, int charPos, string filename) { if (_separator == entry[0]) { @@ -155,11 +151,10 @@ public class DataSeparatorValidator : IDataValidator return null; } - return new ValidationFeedback( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.DataValidationFeedbackInconsistentSeparator, - lineNumber, - charPos); + return new( + new(ValidationFeedbackLevel.Warning, ValidationFeedbackRule.DataValidationFeedbackInconsistentSeparator), + new(filename, lineNumber, charPos) + ); } private const byte Sentinel = 0x0; @@ -187,8 +182,8 @@ public class DataStructureValidator : IDataValidator /// Line number for the validation item. /// Represents the position relative to the line for the validation item. /// Reference to a list of feedback items to which any validation feedback is added to. - /// object if the entry sequence is invalid. Otherwise null. - public ValidationFeedback? Validate(List entry, EntryType entryType, Encoding encoding, int lineNumber, int charPos) + /// Key value pair containing information about the rule violation if the entry sequence is invalid. Otherwise null. + public KeyValuePair? Validate(List entry, EntryType entryType, Encoding encoding, int lineNumber, int charPos, string filename) { if (_allowedPreviousTokens[entryType].Contains(_previousTokenType)) { @@ -199,12 +194,10 @@ public class DataStructureValidator : IDataValidator string additionalInfo = $"{_previousTokenType},{entryType}"; _previousTokenType = entryType; - return new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.DataValidationFeedbackInvalidStructure, - lineNumber, - charPos, - additionalInfo); + return new( + new(ValidationFeedbackLevel.Error, ValidationFeedbackRule.DataValidationFeedbackInvalidStructure), + new(filename, lineNumber, charPos, additionalInfo) + ); } } } \ No newline at end of file diff --git a/Px.Utils/Validation/DatabaseValidation/DatabaseValidator.cs b/Px.Utils/Validation/DatabaseValidation/DatabaseValidator.cs new file mode 100644 index 00000000..8eb654cf --- /dev/null +++ b/Px.Utils/Validation/DatabaseValidation/DatabaseValidator.cs @@ -0,0 +1,430 @@ +using Px.Utils.Exceptions; +using Px.Utils.PxFile; +using Px.Utils.PxFile.Metadata; +using Px.Utils.Validation.SyntaxValidation; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Px.Utils.Validation.DatabaseValidation +{ + /// + /// Validates a whole px file database including all px files it contains. + /// + /// Path to the database root directory + /// object that defines the file system used for the validation process. + /// object that defines the tokens and symbols for px file syntax. + /// Optional custom validator functions ran for each px file within the database + /// Optional custom validator functions that are ran for each alias file within the database + /// Optional custom validator functions for each subdirectory within the database. + public class DatabaseValidator( + string directoryPath, + IFileSystem fileSystem, + PxFileSyntaxConf? syntaxConf = null, + IDatabaseValidator[]? customPxFileValidators = null, + IDatabaseValidator[]? customAliasFileValidators = null, + IDatabaseValidator[]? customDirectoryValidators = null + ) : IValidator, IValidatorAsync + { + private readonly string _directoryPath = directoryPath; + private readonly PxFileSyntaxConf _syntaxConf = syntaxConf is not null ? syntaxConf : PxFileSyntaxConf.Default; + private readonly IDatabaseValidator[]? _customPxFileValidators = customPxFileValidators; + private readonly IDatabaseValidator[]? _customAliasFileValidators = customAliasFileValidators; + private readonly IDatabaseValidator[]? _customDirectoryValidators = customDirectoryValidators; + private readonly IFileSystem _fileSystem = fileSystem is not null ? fileSystem : new LocalFileSystem(); + + /// + /// Blocking px file database validation process. + /// + /// object that contains feedback gathered during the validation process. + public ValidationResult Validate() + { + ValidationFeedback feedbacks = []; + ConcurrentBag pxFiles = []; + ConcurrentBag aliasFiles = []; + List fileTasks = []; + + IEnumerable pxFilePaths = _fileSystem.EnumerateFiles(_directoryPath, "*.px"); + foreach (string fileName in pxFilePaths) + { + fileTasks.Add(Task.Run(() => + { + (DatabaseFileInfo? file, ValidationFeedback feedback) = ProcessPxFile(fileName); + if (file != null) pxFiles.Add(file); + feedbacks.AddRange(feedback); + })); + } + + IEnumerable aliasFilePaths = _fileSystem.EnumerateFiles(_directoryPath, "Alias_*.txt"); + foreach (string fileName in aliasFilePaths) + { + fileTasks.Add(Task.Run(() => + { + DatabaseFileInfo file = ProcessAliasFile(fileName); + aliasFiles.Add(file); + })); + } + + Task.WaitAll([.. fileTasks]); + feedbacks.AddRange(ValidateDatabaseContents(pxFiles, aliasFiles)); + return new (feedbacks); + } + + /// + /// Asynchronous process for validating a px file database. + /// + /// Optional cancellation token + /// object that contains feedback gathered during the validation process. + public async Task ValidateAsync(CancellationToken cancellationToken = default) + { + ValidationFeedback feedbacks = []; + ConcurrentBag pxFiles = []; + ConcurrentBag aliasFiles = []; + List fileTasks = []; + + IEnumerable pxFilePaths = _fileSystem.EnumerateFiles(_directoryPath, "*.px"); + foreach (string fileName in pxFilePaths) + { + fileTasks.Add(Task.Run(async () => + { + (DatabaseFileInfo? file, ValidationFeedback feedback) = await ProcessPxFileAsync(fileName, cancellationToken); + if (file != null) pxFiles.Add(file); + feedbacks.AddRange(feedback); + }, cancellationToken)); + } + + IEnumerable aliasFilePaths = _fileSystem.EnumerateFiles(_directoryPath, "Alias_*.txt"); + foreach (string fileName in aliasFilePaths) + { + fileTasks.Add(Task.Run(async () => + { + DatabaseFileInfo file = await ProcessAliasFileAsync(fileName, cancellationToken); + aliasFiles.Add(file); + }, cancellationToken)); + } + + await Task.WhenAll(fileTasks); + feedbacks.AddRange(ValidateDatabaseContents(pxFiles, aliasFiles)); + return new (feedbacks); + } + + private (DatabaseFileInfo?, ValidationFeedback) ProcessPxFile(string fileName) + { + ValidationFeedback feedbacks = []; + using Stream stream = _fileSystem.GetFileStream(fileName); + (DatabaseFileInfo? fileInfo, KeyValuePair? feedback) = GetPxFileInfo(fileName, stream); + if (fileInfo == null) + { + if (feedback != null) feedbacks.Add((KeyValuePair)feedback); + return (null, feedbacks); + } + stream.Position = 0; + PxFileValidator validator = new(_syntaxConf); + feedbacks.AddRange(validator.Validate(stream, fileName, fileInfo.Encoding).FeedbackItems); + return (fileInfo, feedbacks); + } + + private DatabaseFileInfo ProcessAliasFile(string fileName) + { + using Stream stream = _fileSystem.GetFileStream(fileName); + return GetAliasFileInfo(fileName, stream); + } + + private async Task<(DatabaseFileInfo?, ValidationFeedback)> ProcessPxFileAsync(string fileName, CancellationToken cancellationToken) + { + ValidationFeedback feedbacks = []; + using Stream stream = _fileSystem.GetFileStream(fileName); + (DatabaseFileInfo? fileInfo, KeyValuePair? feedback) = await GetPxFileInfoAsync(fileName, stream, cancellationToken); + if (fileInfo == null) + { + if (feedback != null) feedbacks.Add((KeyValuePair)feedback); + return (null, feedbacks); + } + stream.Position = 0; + PxFileValidator validator = new(_syntaxConf); + ValidationResult result = await validator.ValidateAsync(stream, fileName, fileInfo.Encoding, cancellationToken: cancellationToken); + feedbacks.AddRange(result.FeedbackItems); + cancellationToken.ThrowIfCancellationRequested(); + return (fileInfo, feedbacks); + } + + private async Task ProcessAliasFileAsync(string fileName, CancellationToken cancellationToken) + { + using Stream stream = _fileSystem.GetFileStream(fileName); + cancellationToken.ThrowIfCancellationRequested(); + return await GetAliasFileInfoAsync(fileName, stream, cancellationToken); + } + + private ValidationFeedback ValidateDatabaseContents(ConcurrentBag pxFiles, ConcurrentBag aliasFiles) + { + ValidationFeedback feedbacks = []; + IEnumerable allFiles = pxFiles.Concat(aliasFiles); + IEnumerable databaseLanguages = pxFiles.SelectMany(file => file.Languages).Distinct(); + Encoding mostCommonEncoding = allFiles.Select(file => file.Encoding) + .GroupBy(enc => enc) + .OrderByDescending(group => group.Count()) + .First() + .Key; + + feedbacks.AddRange(ValidatePxFiles(databaseLanguages, mostCommonEncoding, pxFiles)); + feedbacks.AddRange(ValidateAliasFiles(mostCommonEncoding, aliasFiles)); + feedbacks.AddRange(ValidateDirectories(databaseLanguages, aliasFiles)); + return feedbacks; + } + + private ValidationFeedback ValidatePxFiles( + IEnumerable databaseLanguages, + Encoding mostCommonEncoding, + ConcurrentBag pxFiles) + { + ValidationFeedback feedbacks = []; + IDatabaseValidator[] pxFileValidators = + [ + new DuplicatePxFileName([.. pxFiles]), + new MissingPxFileLanguages(databaseLanguages), + new MismatchingEncoding(mostCommonEncoding), + ]; + if (_customPxFileValidators is not null) + { + pxFileValidators = [.. pxFileValidators, .. _customPxFileValidators]; + } + + foreach (DatabaseFileInfo fileInfo in pxFiles) + { + foreach (IDatabaseValidator validator in pxFileValidators) + { + KeyValuePair? feedback = validator.Validate(fileInfo); + if (feedback is not null) + { + feedbacks.Add((KeyValuePair)feedback); + } + } + } + return feedbacks; + } + + private ValidationFeedback ValidateAliasFiles(Encoding mostCommonEncoding, ConcurrentBag aliasFiles) + { + ValidationFeedback feedbacks = []; + IDatabaseValidator[] aliasFileValidators = + [ + new MismatchingEncoding(mostCommonEncoding), + ]; + if (_customAliasFileValidators is not null) + { + aliasFileValidators = [.. aliasFileValidators, .. _customAliasFileValidators]; + } + + foreach (DatabaseFileInfo fileInfo in aliasFiles) + { + foreach (IDatabaseValidator validator in aliasFileValidators) + { + KeyValuePair? feedback = validator.Validate(fileInfo); + if (feedback is not null) + { + feedbacks.Add((KeyValuePair)feedback); + } + } + } + return feedbacks; + } + + private ValidationFeedback ValidateDirectories(IEnumerable databaseLanguages, ConcurrentBag aliasFiles) + { + ValidationFeedback feedbacks = []; + IDatabaseValidator[] directoryValidators = + [ + new MissingAliasFiles([..aliasFiles], databaseLanguages), + ]; + if (_customDirectoryValidators is not null) + { + directoryValidators = [.. directoryValidators, .. _customDirectoryValidators]; + } + + IEnumerable allDirectories = _fileSystem.EnumerateDirectories(_directoryPath); + foreach (string directory in allDirectories) + { + string directoryName = new DirectoryInfo(directory).Name; + if (directoryName == _syntaxConf.Tokens.Database.Index) continue; + + foreach (IDatabaseValidator validator in directoryValidators) + { + KeyValuePair? feedback = validator.Validate(new DatabaseValidationItem(directory)); + if (feedback is not null) + { + feedbacks.Add((KeyValuePair)feedback); + } + } + } + return feedbacks; + } + + private (DatabaseFileInfo?, KeyValuePair?) GetPxFileInfo(string filename, Stream stream) + { + string name = _fileSystem.GetFileName(filename); + string? path = _fileSystem.GetDirectoryName(filename); + string location = path is not null ? path : string.Empty; + string[] languages = []; + PxFileMetadataReader metadataReader = new (); + Encoding encoding; + try + { + encoding = metadataReader.GetEncoding(stream, _syntaxConf); + } + catch (InvalidPxFileMetadataException e) + { + return (null, new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.NoEncoding), + new(filename, additionalInfo: $"Error while reading the encoding of the file {filename}: {e.Message}")) + ); + } + stream.Position = 0; + const int bufferSize = 1024; + bool isProcessingString = false; + using StreamReader streamReader = new(stream, encoding, leaveOpen: true); + StringBuilder entryBuilder = new(); + char[] buffer = new char[bufferSize]; + string defaultLanguage = string.Empty; + + while (languages.Length == 0 && (streamReader.Read(buffer, 0, bufferSize) > 0)) + { + for (int i = 0; i < buffer.Length; i++) + { + ProcessBuffer(buffer[i], ref entryBuilder, ref isProcessingString, ref defaultLanguage, ref languages); + } + } + + DatabaseFileInfo fileInfo = new (name, location, languages, encoding); + return (fileInfo, null); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ProcessBuffer(char character, ref StringBuilder entryBuilder, ref bool isProcessingString, ref string defaultLanguage, ref string[] languages) + { + if (SyntaxValidator.IsEndOfMetadataSection(character, _syntaxConf, entryBuilder, isProcessingString) && defaultLanguage != string.Empty) + { + languages = [defaultLanguage.Trim(_syntaxConf.Symbols.Key.StringDelimeter)]; + } + else if (character == _syntaxConf.Symbols.Key.StringDelimeter) + { + isProcessingString = !isProcessingString; + } + else if (character == _syntaxConf.Symbols.EntrySeparator && !isProcessingString) + { + ProcessEntry(entryBuilder.ToString(), ref defaultLanguage, ref languages); + entryBuilder.Clear(); + } + else + { + entryBuilder.Append(character); + } + } + + private async Task<(DatabaseFileInfo?, KeyValuePair?)> GetPxFileInfoAsync(string filename, Stream stream, CancellationToken cancellationToken) + { + string name = _fileSystem.GetFileName(filename); + string? path = _fileSystem.GetDirectoryName(filename); + string location = path is not null ? path : string.Empty; + string[] languages = []; + PxFileMetadataReader metadataReader = new (); + Encoding encoding; + try + { + encoding = await metadataReader.GetEncodingAsync(stream, _syntaxConf, cancellationToken); + } + catch (InvalidPxFileMetadataException e) + { + return (null, new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.NoEncoding), + new(filename, additionalInfo: $"Error while reading the encoding of the file {filename}: {e.Message}")) + ); + } + stream.Position = 0; + const int bufferSize = 1024; + bool isProcessingString = false; + using StreamReader streamReader = new(stream, encoding, leaveOpen: true); + StringBuilder entryBuilder = new(); + char[] buffer = new char[bufferSize]; + string defaultLanguage = string.Empty; + int read = 0; + do + { + cancellationToken.ThrowIfCancellationRequested(); + read = await streamReader.ReadAsync(buffer.AsMemory(), cancellationToken); + for (int i = 0; i < buffer.Length; i++) + { + ProcessBuffer(buffer[i], ref entryBuilder, ref isProcessingString, ref defaultLanguage, ref languages); + } + } while (languages.Length == 0 && read > 0); + + DatabaseFileInfo fileInfo = new (name, location, languages, encoding); + return (fileInfo, null); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ProcessEntry(string entry, ref string defaultLanguage, ref string[] languages) + { + string[] splitEntry = entry.Trim().Split(_syntaxConf.Symbols.KeywordSeparator); + if (splitEntry[0] == _syntaxConf.Tokens.KeyWords.DefaultLanguage) + { + defaultLanguage = splitEntry[1]; + } + else if (splitEntry[0] == _syntaxConf.Tokens.KeyWords.AvailableLanguages) + { + languages = splitEntry[1].Split(_syntaxConf.Symbols.Value.ListSeparator); + for (int j = 0; j < languages.Length; j++) + { + languages[j] = languages[j].Trim(_syntaxConf.Symbols.Key.StringDelimeter); + } + } + } + + private DatabaseFileInfo GetAliasFileInfo(string filename, Stream stream) + { + string name = _fileSystem.GetFileName(filename); + string? path = _fileSystem.GetDirectoryName(filename); + string location = path is not null ? path : string.Empty; + string[] languages = [ + name.Split(_syntaxConf.Tokens.Database.LanguageSeparator)[1].Split('.')[0] + ]; + + Encoding encoding = _fileSystem.GetEncoding(stream); + DatabaseFileInfo fileInfo = new (name, location, languages, encoding); + return fileInfo; + } + + private async Task GetAliasFileInfoAsync(string filename, Stream stream, CancellationToken cancellationToken) + { + string name = _fileSystem.GetFileName(filename); + string? path = _fileSystem.GetDirectoryName(filename); + string location = path is not null ? path : string.Empty; + string[] languages = [ + name.Split(_syntaxConf.Tokens.Database.LanguageSeparator)[1].Split('.')[0] + ]; + + Encoding encoding = await _fileSystem.GetEncodingAsync(stream, cancellationToken); + DatabaseFileInfo fileInfo = new (name, location, languages, encoding); + return fileInfo; + } + } + + public class DatabaseValidationItem(string path) + { + public string Path { get; } = path; + } + + public class DatabaseFileInfo(string name, string location, string[] languages, Encoding encoding) : DatabaseValidationItem(name) + { + public string Name { get; } = name; + public string Location { get; } = location; + public string[] Languages { get; } = languages; + public Encoding Encoding { get; } = encoding; + } + + public interface IDatabaseValidator + { + public KeyValuePair? Validate(DatabaseValidationItem item); + } +} diff --git a/Px.Utils/Validation/DatabaseValidation/DatabaseValidatorFunctions.cs b/Px.Utils/Validation/DatabaseValidation/DatabaseValidatorFunctions.cs new file mode 100644 index 00000000..c6b1fc12 --- /dev/null +++ b/Px.Utils/Validation/DatabaseValidation/DatabaseValidatorFunctions.cs @@ -0,0 +1,131 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace Px.Utils.Validation.DatabaseValidation +{ + /// + /// Validates that each px file within the database has a unique filename + /// + /// + public class DuplicatePxFileName(List pxFiles) : IDatabaseValidator + { + private readonly List _pxFiles = pxFiles; + + /// + /// Validation function that checks if the filename of the given file is unique within the database + /// + /// object - in this case subject to validation + /// Null if no issues are found, a key value pair containing information about rule violation in case there are multiple files with the same filename. + public KeyValuePair? Validate(DatabaseValidationItem item) + { + if (item is DatabaseFileInfo fileInfo && + _pxFiles.Exists(file => file.Name == fileInfo.Name && file != fileInfo)) + { + return new( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.DuplicateFileNames), + new(fileInfo.Name) + ); + } + else + { + return null; + } + } + } + + /// + /// Validates that each px file within the database contains the same languages + /// + /// Codes of all languages that should be present in each px file + public class MissingPxFileLanguages(IEnumerable allLanguages) : IDatabaseValidator + { + private readonly IEnumerable _allLanguages = allLanguages; + + /// + /// Validation function that checks if the given file contains all the languages that should be present in each px file + /// + /// object - in this case subject to validation + /// Null if no issues are found, a key value pair containing information about rule violation> in case some languages are missing + public KeyValuePair? Validate(DatabaseValidationItem item) + { + if (item is DatabaseFileInfo fileInfo && + !_allLanguages.All(lang => fileInfo.Languages.Contains(lang))) + { + return new( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.FileLanguageDiffersFromDatabase), + new(fileInfo.Name, additionalInfo: $"Missing languages: {string.Join(", ", _allLanguages.Except(fileInfo.Languages))}") + ); + } + else + { + return null; + } + } + } + + /// + /// Validates that the encoding of each px and alias file within the database is consistent + /// + /// The most commonly used encoding within the database + public class MismatchingEncoding(Encoding mostCommonEncoding) : IDatabaseValidator + { + private readonly Encoding _mostCommonEncoding = mostCommonEncoding; + + /// + /// Validation function that checks if the encoding of the given file is consistent with the most commonly used encoding + /// + /// object - in this case subject to validation + /// Null if no issues are found, a key value pair containing information about rule violation> in case the encoding is inconsistent + public KeyValuePair? Validate(DatabaseValidationItem item) + { + if (item is DatabaseFileInfo fileInfo && + fileInfo.Encoding != _mostCommonEncoding) + { + return new ( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.FileEncodingDiffersFromDatabase), + new(fileInfo.Name, additionalInfo: $"Inconsistent encoding: {fileInfo.Encoding.EncodingName}. " + + $"Most commonly used encoding is {_mostCommonEncoding.EncodingName}")); + } + else + { + return null; + } + } + } + + /// + /// Validates that each subdirectory within the database has an alias file for each language + /// + /// List of alias files within the database + /// Codes of all languages that should be present in each alias file + public class MissingAliasFiles(List aliasFiles, IEnumerable allLanguages) : IDatabaseValidator + { + private readonly List _aliasFiles = aliasFiles; + private readonly IEnumerable _allLanguages = allLanguages; + + /// + /// Validation function that checks if the given subdirectory contains an alias file for each language + /// + /// object subject to validation + /// Null if no issues are found, a key value pair containing information about rule violation> in case some alias files are missing + public KeyValuePair? Validate(DatabaseValidationItem item) + { + foreach (string language in _allLanguages) + { + if (!_aliasFiles.Exists(file => file.Languages.Contains(language))) + { + return new( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.AliasFileMissing), + new(item.Path, additionalInfo: $"Alias file for {language} is missing") + ); + } + } + + return null; + } + } +} diff --git a/Px.Utils/Validation/DatabaseValidation/IFileSystem.cs b/Px.Utils/Validation/DatabaseValidation/IFileSystem.cs new file mode 100644 index 00000000..64394039 --- /dev/null +++ b/Px.Utils/Validation/DatabaseValidation/IFileSystem.cs @@ -0,0 +1,20 @@ +using Px.Utils.PxFile.Metadata; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Px.Utils.Validation.DatabaseValidation +{ + /// + /// Interface for implementing custom file processing systems for database validation. + /// + public interface IFileSystem + { + public IEnumerable EnumerateFiles(string path, string searchPattern); + public Stream GetFileStream(string path); + public IEnumerable EnumerateDirectories(string path); + public string GetFileName(string path); + public string GetDirectoryName(string path); + public Encoding GetEncoding(Stream stream); + public Task GetEncodingAsync(Stream stream, CancellationToken cancellationToken); + } +} diff --git a/Px.Utils/Validation/DatabaseValidation/LocalFileSystem.cs b/Px.Utils/Validation/DatabaseValidation/LocalFileSystem.cs new file mode 100644 index 00000000..0e08c638 --- /dev/null +++ b/Px.Utils/Validation/DatabaseValidation/LocalFileSystem.cs @@ -0,0 +1,53 @@ +using Px.Utils.PxFile.Metadata; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Px.Utils.Validation.DatabaseValidation +{ + // Excluded from code coverage because it is a wrapper around the file system and testing IO operations is not feasible. + [ExcludeFromCodeCoverage] + /// + /// Default file system used for database validation process. Contains default implementations of numerous IO operations + /// and a function for determining a file's encoding format. + /// + public class LocalFileSystem : IFileSystem + { + public IEnumerable EnumerateFiles(string path, string searchPattern) + { + return Directory.EnumerateFiles(path, searchPattern, SearchOption.AllDirectories); + } + + public Stream GetFileStream(string path) + { + return new FileStream(path, FileMode.Open, FileAccess.Read); + } + + public IEnumerable EnumerateDirectories(string path) + { + return Directory.EnumerateDirectories(path, "*", SearchOption.AllDirectories); + } + + public string GetFileName(string path) + { + return Path.GetFileName(path); + } + + public string GetDirectoryName(string path) + { + string? directory = Path.GetDirectoryName(path); + return directory ?? throw new ArgumentException("Path does not contain a directory."); + } + + public Encoding GetEncoding(Stream stream) + { + PxFileMetadataReader reader = new(); + return reader.GetEncoding(stream); + } + + public async Task GetEncodingAsync(Stream stream, CancellationToken cancellationToken) + { + PxFileMetadataReader reader = new(); + return await reader.GetEncodingAsync(stream, cancellationToken: cancellationToken); + } + } +} diff --git a/Px.Utils/Validation/Enums.cs b/Px.Utils/Validation/Enums.cs index 33777255..05e1b8d1 100644 --- a/Px.Utils/Validation/Enums.cs +++ b/Px.Utils/Validation/Enums.cs @@ -44,8 +44,8 @@ public enum ValidationFeedbackRule IllegalCharactersInKeyword = 13, KeywordDoesntStartWithALetter = 14, RegexTimeout = 15, - IllegalCharactersInLanguageParameter = 17, - IllegalCharactersInSpecifierParameter = 18, + IllegalCharactersInLanguageSection = 17, + IllegalCharactersInSpecifierSection = 18, EntryWithoutValue = 19, IncompliantLanguage = 20, KeywordContainsUnderscore = 21, @@ -78,6 +78,13 @@ public enum ValidationFeedbackRule DataValidationFeedbackInconsistentSeparator = 48, DataValidationFeedbackInvalidString = 49, DataValidationFeedbackInvalidNumber = 50, - DataValidationFeedbackInvalidChar = 51 + DataValidationFeedbackInvalidChar = 51, + FileLanguageDiffersFromDatabase = 52, + FileEncodingDiffersFromDatabase = 53, + AliasFileMissing = 54, + DuplicateFileNames = 55, + StartOfDataSectionNotFound = 56, + DuplicateEntry = 57, + DuplicateDimension = 58 } } diff --git a/Px.Utils/Validation/IPxFileStreamValidator.cs b/Px.Utils/Validation/IPxFileStreamValidator.cs new file mode 100644 index 00000000..9a2c10fe --- /dev/null +++ b/Px.Utils/Validation/IPxFileStreamValidator.cs @@ -0,0 +1,38 @@ +using Px.Utils.Validation.DatabaseValidation; +using System.Text; + +namespace Px.Utils.Validation +{ + /// + /// Represents a validator for a Px file. + /// + public interface IPxFileStreamValidator + { + /// + /// Blocking method that validates some aspect of a Px file. + /// + /// Stream of the PX file to be validated + /// Name of the PX file + /// Encoding of the PX file. If not provided, validator tries to find the encoding. + /// File system to use for file operations. If not provided, default file system is used. + /// object that contains an array of feedback items gathered during the validation process. + public ValidationResult Validate(Stream stream, string filename, Encoding? encoding = null, IFileSystem? fileSystem = null); + } + + /// + /// Represents a validator for a Px file with asynchronous validation capabilities. + /// + public interface IPxFileStreamValidatorAsync + { + /// + /// Asynchronous method that validates some aspect of a Px file. + /// + /// Stream of the PX file to be validated + /// Name of the PX file + /// Encoding of the PX file. If not provided, validator tries to find the encoding. + /// File system to use for file operations. If not provided, default file system is used. + /// Cancellation token for cancelling the validation process + /// object that contains an array of feedback items gathered during the validation process. + public Task ValidateAsync(Stream stream, string filename, Encoding? encoding = null, IFileSystem? fileSystem = null, CancellationToken cancellationToken = default); + } +} diff --git a/Px.Utils/Validation/IValidationResult.cs b/Px.Utils/Validation/IValidationResult.cs index 56be4291..c875e61e 100644 --- a/Px.Utils/Validation/IValidationResult.cs +++ b/Px.Utils/Validation/IValidationResult.cs @@ -3,8 +3,9 @@ /// /// Represents a feedback item that was produced during a px file validation operation. /// - public class ValidationResult(ValidationFeedbackItem[] feedbackItems) + /// The feedback items that were produced during the validation operation. + public class ValidationResult(ValidationFeedback feedbackItems) { - public ValidationFeedbackItem[] FeedbackItems { get; } = feedbackItems; + public ValidationFeedback FeedbackItems { get; } = feedbackItems; } } diff --git a/Px.Utils/Validation/IPxFileValidator.cs b/Px.Utils/Validation/IValidator.cs similarity index 78% rename from Px.Utils/Validation/IPxFileValidator.cs rename to Px.Utils/Validation/IValidator.cs index a0411598..0666841b 100644 --- a/Px.Utils/Validation/IPxFileValidator.cs +++ b/Px.Utils/Validation/IValidator.cs @@ -1,21 +1,21 @@ namespace Px.Utils.Validation { /// - /// Represents a validator for a Px file. + /// Represents a validator for a Px file or database /// - public interface IPxFileValidator + public interface IValidator { /// - /// Blocking method that validates some aspect of a Px file. + /// Blocking method that validates some aspect of a Px file or a database /// /// object that contains an array of feedback items gathered during the validation process. public ValidationResult Validate(); } /// - /// Represents a validator for a Px file with asynchronous validation capabilities. + /// Represents a validator for a Px file or database with asynchronous validation capabilities. /// - public interface IPxFileValidatorAsync + public interface IValidatorAsync { /// /// Asynchronous method that validates some aspect of a Px file. @@ -24,4 +24,4 @@ public interface IPxFileValidatorAsync /// object that contains an array of feedback items gathered during the validation process. public Task ValidateAsync(CancellationToken cancellationToken = default); } -} +} \ No newline at end of file diff --git a/Px.Utils/Validation/PxFileValidator.cs b/Px.Utils/Validation/PxFileValidator.cs index 9dceb723..0dfb9510 100644 --- a/Px.Utils/Validation/PxFileValidator.cs +++ b/Px.Utils/Validation/PxFileValidator.cs @@ -1,5 +1,7 @@ -using Px.Utils.PxFile; +using Px.Utils.Exceptions; +using Px.Utils.PxFile; using Px.Utils.Validation.ContentValidation; +using Px.Utils.Validation.DatabaseValidation; using Px.Utils.Validation.DataValidation; using Px.Utils.Validation.SyntaxValidation; using System.Text; @@ -9,21 +11,17 @@ namespace Px.Utils.Validation /// /// Validates a Px file as a whole. /// - /// Px file stream to be validated - /// Name of the file subject to validation - /// Encoding format of the px file /// object that contains symbols and tokens required for the px file syntax. public class PxFileValidator( - Stream stream, - string filename, - Encoding? encoding, PxFileSyntaxConf? syntaxConf = null - ) : IPxFileValidator, IPxFileValidatorAsync + ) : IPxFileStreamValidator, IPxFileStreamValidatorAsync { private CustomSyntaxValidationFunctions? _customSyntaxValidationFunctions; private CustomContentValidationFunctions? _customContentValidationFunctions; - private IPxFileValidator[]? _customValidators; - private IPxFileValidatorAsync[]? _customAsyncValidators; + private IPxFileStreamValidator[]? _customStreamValidators; + private IPxFileStreamValidatorAsync[]? _customStreamAsyncValidators; + private IValidator[]? _customValidators; + private IValidatorAsync[]? _customAsyncValidators; /// /// Set custom validation functions to be used during validation. @@ -39,10 +37,18 @@ public void SetCustomValidatorFunctions(CustomSyntaxValidationFunctions? customS /// /// Set custom validators to be used during validation. /// - /// Array of objects that implement interface, used for blocking validation. - /// Array of objects that implement interface, used for asynchronous validation. - public void SetCustomValidators(IPxFileValidator[]? customValidators = null, IPxFileValidatorAsync[]? customAsyncValidators = null) + /// Array of objects that implement interface, used for px file stream blocking validation. + /// Array of objects that implement interface, used for px file stream asynchronous validation. + /// Array of objects that implement interface, used for custom blocking validation. + /// Array of objects that implement interface, used for custom asynchronous validation. + public void SetCustomValidators( + IPxFileStreamValidator[]? customStreamValidators = null, + IPxFileStreamValidatorAsync[]? customStreamAsyncValidators = null, + IValidator[]? customValidators = null, + IValidatorAsync[]? customAsyncValidators = null) { + _customStreamValidators = customStreamValidators; + _customStreamAsyncValidators = customStreamAsyncValidators; _customValidators = customValidators; _customAsyncValidators = customAsyncValidators; } @@ -50,88 +56,138 @@ public void SetCustomValidators(IPxFileValidator[]? customValidators = null, IPx /// /// Validates the Px file. Starts with metadata syntax validation, then metadata content validation, and finally data validation. /// If any custom validation functions are set, they are executed after the default validation steps. + /// Px file stream to be validated + /// Name of the file subject to validation + /// Encoding format of the px file + /// File system to use for file operations. If not provided, default file system is used. /// /// object that contains the feedback gathered during the validation process. - public ValidationResult Validate() + public ValidationResult Validate( + Stream stream, + string filename, + Encoding? encoding = null, + IFileSystem? fileSystem = null) { - encoding ??= Encoding.Default; + encoding ??= new LocalFileSystem().GetEncoding(stream); syntaxConf ??= PxFileSyntaxConf.Default; - List feedbacks = []; - SyntaxValidator syntaxValidator = new(stream, encoding, filename, syntaxConf, _customSyntaxValidationFunctions, true); - SyntaxValidationResult syntaxValidationResult = syntaxValidator.Validate(); + ValidationFeedback feedbacks = []; + SyntaxValidator syntaxValidator = new(syntaxConf, _customSyntaxValidationFunctions); + SyntaxValidationResult syntaxValidationResult = syntaxValidator.Validate(stream, filename, encoding, fileSystem); feedbacks.AddRange(syntaxValidationResult.FeedbackItems); - ContentValidator contentValidator = new(filename, encoding, [..syntaxValidationResult.Result], _customContentValidationFunctions, syntaxConf); + ContentValidator contentValidator = new(filename, encoding, [.. syntaxValidationResult.Result], _customContentValidationFunctions, syntaxConf); ContentValidationResult contentValidationResult = contentValidator.Validate(); feedbacks.AddRange(contentValidationResult.FeedbackItems); + if (syntaxValidationResult.DataStartStreamPosition == -1) + { + feedbacks.Add(new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.StartOfDataSectionNotFound), + new(filename, 0, 0) + )); + + return new (feedbacks); + } + stream.Position = syntaxValidationResult.DataStartStreamPosition; DataValidator dataValidator = new( - stream, contentValidationResult.DataRowLength, contentValidationResult.DataRowAmount, - filename, syntaxValidationResult.DataStartRow, - encoding, syntaxConf); - ValidationResult dataValidationResult = dataValidator.Validate(); + ValidationResult dataValidationResult = dataValidator.Validate(stream, filename, encoding, fileSystem); feedbacks.AddRange(dataValidationResult.FeedbackItems); + if (_customStreamValidators is not null) + { + foreach (IPxFileStreamValidator customValidator in _customStreamValidators) + { + ValidationResult customValidationResult = customValidator.Validate(stream, filename, encoding, fileSystem); + feedbacks.AddRange(customValidationResult.FeedbackItems); + } + } if (_customValidators is not null) { - foreach (IPxFileValidator customValidator in _customValidators) + foreach (IValidator customValidator in _customValidators) { ValidationResult customValidationResult = customValidator.Validate(); feedbacks.AddRange(customValidationResult.FeedbackItems); } } - - return new ValidationResult([..feedbacks]); + stream.Close(); + return new ValidationResult(feedbacks); } /// /// Validates the Px file asynchronously. Starts with metadata syntax validation, then metadata content validation, and finally data validation. /// If any custom validation functions are set, they are executed after the default validation steps. + /// Px file stream to be validated + /// Name of the file subject to validation + /// Encoding format of the px file + /// File system to use for file operations. If not provided, default file system is used. + /// Cancellation token for cancelling the validation process /// /// object that contains the feedback gathered during the validation process. - public async Task ValidateAsync(CancellationToken cancellationToken = default) + public async Task ValidateAsync( + Stream stream, + string filename, + Encoding? encoding = null, + IFileSystem? fileSystem = null, + CancellationToken cancellationToken = default) { - encoding ??= Encoding.Default; + encoding ??= await new LocalFileSystem().GetEncodingAsync(stream, cancellationToken); syntaxConf ??= PxFileSyntaxConf.Default; - List feedbacks = []; - SyntaxValidator syntaxValidator = new(stream, encoding, filename, syntaxConf, _customSyntaxValidationFunctions, true); - SyntaxValidationResult syntaxValidationResult = await syntaxValidator.ValidateAsync(cancellationToken); + ValidationFeedback feedbacks = []; + SyntaxValidator syntaxValidator = new(syntaxConf, _customSyntaxValidationFunctions); + SyntaxValidationResult syntaxValidationResult = await syntaxValidator.ValidateAsync(stream, filename, encoding, fileSystem, cancellationToken); feedbacks.AddRange(syntaxValidationResult.FeedbackItems); ContentValidator contentValidator = new(filename, encoding, [..syntaxValidationResult.Result], _customContentValidationFunctions, syntaxConf); ContentValidationResult contentValidationResult = contentValidator.Validate(); feedbacks.AddRange(contentValidationResult.FeedbackItems); + if (syntaxValidationResult.DataStartStreamPosition == -1) + { + feedbacks.Add(new( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.StartOfDataSectionNotFound), + new(filename, 0, 0) + )); + + return new (feedbacks); + } + stream.Position = syntaxValidationResult.DataStartStreamPosition; DataValidator dataValidator = new( - stream, contentValidationResult.DataRowLength, contentValidationResult.DataRowAmount, - filename, syntaxValidationResult.DataStartRow, - encoding, syntaxConf); - ValidationResult dataValidationResult = await dataValidator.ValidateAsync(cancellationToken); + ValidationResult dataValidationResult = await dataValidator.ValidateAsync(stream, filename, encoding, fileSystem, cancellationToken); feedbacks.AddRange(dataValidationResult.FeedbackItems); + if (_customStreamAsyncValidators is not null) + { + foreach (IPxFileStreamValidatorAsync customValidator in _customStreamAsyncValidators) + { + ValidationResult customValidationResult = await customValidator.ValidateAsync(stream, filename, encoding, fileSystem, cancellationToken); + feedbacks.AddRange(customValidationResult.FeedbackItems); + } + } if (_customAsyncValidators is not null) { - foreach (IPxFileValidatorAsync customValidator in _customAsyncValidators) + foreach (IValidatorAsync customValidator in _customAsyncValidators) { ValidationResult customValidationResult = await customValidator.ValidateAsync(cancellationToken); feedbacks.AddRange(customValidationResult.FeedbackItems); } } - - return new ValidationResult([..feedbacks]); + stream.Close(); + return new ValidationResult(feedbacks); } } } diff --git a/Px.Utils/Validation/SyntaxValidation/SyntaxValidationFunctions.KeyValueValidationFunctions.cs b/Px.Utils/Validation/SyntaxValidation/SyntaxValidationFunctions.KeyValueValidationFunctions.cs new file mode 100644 index 00000000..c405ea65 --- /dev/null +++ b/Px.Utils/Validation/SyntaxValidation/SyntaxValidationFunctions.KeyValueValidationFunctions.cs @@ -0,0 +1,709 @@ +using Px.Utils.PxFile; + +namespace Px.Utils.Validation.SyntaxValidation +{ + public delegate KeyValuePair? KeyValuePairValidationFunction(ValidationKeyValuePair validationObject, PxFileSyntaxConf syntaxConf); + + /// + /// Contains the default validation functions for syntax validation key value pair entries. + /// + public partial class SyntaxValidationFunctions + { + public List DefaultKeyValueValidationFunctions { get; } = + [ + MoreThanOneLanguageParameter, + MoreThanOneSpecifierParameter, + WrongKeyOrderOrMissingKeyword, + SpecifierPartNotEnclosed, + MoreThanTwoSpecifierParts, + NoDelimiterBetweenSpecifierParts, + IllegalSymbolsInLanguageParamSection, + IllegalCharactersInSpecifierSection, + InvalidValueFormat, + ExcessWhitespaceInValue, + KeyContainsExcessWhiteSpace, + ExcessNewLinesInValue + ]; + + /// + /// If the key contains more than one language parameter, a new feedback is returned. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the key contains more than one language parameter, null otherwise. + public static KeyValuePair? MoreThanOneLanguageParameter(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) + { + ExtractSectionResult languageSections = SyntaxValidationUtilityMethods.ExtractSectionFromString( + validationKeyValuePair.KeyValuePair.Key, + syntaxConf.Symbols.Key.LangParamStart, + syntaxConf.Symbols.Key.StringDelimeter, + syntaxConf.Symbols.Key.LangParamEnd); + + if (languageSections.Sections.Length > 1) + { + KeyValuePair lineAndCharacter = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationKeyValuePair.KeyStartLineIndex, + languageSections.StartIndexes[1], + validationKeyValuePair.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.MoreThanOneLanguageParameterSection), + new(validationKeyValuePair.File, + lineAndCharacter.Key, + lineAndCharacter.Value) + ); + } + else + { + return null; + } + } + + /// + /// If the key contains more than one specifier parameter, a new feedback is returned. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the key contains more than one specifier parameter, null otherwise. + public static KeyValuePair? MoreThanOneSpecifierParameter(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) + { + ExtractSectionResult specifierSpections = SyntaxValidationUtilityMethods.ExtractSectionFromString( + validationKeyValuePair.KeyValuePair.Key, + syntaxConf.Symbols.Key.SpecifierParamStart, + syntaxConf.Symbols.Key.StringDelimeter, + syntaxConf.Symbols.Key.SpecifierParamEnd); + + if (specifierSpections.Sections.Length > 1) + { + KeyValuePair lineAndCharacter = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationKeyValuePair.KeyStartLineIndex, + specifierSpections.StartIndexes[1], + validationKeyValuePair.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.MoreThanOneSpecifierParameterSection), + new(validationKeyValuePair.File, + lineAndCharacter.Key, + lineAndCharacter.Value) + ); + } + else + { + return null; + } + } + + /// + /// If the key is not defined in the order of KEYWORD[language](\"specifier\"), a new feedback is returned. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the key is not defined in the order of KEYWORD[language](\"specifier\"), null otherwise. + public static KeyValuePair? WrongKeyOrderOrMissingKeyword(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) + { + string key = validationKeyValuePair.KeyValuePair.Key; + + // Remove language parameter section if it exists + ExtractSectionResult languageSection = SyntaxValidationUtilityMethods.ExtractSectionFromString( + key, + syntaxConf.Symbols.Key.LangParamStart, + syntaxConf.Symbols.Key.StringDelimeter, + syntaxConf.Symbols.Key.LangParamEnd); + + string languageRemoved = languageSection.Remainder; + + // Remove specifier section + ExtractSectionResult specifierRemoved = SyntaxValidationUtilityMethods.ExtractSectionFromString( + languageRemoved, + syntaxConf.Symbols.Key.SpecifierParamStart, + syntaxConf.Symbols.Key.StringDelimeter, + syntaxConf.Symbols.Key.SpecifierParamEnd); + + // Check for missing specifierRemoved. Keyword is the remainder of the string after removing language and specifier sections + if (specifierRemoved.Remainder.Trim() == string.Empty) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationKeyValuePair.KeyStartLineIndex, + 0, + validationKeyValuePair.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.MissingKeyword), + new(validationKeyValuePair.File, + feedbackIndexes.Key, + feedbackIndexes.Value) + ); + } + + // Check where language and parameter sections start + Dictionary stringSections = SyntaxValidationUtilityMethods.GetEnclosingCharacterIndexes(key, syntaxConf.Symbols.Key.StringDelimeter); + int langParamStartIndex = SyntaxValidationUtilityMethods.FindSymbolIndex(key, syntaxConf.Symbols.Key.LangParamStart, stringSections); + int specifierParamStartIndex = SyntaxValidationUtilityMethods.FindSymbolIndex(key, syntaxConf.Symbols.Key.SpecifierParamStart, stringSections); + + // If the key contains no language or specifier section, it is in the correct order + if (langParamStartIndex == -1 && specifierParamStartIndex == -1) + { + return null; + } + // Check if the specifier section comes before the language + else if (specifierParamStartIndex != -1 && langParamStartIndex > specifierParamStartIndex) + { + KeyValuePair specifierIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationKeyValuePair.KeyStartLineIndex, + specifierParamStartIndex, + validationKeyValuePair.LineChangeIndexes + ); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.KeyHasWrongOrder), + new(validationKeyValuePair.File, + specifierIndexes.Key, + specifierIndexes.Value) + ); + } + // Check if the key starts with a language or specifier parameter section + else if ( + key.Trim().StartsWith(syntaxConf.Symbols.Key.SpecifierParamStart) || + key.Trim().StartsWith(syntaxConf.Symbols.Key.LangParamStart)) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationKeyValuePair.KeyStartLineIndex, + 0, + validationKeyValuePair.LineChangeIndexes + ); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.KeyHasWrongOrder), + new(validationKeyValuePair.File, + feedbackIndexes.Key, + feedbackIndexes.Value) + ); + } + else + { + return null; + } + } + + /// + /// If the key contains more than two specifiers, a new feedback is returned. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if more than two specifiers are found, null otherwise. + public static KeyValuePair? MoreThanTwoSpecifierParts(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) + { + ExtractSectionResult specifierSection = SyntaxValidationUtilityMethods.ExtractSectionFromString( + validationKeyValuePair.KeyValuePair.Key, + syntaxConf.Symbols.Key.SpecifierParamStart, + syntaxConf.Symbols.Key.StringDelimeter, + syntaxConf.Symbols.Key.SpecifierParamEnd + ); + + string? specifierParamSection = specifierSection.Sections.FirstOrDefault(); + + // If no parameter section is found, there are no specifiers + if (specifierParamSection is null) + { + return null; + } + + // Extract specifiers from the specifier section + ExtractSectionResult specifierResult = SyntaxValidationUtilityMethods.ExtractSectionFromString( + specifierParamSection, + syntaxConf.Symbols.Key.StringDelimeter, + syntaxConf.Symbols.Key.StringDelimeter + ); + + if (specifierResult.Sections.Length > 2) + { + KeyValuePair secondSpecifierIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationKeyValuePair.KeyStartLineIndex, + specifierSection.StartIndexes[0] + specifierResult.StartIndexes[2], + validationKeyValuePair.LineChangeIndexes + ); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.TooManySpecifiers), + new(validationKeyValuePair.File, + secondSpecifierIndexes.Key, + secondSpecifierIndexes.Value) + ); + } + return null; + } + + /// + /// If there is no delimeter between specifier parts, a new feedback is returned. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if there is no delimeter between specifier parts, null otherwise. + public static KeyValuePair? NoDelimiterBetweenSpecifierParts(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) + { + ExtractSectionResult specifierSection = SyntaxValidationUtilityMethods.ExtractSectionFromString( + validationKeyValuePair.KeyValuePair.Key, + syntaxConf.Symbols.Key.SpecifierParamStart, + syntaxConf.Symbols.Key.StringDelimeter, + syntaxConf.Symbols.Key.SpecifierParamEnd + ); + + string? specifierParamSection = specifierSection.Sections.FirstOrDefault(); + + // If no specifier section is found, there are no specifiers + if (specifierParamSection is null) + { + return null; + } + + // Extract specifiers from the specifier section + ExtractSectionResult specifierResult = SyntaxValidationUtilityMethods.ExtractSectionFromString( + specifierParamSection, + syntaxConf.Symbols.Key.StringDelimeter, + syntaxConf.Symbols.Key.StringDelimeter + ); + + string[] specifiers = specifierResult.Sections; + + // If there are more than one specifier and the remainder of the specifier section does not contain a list separator, a delimiter is missing + if (specifiers.Length > 1 && !specifierResult.Remainder.Contains(syntaxConf.Symbols.Key.ListSeparator)) + { + KeyValuePair specifierIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationKeyValuePair.KeyStartLineIndex, + specifierSection.StartIndexes[0] + specifierResult.StartIndexes[1], + validationKeyValuePair.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.SpecifierDelimiterMissing), + new(validationKeyValuePair.File, + specifierIndexes.Key, + specifierIndexes.Value) + ); + } + else + { + return null; + } + } + + /// + /// If any of the specifiers are not enclosed a new feedback is returned. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if specifier parts are not enclosed, null otherwise. + public static KeyValuePair? SpecifierPartNotEnclosed(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) + { + ExtractSectionResult specifierSection = SyntaxValidationUtilityMethods.ExtractSectionFromString( + validationKeyValuePair.KeyValuePair.Key, + syntaxConf.Symbols.Key.SpecifierParamStart, + syntaxConf.Symbols.Key.StringDelimeter, + syntaxConf.Symbols.Key.SpecifierParamEnd + ); + + string? specifierParamSection = specifierSection.Sections.FirstOrDefault(); + + // If specifier section is not found, there are no specifiers + if (specifierParamSection == null) + { + return null; + } + List specifiers = SyntaxValidationUtilityMethods.GetListItemsFromString(specifierParamSection, syntaxConf.Symbols.Key.ListSeparator, syntaxConf.Symbols.Key.StringDelimeter); + // Check if specifiers are enclosed in string delimeters + for (int i = 0; i < specifiers.Count; i++) + { + string specifier = specifiers[i]; + string trimmedSpecifier = specifier.Trim(); + if (!trimmedSpecifier.StartsWith(syntaxConf.Symbols.Key.StringDelimeter) || !trimmedSpecifier.EndsWith(syntaxConf.Symbols.Key.StringDelimeter)) + { + KeyValuePair specifierIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationKeyValuePair.KeyStartLineIndex, + specifierSection.StartIndexes[0], + validationKeyValuePair.LineChangeIndexes + ); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.SpecifierPartNotEnclosed), + new(validationKeyValuePair.File, + specifierIndexes.Key, + specifierIndexes.Value) + ); + } + } + return null; + } + + /// + /// If the key language section contains illegal symbols, a new feedback is returned. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the key language section contains illegal symbols, null otherwise. + public static KeyValuePair? IllegalSymbolsInLanguageParamSection(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) + { + string key = validationKeyValuePair.KeyValuePair.Key; + + ExtractSectionResult languageParam = SyntaxValidationUtilityMethods.ExtractSectionFromString( + key, + syntaxConf.Symbols.Key.LangParamStart, + syntaxConf.Symbols.Key.StringDelimeter, + syntaxConf.Symbols.Key.LangParamEnd + ); + + string? languageParamSection = languageParam.Sections.FirstOrDefault(); + + // If language section is not found, the validation is not relevant + if (languageParamSection == null) + { + return null; + } + + char[] languageIllegalSymbols = [ + syntaxConf.Symbols.Key.LangParamStart, + syntaxConf.Symbols.Key.LangParamEnd, + syntaxConf.Symbols.Key.SpecifierParamStart, + syntaxConf.Symbols.Key.SpecifierParamEnd, + syntaxConf.Symbols.Key.StringDelimeter, + syntaxConf.Symbols.Key.ListSeparator + ]; + + IEnumerable foundIllegalSymbols = languageParamSection.Where(c => languageIllegalSymbols.Contains(c)); + if (foundIllegalSymbols.Any()) + { + string foundSymbols = string.Join(", ", foundIllegalSymbols); + int indexOfFirstIllegalSymbol = languageParam.StartIndexes[0] + languageParamSection.IndexOf(foundIllegalSymbols.First()); + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationKeyValuePair.KeyStartLineIndex, + languageParam.StartIndexes[0] + indexOfFirstIllegalSymbol, + validationKeyValuePair.LineChangeIndexes); + + + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.IllegalCharactersInLanguageSection), + new(validationKeyValuePair.File, + feedbackIndexes.Key, + feedbackIndexes.Value, + foundSymbols) + ); + } + else + { + return null; + } + } + + /// + /// Checks that the key specifier contains only specifier parts, whitespace and 0-1 list separators. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the key specifier section contains illegal symbols, null otherwise. + public static KeyValuePair? IllegalCharactersInSpecifierSection(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) + { + string key = validationKeyValuePair.KeyValuePair.Key; + + ExtractSectionResult specifierParam = SyntaxValidationUtilityMethods.ExtractSectionFromString( + key, + syntaxConf.Symbols.Key.SpecifierParamStart, + syntaxConf.Symbols.Key.StringDelimeter, + syntaxConf.Symbols.Key.SpecifierParamEnd + ); + + string? specifierParamSection = specifierParam.Sections.FirstOrDefault(); + + // If specifier section is not found, there are no specifiers + if (specifierParamSection == null) + { + return null; + } + + // Remove specifier parts from the section + string removedSpecifiers = SyntaxValidationUtilityMethods.ExtractSectionFromString( + specifierParamSection, + syntaxConf.Symbols.Key.StringDelimeter, + syntaxConf.Symbols.Key.StringDelimeter + ).Remainder; + + // Allowed symbols in the specifier section after removing specifiers + char[] allowedSymbols = [ + syntaxConf.Symbols.Key.ListSeparator, + syntaxConf.Symbols.Key.StringDelimeter + ]; + allowedSymbols = [.. allowedSymbols, .. CharacterConstants.WhitespaceCharacters]; + + IEnumerable foundIllegalSymbols = removedSpecifiers.Where(c => !allowedSymbols.Contains(c)); + if (foundIllegalSymbols.Any()) + { + string foundSymbols = string.Join(", ", foundIllegalSymbols); + int indexOfFirstIllegalSymbol = specifierParam.StartIndexes[0] + specifierParamSection.IndexOf(foundIllegalSymbols.First()); + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationKeyValuePair.KeyStartLineIndex, + specifierParam.StartIndexes[0] + indexOfFirstIllegalSymbol, + validationKeyValuePair.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.IllegalCharactersInSpecifierSection), + new(validationKeyValuePair.File, + feedbackIndexes.Key, + feedbackIndexes.Value, + foundSymbols) + ); + } + // Only one list separator is allowed + else + { + int listSeparatorCount = removedSpecifiers.Count(c => c == syntaxConf.Symbols.Key.ListSeparator); + if (listSeparatorCount > 1) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationKeyValuePair.KeyStartLineIndex, + specifierParam.StartIndexes[0] + removedSpecifiers.IndexOf(syntaxConf.Symbols.Key.ListSeparator), + validationKeyValuePair.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.IllegalCharactersInSpecifierSection), + new(validationKeyValuePair.File, + feedbackIndexes.Key, + feedbackIndexes.Value, + "Only one list separator is allowed.") + ); + } + else + { + return null; + } + } + } + + /// + /// If the value section is not following a valid format, a new feedback is returned. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the value section is not following a valid format, null otherwise. + public static KeyValuePair? InvalidValueFormat(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) + { + string value = validationKeyValuePair.KeyValuePair.Value; + + // Try to parse the value section to a ValueType + ValueType? type = SyntaxValidationUtilityMethods.GetValueTypeFromString(value, syntaxConf); + + if (type is null) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationKeyValuePair.KeyStartLineIndex, + validationKeyValuePair.ValueStartIndex, + validationKeyValuePair.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.InvalidValueFormat), + new(validationKeyValuePair.File, + feedbackIndexes.Key, + feedbackIndexes.Value, + value) + ); + } + + // If value is of type String or ListOfStrings, check if the line changes are compliant to the specification + if (type is ValueType.StringValue || type is ValueType.ListOfStrings) + { + int lineChangeValidityIndex = SyntaxValidationUtilityMethods.GetLineChangesValidity(value, syntaxConf, (ValueType)type); + if (lineChangeValidityIndex != -1) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationKeyValuePair.KeyStartLineIndex, + lineChangeValidityIndex, + validationKeyValuePair.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.InvalidValueFormat), + new(validationKeyValuePair.File, + feedbackIndexes.Key, + feedbackIndexes.Value, + value) + ); + } + } + return null; + } + + /// + /// If the value section contains excess whitespace, a new feedback is returned. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the value section contains excess whitespace, null otherwise. + public static KeyValuePair? ExcessWhitespaceInValue(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) + { + // Validation only matters if the value is a list of strings + if (!SyntaxValidationUtilityMethods.IsStringListFormat( + validationKeyValuePair.KeyValuePair.Value, + syntaxConf.Symbols.Value.ListSeparator, + syntaxConf.Symbols.Key.StringDelimeter + )) + { + return null; + } + + string value = validationKeyValuePair.KeyValuePair.Value; + + int firstExcessWhitespaceIndex = SyntaxValidationUtilityMethods.FirstSubstringIndexFromString( + value, + [ + $"{CharacterConstants.CHARSPACE}{CharacterConstants.CHARSPACE}", + ], + syntaxConf.Symbols.Key.StringDelimeter); + + if (firstExcessWhitespaceIndex != -1) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationKeyValuePair.KeyStartLineIndex, + validationKeyValuePair.ValueStartIndex + firstExcessWhitespaceIndex, + validationKeyValuePair.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.ExcessWhitespaceInValue), + new(validationKeyValuePair.File, + feedbackIndexes.Key, + feedbackIndexes.Value, + value) + ); + } + + return null; + } + + /// + /// If the key contains excess whitespace, a new feedback is returned. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the key contains excess whitespace, null otherwise. + public static KeyValuePair? KeyContainsExcessWhiteSpace(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) + { + string key = validationKeyValuePair.KeyValuePair.Key; + bool insideString = false; + bool insideSpecifierSection = false; + int i = 0; + while (i < key.Length) + { + char currentCharacter = key[i]; + if (currentCharacter == syntaxConf.Symbols.Key.StringDelimeter) + { + insideString = !insideString; + } + else if (currentCharacter == syntaxConf.Symbols.Key.SpecifierParamStart) + { + insideSpecifierSection = true; + } + else if (currentCharacter == syntaxConf.Symbols.Key.SpecifierParamEnd) + { + insideSpecifierSection = false; + } + if (!insideString && currentCharacter == CharacterConstants.SPACE) + { + // If whitespace is found outside a string or specifier section, it is considered excess whitespace + if (!insideSpecifierSection) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationKeyValuePair.KeyStartLineIndex, + i, + validationKeyValuePair.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.KeyContainsExcessWhiteSpace), + new(validationKeyValuePair.File, + feedbackIndexes.Key, + feedbackIndexes.Value) + ); + } + + // Only expected whitespace outside string sections within key is between specifier parts, after list separator + if (key[i - 1] != syntaxConf.Symbols.Key.ListSeparator) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationKeyValuePair.KeyStartLineIndex, + i, + validationKeyValuePair.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.KeyContainsExcessWhiteSpace), + new(validationKeyValuePair.File, + feedbackIndexes.Key, + feedbackIndexes.Value) + ); + } + } + i++; + } + return null; + } + + /// + /// If the value section contains excess new lines, a new feedback is returned. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the value section contains excess new lines, null otherwise. + public static KeyValuePair? ExcessNewLinesInValue(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) + { + // We only need to run this validation if the value is less than 150 characters long and it is a string or list + ValueType? type = SyntaxValidationUtilityMethods.GetValueTypeFromString(validationKeyValuePair.KeyValuePair.Value, syntaxConf); + const int oneLineValueLengthRecommendedLimit = 150; + + if (validationKeyValuePair.KeyValuePair.Value.Length > oneLineValueLengthRecommendedLimit || + (type != ValueType.StringValue && + type != ValueType.ListOfStrings)) + { + return null; + } + + string value = validationKeyValuePair.KeyValuePair.Value; + + int firstNewLineIndex = SyntaxValidationUtilityMethods.FirstSubstringIndexFromString( + value, + [ + CharacterConstants.WindowsNewLine, + CharacterConstants.UnixNewLine + ], + syntaxConf.Symbols.Key.StringDelimeter); + + if (firstNewLineIndex != -1) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationKeyValuePair.KeyStartLineIndex, + validationKeyValuePair.ValueStartIndex + firstNewLineIndex, + validationKeyValuePair.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.ExcessNewLinesInValue), + new(validationKeyValuePair.File, + feedbackIndexes.Key, + feedbackIndexes.Value) + ); + } + else + { + return null; + } + } + } +} diff --git a/Px.Utils/Validation/SyntaxValidation/SyntaxValidationFunctions.StringValidationFunctions.cs b/Px.Utils/Validation/SyntaxValidation/SyntaxValidationFunctions.StringValidationFunctions.cs new file mode 100644 index 00000000..8e5c2434 --- /dev/null +++ b/Px.Utils/Validation/SyntaxValidation/SyntaxValidationFunctions.StringValidationFunctions.cs @@ -0,0 +1,98 @@ +using Px.Utils.PxFile; +using System.Text.RegularExpressions; + +namespace Px.Utils.Validation.SyntaxValidation +{ + public delegate KeyValuePair? EntryValidationFunction(ValidationEntry validationObject, PxFileSyntaxConf syntaxConf); + + /// + /// Contains the default validation functions for syntax validation string entries. + /// + public partial class SyntaxValidationFunctions + { + public List DefaultStringValidationFunctions { get; } = + [ + MultipleEntriesOnLine, + EntryWithoutValue, + ]; + + /// + /// If does not start with a line separator, it is considered as not being on its own line, and a new feedback is returned. + /// + /// The entry to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the entry does not start with a line separator, null otherwise. + public static KeyValuePair? MultipleEntriesOnLine (ValidationEntry validationEntry, PxFileSyntaxConf syntaxConf) + { + // If the entry does not start with a line separator, it is not on its own line. For the first entry this is not relevant. + if ( + validationEntry.EntryIndex == 0 || + validationEntry.EntryString.StartsWith(CharacterConstants.UnixNewLine, StringComparison.Ordinal) || + validationEntry.EntryString.StartsWith(CharacterConstants.WindowsNewLine, StringComparison.Ordinal)) + { + return null; + } + else + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationEntry.KeyStartLineIndex, + 0, + validationEntry.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.MultipleEntriesOnOneLine), + new(validationEntry.File, + feedbackIndexes.Key, + feedbackIndexes.Value) + ); + } + } + + /// + /// If there is no value section, a new feedback is returned. + /// + /// The validationKeyValuePair to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if there is no value section, null otherwise. + public static KeyValuePair? EntryWithoutValue(ValidationEntry validationEntry, PxFileSyntaxConf syntaxConf) + { + int[] keywordSeparatorIndeces = SyntaxValidationUtilityMethods.FindKeywordSeparatorIndeces(validationEntry.EntryString, syntaxConf); + + if (keywordSeparatorIndeces.Length == 0) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationEntry.KeyStartLineIndex, + validationEntry.EntryString.Length, + validationEntry.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.EntryWithoutValue), + new(validationEntry.File, + feedbackIndexes.Key, + feedbackIndexes.Value) + ); + } + else if (keywordSeparatorIndeces.Length > 1) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationEntry.KeyStartLineIndex, + keywordSeparatorIndeces[1], + validationEntry.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.EntryWithMultipleValues), + new(validationEntry.File, + feedbackIndexes.Key, + feedbackIndexes.Value) + ); + } + else + { + return null; + } + } + } +} diff --git a/Px.Utils/Validation/SyntaxValidation/SyntaxValidationFunctions.StructuredValidationFunctions.cs b/Px.Utils/Validation/SyntaxValidation/SyntaxValidationFunctions.StructuredValidationFunctions.cs new file mode 100644 index 00000000..8add2bb1 --- /dev/null +++ b/Px.Utils/Validation/SyntaxValidation/SyntaxValidationFunctions.StructuredValidationFunctions.cs @@ -0,0 +1,359 @@ +using Px.Utils.PxFile; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Px.Utils.Validation.SyntaxValidation +{ + public delegate KeyValuePair? StructuredValidationFunction(ValidationStructuredEntry validationObject, PxFileSyntaxConf syntaxConf); + + /// + /// Contains the default validation functions for syntax validation structured entries. + /// + public partial class SyntaxValidationFunctions + { + public List DefaultStructuredValidationFunctions { get; } = + [ + KeywordContainsIllegalCharacters, + KeywordDoesntStartWithALetter, + IllegalCharactersInLanguageParameter, + IllegalCharactersInSpecifierParts, + IncompliantLanguage, + KeywordContainsUnderscore, + KeywordIsNotInUpperCase, + KeywordIsExcessivelyLong + ]; + + private static readonly TimeSpan regexTimeout = TimeSpan.FromMilliseconds(50); + private static readonly Regex keywordIllegalSymbolsRegex = new(@"[^a-zA-Z0-9_-]", RegexOptions.Compiled, regexTimeout); + + /// + /// If the specifier contains illegal characters, a new feedback is returned. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the specifier contains illegal characters, null otherwise. + public static KeyValuePair? KeywordContainsIllegalCharacters(ValidationStructuredEntry validationStructuredEntry, PxFileSyntaxConf syntaxConf) + { + string keyword = validationStructuredEntry.Key.Keyword; + // Missing specifier is catched earlier + if (keyword.Length == 0) + { + return null; + } + + // Find all illegal symbols in specifier + try + { + MatchCollection matchesKeyword = keywordIllegalSymbolsRegex.Matches(keyword, 0); + IEnumerable illegalSymbolsInKeyWord = matchesKeyword.Cast().Select(m => m.Value).Distinct(); + if (illegalSymbolsInKeyWord.Any()) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationStructuredEntry.KeyStartLineIndex, + keyword.IndexOf(illegalSymbolsInKeyWord.First(), StringComparison.Ordinal), + validationStructuredEntry.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.IllegalCharactersInKeyword), + new(validationStructuredEntry.File, + feedbackIndexes.Key, + feedbackIndexes.Value, + string.Join(", ", illegalSymbolsInKeyWord)) + ); + } + } + catch (RegexMatchTimeoutException) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationStructuredEntry.KeyStartLineIndex, + 0, + validationStructuredEntry.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.RegexTimeout), + new(validationStructuredEntry.File, + feedbackIndexes.Key, + feedbackIndexes.Value) + ); + } + + return null; + } + + /// + /// If the keyword doesn't start with a letter, a new feedback is returned. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the specifier doesn't start with a letter, null otherwise. + public static KeyValuePair? KeywordDoesntStartWithALetter(ValidationStructuredEntry validationStructuredEntry, PxFileSyntaxConf syntaxConf) + { + string keyword = validationStructuredEntry.Key.Keyword; + + // Check if keyword starts with a letter + if (!char.IsLetter(keyword[0])) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationStructuredEntry.KeyStartLineIndex, + 0, + validationStructuredEntry.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.KeywordDoesntStartWithALetter), + new(validationStructuredEntry.File, + feedbackIndexes.Key, + feedbackIndexes.Value) + ); + } + else + { + return null; + } + } + + /// + /// If the language parameter is not following a valid format, a new feedback is returned. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the language parameter is not following a valid format, null otherwise. + public static KeyValuePair? IllegalCharactersInLanguageParameter( + ValidationStructuredEntry validationStructuredEntry, + PxFileSyntaxConf syntaxConf) + { + // Running this validation is relevant only for objects with a language parameter + if (validationStructuredEntry.Key.Language is null) + { + return null; + } + + string lang = validationStructuredEntry.Key.Language; + + // Find illegal characters from language parameter string + char[] illegalCharacters = [ + syntaxConf.Symbols.Key.LangParamStart, + syntaxConf.Symbols.Key.LangParamEnd, + syntaxConf.Symbols.Key.StringDelimeter, + syntaxConf.Symbols.EntrySeparator, + syntaxConf.Symbols.KeywordSeparator, + CharacterConstants.CHARSPACE, + CharacterConstants.CHARCARRIAGERETURN, + CharacterConstants.CHARLINEFEED, + CharacterConstants.CHARHORIZONTALTAB + ]; + + IEnumerable foundIllegalCharacters = lang.Where(c => illegalCharacters.Contains(c)); + if (foundIllegalCharacters.Any()) + { + int indexOfFirstIllegalCharacter = lang.IndexOf(foundIllegalCharacters.First()); + + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationStructuredEntry.KeyStartLineIndex, + validationStructuredEntry.Key.Keyword.Length + indexOfFirstIllegalCharacter + 1, + validationStructuredEntry.LineChangeIndexes); + + string foundSymbols = string.Join(", ", foundIllegalCharacters); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.IllegalCharactersInLanguageSection), + new(validationStructuredEntry.File, + feedbackIndexes.Key, + feedbackIndexes.Value, + foundSymbols) + ); + } + else + { + return null; + } + } + + /// + /// If the specifier parameter is not following a valid format, a new feedback is returned. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the specifier parameter is not following a valid format, null otherwise. + public static KeyValuePair? IllegalCharactersInSpecifierParts(ValidationStructuredEntry validationStructuredEntry, PxFileSyntaxConf syntaxConf) + { + // Running this validation is relevant only for objects with a specifier + if (validationStructuredEntry.Key.FirstSpecifier is null) + { + return null; + } + + char[] illegalCharacters = [syntaxConf.Symbols.EntrySeparator, syntaxConf.Symbols.Key.StringDelimeter]; + IEnumerable illegalcharactersInFirstSpecifier = validationStructuredEntry.Key.FirstSpecifier.Where(c => illegalCharacters.Contains(c)); + IEnumerable illegalcharactersInSecondSpecifier = []; + if (validationStructuredEntry.Key.SecondSpecifier is not null) + { + illegalcharactersInSecondSpecifier = validationStructuredEntry.Key.SecondSpecifier.Where(c => illegalCharacters.Contains(c)); + } + + if (illegalcharactersInFirstSpecifier.Any() || illegalcharactersInSecondSpecifier.Any()) + { + int languageSectionLength = validationStructuredEntry.Key.Language is not null ? validationStructuredEntry.Key.Language.Length + 3 : 1; + + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationStructuredEntry.KeyStartLineIndex, + validationStructuredEntry.Key.Keyword.Length + languageSectionLength, + validationStructuredEntry.LineChangeIndexes); + + char[] characters = [.. illegalcharactersInFirstSpecifier, .. illegalcharactersInSecondSpecifier]; + + return new KeyValuePair( + new(ValidationFeedbackLevel.Error, + ValidationFeedbackRule.IllegalCharactersInSpecifierPart), + new(validationStructuredEntry.File, + feedbackIndexes.Key, + feedbackIndexes.Value, + string.Join(", ", characters)) + ); + } + else + { + return null; + } + } + + /// + /// If the language parameter is not compliant with ISO 639 or BCP 47, a new feedback is returned. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the language parameter is not compliant with ISO 639 or BCP 47, null otherwise. + public static KeyValuePair? IncompliantLanguage(ValidationStructuredEntry validationStructuredEntry, PxFileSyntaxConf syntaxConf) + { + // Running this validation is relevant only for objects with a language + if (validationStructuredEntry.Key.Language is null) + { + return null; + } + + string lang = validationStructuredEntry.Key.Language; + + bool iso639OrMsLcidCompliant = CultureInfo.GetCultures(CultureTypes.AllCultures).ToList().Exists(c => c.Name == lang || c.ThreeLetterISOLanguageName == lang); + bool bcp47Compliant = Bcp47Codes.Codes.Contains(lang); + + if (iso639OrMsLcidCompliant || bcp47Compliant) + { + return null; + } + else + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationStructuredEntry.KeyStartLineIndex, + validationStructuredEntry.Key.Keyword.Length + 1, + validationStructuredEntry.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.IncompliantLanguage), + new(validationStructuredEntry.File, + feedbackIndexes.Key, + feedbackIndexes.Value) + ); + } + } + + /// + /// If the specifier contains unrecommended characters, a new feedback is returned. + /// + /// The to validate. + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the specifier contains unrecommended characters, null otherwise. + public static KeyValuePair? KeywordContainsUnderscore(ValidationStructuredEntry validationStructuredEntry, PxFileSyntaxConf syntaxConf) + { + int underscoreIndex = validationStructuredEntry.Key.Keyword.IndexOf('_'); + if (underscoreIndex != -1) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationStructuredEntry.KeyStartLineIndex, + underscoreIndex, + validationStructuredEntry.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.KeywordContainsUnderscore), + new(validationStructuredEntry.File, + feedbackIndexes.Key, + feedbackIndexes.Value) + ); + } + else + { + return null; + } + } + + /// + /// If the specifier is not in upper case, a new feedback is returned. + /// + /// The to validate + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the specifier is not in upper case, otherwise null + public static KeyValuePair? KeywordIsNotInUpperCase(ValidationStructuredEntry validationStructuredEntry, PxFileSyntaxConf syntaxConf) + { + string keyword = validationStructuredEntry.Key.Keyword; + string uppercaseKeyword = keyword.ToUpper(CultureInfo.InvariantCulture); + + if (uppercaseKeyword != keyword) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationStructuredEntry.KeyStartLineIndex, + 0, + validationStructuredEntry.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.KeywordIsNotInUpperCase), + new(validationStructuredEntry.File, + feedbackIndexes.Key, + feedbackIndexes.Value, + validationStructuredEntry.Key.Keyword) + ); + } + else + { + return null; + } + } + + /// + /// If the specifier is excessively long, a new feedback is returned. + /// + /// The object to validate + /// The syntax configuration for the PX file. + /// A validation feedback key value pair if the specifier is excessively long, otherwise null + public static KeyValuePair? KeywordIsExcessivelyLong(ValidationStructuredEntry validationStructuredEntry, PxFileSyntaxConf syntaxConf) + { + const int keywordLengthRecommendedLimit = 20; + string keyword = validationStructuredEntry.Key.Keyword; + + if (keyword.Length > keywordLengthRecommendedLimit) + { + KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( + validationStructuredEntry.KeyStartLineIndex, + keyword.Length, + validationStructuredEntry.LineChangeIndexes); + + return new KeyValuePair( + new(ValidationFeedbackLevel.Warning, + ValidationFeedbackRule.KeywordExcessivelyLong), + new(validationStructuredEntry.File, + feedbackIndexes.Key, + feedbackIndexes.Value, + validationStructuredEntry.Key.Keyword) + ); + } + else + { + return null; + } + } + } +} diff --git a/Px.Utils/Validation/SyntaxValidation/SyntaxValidationFunctions.cs b/Px.Utils/Validation/SyntaxValidation/SyntaxValidationFunctions.cs deleted file mode 100644 index f92e2028..00000000 --- a/Px.Utils/Validation/SyntaxValidation/SyntaxValidationFunctions.cs +++ /dev/null @@ -1,1071 +0,0 @@ -using Px.Utils.PxFile; -using System.Globalization; -using System.Text.RegularExpressions; - -namespace Px.Utils.Validation.SyntaxValidation -{ - public delegate ValidationFeedbackItem? EntryValidationFunction(ValidationEntry validationObject, PxFileSyntaxConf syntaxConf); - public delegate ValidationFeedbackItem? KeyValuePairValidationFunction(ValidationKeyValuePair validationObject, PxFileSyntaxConf syntaxConf); - public delegate ValidationFeedbackItem? StructuredValidationFunction(ValidationStructuredEntry validationObject, PxFileSyntaxConf syntaxConf); - - /// - /// Contains the default validation functions for syntax validation. - /// - public class SyntaxValidationFunctions - { - public List DefaultStringValidationFunctions { get; } - public List DefaultKeyValueValidationFunctions { get; } - public List DefaultStructuredValidationFunctions { get; } - - /// - /// Initializes a new instance of the class with default validation functions. - /// - public SyntaxValidationFunctions() - { - DefaultStringValidationFunctions = - [ - MultipleEntriesOnLine, - EntryWithoutValue, - ]; - - DefaultKeyValueValidationFunctions = - [ - MoreThanOneLanguageParameter, - MoreThanOneSpecifierParameter, - WrongKeyOrderOrMissingKeyword, - SpecifierPartNotEnclosed, - MoreThanTwoSpecifierParts, - NoDelimiterBetweenSpecifierParts, - IllegalSymbolsInLanguageParamSection, - IllegalSymbolsInSpecifierParamSection, - InvalidValueFormat, - ExcessWhitespaceInValue, - KeyContainsExcessWhiteSpace, - ExcessNewLinesInValue - ]; - - DefaultStructuredValidationFunctions = - [ - KeywordContainsIllegalCharacters, - KeywordDoesntStartWithALetter, - IllegalCharactersInLanguageParameter, - IllegalCharactersInSpecifierParts, - IncompliantLanguage, - KeywordContainsUnderscore, - KeywordIsNotInUpperCase, - KeywordIsExcessivelyLong - ]; - } - - private static readonly TimeSpan regexTimeout = TimeSpan.FromMilliseconds(50); - private static readonly Regex keywordIllegalSymbolsRegex = new(@"[^a-zA-Z0-9_-]", RegexOptions.Compiled, regexTimeout); - - /// - /// If does not start with a line separator, it is considered as not being on its own line, and a new is returned. - /// - /// The entry to validate. - /// The syntax configuration for the PX file. - /// A if the entry does not start with a line separator, null otherwise. - public static ValidationFeedbackItem? MultipleEntriesOnLine (ValidationEntry validationEntry, PxFileSyntaxConf syntaxConf) - { - // If the entry does not start with a line separator, it is not on its own line. For the first entry this is not relevant. - if ( - validationEntry.EntryIndex == 0 || - validationEntry.EntryString.StartsWith(CharacterConstants.UnixNewLine, StringComparison.Ordinal) || - validationEntry.EntryString.StartsWith(CharacterConstants.WindowsNewLine, StringComparison.Ordinal)) - { - return null; - } - else - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationEntry.KeyStartLineIndex, - 0, - validationEntry.LineChangeIndexes); - - return new ValidationFeedbackItem(validationEntry, new ValidationFeedback( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.MultipleEntriesOnOneLine, - feedbackIndexes.Key, - feedbackIndexes.Value - )); - } - } - - /// - /// If the key contains more than one language parameter, a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if the key contains more than one language parameter, null otherwise. - public static ValidationFeedbackItem? MoreThanOneLanguageParameter(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) - { - ExtractSectionResult languageSections = SyntaxValidationUtilityMethods.ExtractSectionFromString( - validationKeyValuePair.KeyValuePair.Key, - syntaxConf.Symbols.Key.LangParamStart, - syntaxConf.Symbols.Key.StringDelimeter, - syntaxConf.Symbols.Key.LangParamEnd); - - if (languageSections.Sections.Length > 1) - { - KeyValuePair lineAndCharacter = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationKeyValuePair.KeyStartLineIndex, - languageSections.StartIndexes[1], - validationKeyValuePair.LineChangeIndexes); - - return new ValidationFeedbackItem(validationKeyValuePair, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.MoreThanOneLanguageParameterSection, - lineAndCharacter.Key, - lineAndCharacter.Value - )); - } - else - { - return null; - } - } - - /// - /// If the key contains more than one specifier parameter, a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if the key contains more than one specifier parameter, null otherwise. - public static ValidationFeedbackItem? MoreThanOneSpecifierParameter(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) - { - ExtractSectionResult specifierSpections = SyntaxValidationUtilityMethods.ExtractSectionFromString( - validationKeyValuePair.KeyValuePair.Key, - syntaxConf.Symbols.Key.SpecifierParamStart, - syntaxConf.Symbols.Key.StringDelimeter, - syntaxConf.Symbols.Key.SpecifierParamEnd); - - if (specifierSpections.Sections.Length > 1) - { - KeyValuePair lineAndCharacter = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationKeyValuePair.KeyStartLineIndex, - specifierSpections.StartIndexes[1], - validationKeyValuePair.LineChangeIndexes); - - return new ValidationFeedbackItem(validationKeyValuePair, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.MoreThanOneSpecifierParameterSection, - lineAndCharacter.Key, - lineAndCharacter.Value - )); - } - else - { - return null; - } - } - - /// - /// If the key is not defined in the order of KEYWORD[language](\"specifier\"), a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if the key is not defined in the order of KEYWORD[language](\"specifier\"), null otherwise. - public static ValidationFeedbackItem? WrongKeyOrderOrMissingKeyword(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) - { - string key = validationKeyValuePair.KeyValuePair.Key; - - // Remove language parameter section if it exists - ExtractSectionResult languageSection = SyntaxValidationUtilityMethods.ExtractSectionFromString( - key, - syntaxConf.Symbols.Key.LangParamStart, - syntaxConf.Symbols.Key.StringDelimeter, - syntaxConf.Symbols.Key.LangParamEnd); - - string languageRemoved = languageSection.Remainder; - - // Remove specifier section - ExtractSectionResult specifierRemoved = SyntaxValidationUtilityMethods.ExtractSectionFromString( - languageRemoved, - syntaxConf.Symbols.Key.SpecifierParamStart, - syntaxConf.Symbols.Key.StringDelimeter, - syntaxConf.Symbols.Key.SpecifierParamEnd); - - // Check for missing specifierRemoved. Keyword is the remainder of the string after removing language and specifier sections - if (specifierRemoved.Remainder.Trim() == string.Empty) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationKeyValuePair.KeyStartLineIndex, - 0, - validationKeyValuePair.LineChangeIndexes); - - return new ValidationFeedbackItem(validationKeyValuePair, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.MissingKeyword, - feedbackIndexes.Key, - feedbackIndexes.Value)); - } - - // Check where language and parameter sections start - Dictionary stringSections = SyntaxValidationUtilityMethods.GetEnclosingCharacterIndexes(key, syntaxConf.Symbols.Key.StringDelimeter); - int langParamStartIndex = SyntaxValidationUtilityMethods.FindSymbolIndex(key, syntaxConf.Symbols.Key.LangParamStart, stringSections); - int specifierParamStartIndex = SyntaxValidationUtilityMethods.FindSymbolIndex(key, syntaxConf.Symbols.Key.SpecifierParamStart, stringSections); - - // If the key contains no language or specifier section, it is in the correct order - if (langParamStartIndex == -1 && specifierParamStartIndex == -1) - { - return null; - } - // Check if the specifier section comes before the language - else if (specifierParamStartIndex != -1 && langParamStartIndex > specifierParamStartIndex) - { - KeyValuePair specifierIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationKeyValuePair.KeyStartLineIndex, - specifierParamStartIndex, - validationKeyValuePair.LineChangeIndexes - ); - - return new ValidationFeedbackItem(validationKeyValuePair, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.KeyHasWrongOrder, - specifierIndexes.Key, - specifierIndexes.Value - )); - } - // Check if the key starts with a language or specifier parameter section - else if ( - key.Trim().StartsWith(syntaxConf.Symbols.Key.SpecifierParamStart) || - key.Trim().StartsWith(syntaxConf.Symbols.Key.LangParamStart)) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationKeyValuePair.KeyStartLineIndex, - 0, - validationKeyValuePair.LineChangeIndexes - ); - - return new ValidationFeedbackItem(validationKeyValuePair, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.KeyHasWrongOrder, - feedbackIndexes.Key, - feedbackIndexes.Value - )); - } - else - { - return null; - } - } - - /// - /// If the key contains more than two specifiers, a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if more than two specifiers are found, null otherwise. - public static ValidationFeedbackItem? MoreThanTwoSpecifierParts(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) - { - ExtractSectionResult specifierSection = SyntaxValidationUtilityMethods.ExtractSectionFromString( - validationKeyValuePair.KeyValuePair.Key, - syntaxConf.Symbols.Key.SpecifierParamStart, - syntaxConf.Symbols.Key.StringDelimeter, - syntaxConf.Symbols.Key.SpecifierParamEnd - ); - - string? specifierParamSection = specifierSection.Sections.FirstOrDefault(); - - // If no parameter section is found, there are no specifiers - if (specifierParamSection is null) - { - return null; - } - - // Extract specifiers from the specifier section - ExtractSectionResult specifierResult = SyntaxValidationUtilityMethods.ExtractSectionFromString( - specifierParamSection, - syntaxConf.Symbols.Key.StringDelimeter, - syntaxConf.Symbols.Key.StringDelimeter - ); - - if (specifierResult.Sections.Length > 2) - { - KeyValuePair secondSpecifierIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationKeyValuePair.KeyStartLineIndex, - specifierSection.StartIndexes[0] + specifierResult.StartIndexes[2], - validationKeyValuePair.LineChangeIndexes - ); - - return new ValidationFeedbackItem(validationKeyValuePair, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.TooManySpecifiers, - secondSpecifierIndexes.Key, - secondSpecifierIndexes.Value - )); - } - return null; - } - - /// - /// If there is no delimeter between specifier parts, a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if there is no delimeter between specifier parts, null otherwise. - public static ValidationFeedbackItem? NoDelimiterBetweenSpecifierParts(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) - { - ExtractSectionResult specifierSection = SyntaxValidationUtilityMethods.ExtractSectionFromString( - validationKeyValuePair.KeyValuePair.Key, - syntaxConf.Symbols.Key.SpecifierParamStart, - syntaxConf.Symbols.Key.StringDelimeter, - syntaxConf.Symbols.Key.SpecifierParamEnd - ); - - string? specifierParamSection = specifierSection.Sections.FirstOrDefault(); - - // If no specifier section is found, there are no specifiers - if (specifierParamSection is null) - { - return null; - } - - // Extract specifiers from the specifier section - ExtractSectionResult specifierResult = SyntaxValidationUtilityMethods.ExtractSectionFromString( - specifierParamSection, - syntaxConf.Symbols.Key.StringDelimeter, - syntaxConf.Symbols.Key.StringDelimeter - ); - - string[] specifiers = specifierResult.Sections; - - // If there are more than one specifier and the remainder of the specifier section does not contain a list separator, a delimiter is missing - if (specifiers.Length > 1 && !specifierResult.Remainder.Contains(syntaxConf.Symbols.Key.ListSeparator)) - { - KeyValuePair specifierIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationKeyValuePair.KeyStartLineIndex, - specifierSection.StartIndexes[0] + specifierResult.StartIndexes[1], - validationKeyValuePair.LineChangeIndexes); - - return new ValidationFeedbackItem(validationKeyValuePair, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.SpecifierDelimiterMissing, - specifierIndexes.Key, - specifierIndexes.Value)); - } - else - { - return null; - } - } - - /// - /// If any of the specifiers are not enclosed a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if specifier parts are not enclosed, null otherwise. - public static ValidationFeedbackItem? SpecifierPartNotEnclosed(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) - { - ExtractSectionResult specifierSection = SyntaxValidationUtilityMethods.ExtractSectionFromString( - validationKeyValuePair.KeyValuePair.Key, - syntaxConf.Symbols.Key.SpecifierParamStart, - syntaxConf.Symbols.Key.StringDelimeter, - syntaxConf.Symbols.Key.SpecifierParamEnd - ); - - string? specifierParamSection = specifierSection.Sections.FirstOrDefault(); - - // If specifier section is not found, there are no specifiers - if (specifierParamSection == null) - { - return null; - } - - string[] specifiers = specifierParamSection.Split(syntaxConf.Symbols.Key.ListSeparator); - // Check if specifiers are enclosed in string delimeters - for (int i = 0; i < specifiers.Length; i++) - { - string specifier = specifiers[i]; - string trimmedSpecifier = specifier.Trim(); - if (!trimmedSpecifier.StartsWith(syntaxConf.Symbols.Key.StringDelimeter) || !trimmedSpecifier.EndsWith(syntaxConf.Symbols.Key.StringDelimeter)) - { - KeyValuePair specifierIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationKeyValuePair.KeyStartLineIndex, - specifierSection.StartIndexes[0], - validationKeyValuePair.LineChangeIndexes - ); - - return new ValidationFeedbackItem(validationKeyValuePair, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.SpecifierPartNotEnclosed, - specifierIndexes.Key, - specifierIndexes.Value - )); - } - } - return null; - } - - /// - /// If the key language section contains illegal symbols, a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if the key language section contains illegal symbols, null otherwise. - public static ValidationFeedbackItem? IllegalSymbolsInLanguageParamSection(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) - { - string key = validationKeyValuePair.KeyValuePair.Key; - - ExtractSectionResult languageParam = SyntaxValidationUtilityMethods.ExtractSectionFromString( - key, - syntaxConf.Symbols.Key.LangParamStart, - syntaxConf.Symbols.Key.StringDelimeter, - syntaxConf.Symbols.Key.LangParamEnd - ); - - string? languageParamSection = languageParam.Sections.FirstOrDefault(); - - // If language section is not found, the validation is not relevant - if (languageParamSection == null) - { - return null; - } - - char[] languageIllegalSymbols = [ - syntaxConf.Symbols.Key.LangParamStart, - syntaxConf.Symbols.Key.LangParamEnd, - syntaxConf.Symbols.Key.SpecifierParamStart, - syntaxConf.Symbols.Key.SpecifierParamEnd, - syntaxConf.Symbols.Key.StringDelimeter, - syntaxConf.Symbols.Key.ListSeparator - ]; - - IEnumerable foundIllegalSymbols = languageParamSection.Where(c => languageIllegalSymbols.Contains(c)); - if (foundIllegalSymbols.Any()) - { - string foundSymbols = string.Join(", ", foundIllegalSymbols); - int indexOfFirstIllegalSymbol = languageParam.StartIndexes[0] + languageParamSection.IndexOf(foundIllegalSymbols.First()); - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationKeyValuePair.KeyStartLineIndex, - languageParam.StartIndexes[0] + indexOfFirstIllegalSymbol, - validationKeyValuePair.LineChangeIndexes); - - return new ValidationFeedbackItem(validationKeyValuePair, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.IllegalCharactersInLanguageParameter, - feedbackIndexes.Key, - feedbackIndexes.Value, - foundSymbols)); - } - else - { - return null; - } - } - - /// - /// If the key specifier section contains illegal symbols, a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if the key specifier section contains illegal symbols, null otherwise. - public static ValidationFeedbackItem? IllegalSymbolsInSpecifierParamSection(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) - { - string key = validationKeyValuePair.KeyValuePair.Key; - - ExtractSectionResult specifierParam = SyntaxValidationUtilityMethods.ExtractSectionFromString( - key, - syntaxConf.Symbols.Key.SpecifierParamStart, - syntaxConf.Symbols.Key.StringDelimeter, - syntaxConf.Symbols.Key.SpecifierParamEnd - ); - string? specifierParamSection = specifierParam.Sections.FirstOrDefault(); - - // If specifier section is not found, there are no specifiers - if (specifierParamSection == null) - { - return null; - } - - char[] specifierParamIllegalSymbols = [ - syntaxConf.Symbols.EntrySeparator, - syntaxConf.Symbols.Key.LangParamStart, - syntaxConf.Symbols.Key.LangParamEnd - ]; - - IEnumerable foundIllegalSymbols = specifierParamSection.Where(c => specifierParamIllegalSymbols.Contains(c)); - if (foundIllegalSymbols.Any()) - { - string foundSymbols = string.Join(", ", foundIllegalSymbols); - int indexOfFirstIllegalSymbol = specifierParam.StartIndexes[0] + specifierParamSection.IndexOf(foundIllegalSymbols.First()); - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationKeyValuePair.KeyStartLineIndex, - specifierParam.StartIndexes[0] + indexOfFirstIllegalSymbol, - validationKeyValuePair.LineChangeIndexes); - - return new ValidationFeedbackItem(validationKeyValuePair, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.IllegalCharactersInSpecifierParameter, - feedbackIndexes.Key, - feedbackIndexes.Value, - foundSymbols)); - } - else - { - return null; - } - } - - /// - /// If the value section is not following a valid format, a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if the value section is not following a valid format, null otherwise. - public static ValidationFeedbackItem? InvalidValueFormat(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) - { - string value = validationKeyValuePair.KeyValuePair.Value; - - // Try to parse the value section to a ValueType - ValueType? type = SyntaxValidationUtilityMethods.GetValueTypeFromString(value, syntaxConf); - - if (type is null) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationKeyValuePair.KeyStartLineIndex, - validationKeyValuePair.ValueStartIndex, - validationKeyValuePair.LineChangeIndexes); - - return new ValidationFeedbackItem(validationKeyValuePair, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.InvalidValueFormat, - feedbackIndexes.Key, - feedbackIndexes.Value, - value)); - } - - // If value is of type String or ListOfStrings, check if the line changes are compliant to the specification - if (type is ValueType.StringValue || type is ValueType.ListOfStrings) - { - int lineChangeValidityIndex = SyntaxValidationUtilityMethods.GetLineChangesValidity(value, syntaxConf, (ValueType)type); - if (lineChangeValidityIndex != -1) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationKeyValuePair.KeyStartLineIndex, - lineChangeValidityIndex, - validationKeyValuePair.LineChangeIndexes); - - return new ValidationFeedbackItem(validationKeyValuePair, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.InvalidValueFormat, - feedbackIndexes.Key, - feedbackIndexes.Value, - value)); - } - } - return null; - } - - /// - /// If the value section contains excess whitespace, a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if the value section contains excess whitespace, null otherwise. - public static ValidationFeedbackItem? ExcessWhitespaceInValue(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) - { - // Validation only matters if the value is a list of strings - if (!SyntaxValidationUtilityMethods.IsStringListFormat( - validationKeyValuePair.KeyValuePair.Value, - syntaxConf.Symbols.Value.ListSeparator, - syntaxConf.Symbols.Key.StringDelimeter - )) - { - return null; - } - - string value = validationKeyValuePair.KeyValuePair.Value; - - int firstExcessWhitespaceIndex = SyntaxValidationUtilityMethods.FirstSubstringIndexFromString( - value, - [ - $"{CharacterConstants.CHARSPACE}{CharacterConstants.CHARSPACE}", - ], - syntaxConf.Symbols.Key.StringDelimeter); - - if (firstExcessWhitespaceIndex != -1) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationKeyValuePair.KeyStartLineIndex, - validationKeyValuePair.ValueStartIndex + firstExcessWhitespaceIndex, - validationKeyValuePair.LineChangeIndexes); - - return new ValidationFeedbackItem(validationKeyValuePair, new ValidationFeedback( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.ExcessWhitespaceInValue, - feedbackIndexes.Key, - feedbackIndexes.Value, - value)); - } - - return null; - } - - /// - /// If the key contains excess whitespace, a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if the key contains excess whitespace, null otherwise. - public static ValidationFeedbackItem? KeyContainsExcessWhiteSpace(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) - { - string key = validationKeyValuePair.KeyValuePair.Key; - bool insideString = false; - bool insideSpecifierSection = false; - int i = 0; - while (i < key.Length) - { - char currentCharacter = key[i]; - if (currentCharacter == syntaxConf.Symbols.Key.StringDelimeter) - { - insideString = !insideString; - } - else if (currentCharacter == syntaxConf.Symbols.Key.SpecifierParamStart) - { - insideSpecifierSection = true; - } - else if (currentCharacter == syntaxConf.Symbols.Key.SpecifierParamEnd) - { - insideSpecifierSection = false; - } - if (!insideString && currentCharacter == CharacterConstants.SPACE) - { - // If whitespace is found outside a string or specifier section, it is considered excess whitespace - if (!insideSpecifierSection) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationKeyValuePair.KeyStartLineIndex, - i, - validationKeyValuePair.LineChangeIndexes); - - return new ValidationFeedbackItem(validationKeyValuePair, new ValidationFeedback( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.KeyContainsExcessWhiteSpace, - feedbackIndexes.Key, - feedbackIndexes.Value - )); - } - - // Only expected whitespace outside string sections within key is between specifier parts, after list separator - if (key[i - 1] != syntaxConf.Symbols.Key.ListSeparator) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationKeyValuePair.KeyStartLineIndex, - i, - validationKeyValuePair.LineChangeIndexes); - - return new ValidationFeedbackItem(validationKeyValuePair, new ValidationFeedback( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.KeyContainsExcessWhiteSpace, - feedbackIndexes.Key, - feedbackIndexes.Value - )); - } - } - i++; - } - return null; - } - - /// - /// If the value section contains excess new lines, a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if the value section contains excess new lines, null otherwise. - public static ValidationFeedbackItem? ExcessNewLinesInValue(ValidationKeyValuePair validationKeyValuePair, PxFileSyntaxConf syntaxConf) - { - // We only need to run this validation if the value is less than 150 characters long and it is a string or list - ValueType? type = SyntaxValidationUtilityMethods.GetValueTypeFromString(validationKeyValuePair.KeyValuePair.Value, syntaxConf); - const int oneLineValueLengthRecommendedLimit = 150; - - if (validationKeyValuePair.KeyValuePair.Value.Length > oneLineValueLengthRecommendedLimit || - (type != ValueType.StringValue && - type != ValueType.ListOfStrings)) - { - return null; - } - - string value = validationKeyValuePair.KeyValuePair.Value; - - int firstNewLineIndex = SyntaxValidationUtilityMethods.FirstSubstringIndexFromString( - value, - [ - CharacterConstants.WindowsNewLine, - CharacterConstants.UnixNewLine - ], - syntaxConf.Symbols.Key.StringDelimeter); - - if (firstNewLineIndex != -1) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationKeyValuePair.KeyStartLineIndex, - validationKeyValuePair.ValueStartIndex + firstNewLineIndex, - validationKeyValuePair.LineChangeIndexes); - - return new ValidationFeedbackItem(validationKeyValuePair, new ValidationFeedback( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.ExcessNewLinesInValue, - feedbackIndexes.Key, - feedbackIndexes.Value)); - } - else - { - return null; - } - } - - /// - /// If the specifier contains illegal characters, a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if the specifier contains illegal characters, null otherwise. - public static ValidationFeedbackItem? KeywordContainsIllegalCharacters(ValidationStructuredEntry validationStructuredEntry, PxFileSyntaxConf syntaxConf) - { - string keyword = validationStructuredEntry.Key.Keyword; - // Missing specifier is catched earlier - if (keyword.Length == 0) - { - return null; - } - - // Find all illegal symbols in specifier - try - { - MatchCollection matchesKeyword = keywordIllegalSymbolsRegex.Matches(keyword, 0); - IEnumerable illegalSymbolsInKeyWord = matchesKeyword.Cast().Select(m => m.Value).Distinct(); - if (illegalSymbolsInKeyWord.Any()) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationStructuredEntry.KeyStartLineIndex, - keyword.IndexOf(illegalSymbolsInKeyWord.First(), StringComparison.Ordinal), - validationStructuredEntry.LineChangeIndexes); - - return new ValidationFeedbackItem(validationStructuredEntry, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.IllegalCharactersInKeyword, - feedbackIndexes.Key, - feedbackIndexes.Value, - string.Join(", ", illegalSymbolsInKeyWord))); - } - } - catch (RegexMatchTimeoutException) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationStructuredEntry.KeyStartLineIndex, - 0, - validationStructuredEntry.LineChangeIndexes); - - return new ValidationFeedbackItem(validationStructuredEntry, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.RegexTimeout, - feedbackIndexes.Key, - feedbackIndexes.Value)); - } - - return null; - } - - /// - /// If the keyword doesn't start with a letter, a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if the specifier doesn't start with a letter, null otherwise. - public static ValidationFeedbackItem? KeywordDoesntStartWithALetter(ValidationStructuredEntry validationStructuredEntry, PxFileSyntaxConf syntaxConf) - { - string keyword = validationStructuredEntry.Key.Keyword; - - // Check if keyword starts with a letter - if (!char.IsLetter(keyword[0])) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationStructuredEntry.KeyStartLineIndex, - 0, - validationStructuredEntry.LineChangeIndexes); - - return new ValidationFeedbackItem(validationStructuredEntry, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.KeywordDoesntStartWithALetter, - feedbackIndexes.Key, - feedbackIndexes.Value - )); - } - else - { - return null; - } - } - - /// - /// If the language parameter is not following a valid format, a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if the language parameter is not following a valid format, null otherwise. - public static ValidationFeedbackItem? IllegalCharactersInLanguageParameter(ValidationStructuredEntry validationStructuredEntry, PxFileSyntaxConf syntaxConf) - { - // Running this validation is relevant only for objects with a language parameter - if (validationStructuredEntry.Key.Language is null) - { - return null; - } - - string lang = validationStructuredEntry.Key.Language; - - // Find illegal characters from language parameter string - char[] illegalCharacters = [ - syntaxConf.Symbols.Key.LangParamStart, - syntaxConf.Symbols.Key.LangParamEnd, - syntaxConf.Symbols.Key.StringDelimeter, - syntaxConf.Symbols.EntrySeparator, - syntaxConf.Symbols.KeywordSeparator, - CharacterConstants.CHARSPACE, - CharacterConstants.CHARCARRIAGERETURN, - CharacterConstants.CHARLINEFEED, - CharacterConstants.CHARHORIZONTALTAB - ]; - - IEnumerable foundIllegalCharacters = lang.Where(c => illegalCharacters.Contains(c)); - if (foundIllegalCharacters.Any()) - { - int indexOfFirstIllegalCharacter = lang.IndexOf(foundIllegalCharacters.First()); - - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationStructuredEntry.KeyStartLineIndex, - validationStructuredEntry.Key.Keyword.Length + indexOfFirstIllegalCharacter + 1, - validationStructuredEntry.LineChangeIndexes); - - string foundSymbols = string.Join(", ", foundIllegalCharacters); - return new ValidationFeedbackItem(validationStructuredEntry, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.IllegalCharactersInLanguageParameter, - feedbackIndexes.Key, - feedbackIndexes.Value, - foundSymbols)); - } - else - { - return null; - } - } - - /// - /// If the specifier parameter is not following a valid format, a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if the specifier parameter is not following a valid format, null otherwise. - public static ValidationFeedbackItem? IllegalCharactersInSpecifierParts(ValidationStructuredEntry validationStructuredEntry, PxFileSyntaxConf syntaxConf) - { - // Running this validation is relevant only for objects with a specifier - if (validationStructuredEntry.Key.FirstSpecifier is null) - { - return null; - } - - char[] illegalCharacters = [syntaxConf.Symbols.EntrySeparator, syntaxConf.Symbols.Key.StringDelimeter, syntaxConf.Symbols.Key.ListSeparator]; - IEnumerable illegalcharactersInFirstSpecifier = validationStructuredEntry.Key.FirstSpecifier.Where(c => illegalCharacters.Contains(c)); - IEnumerable illegalcharactersInSecondSpecifier = []; - if (validationStructuredEntry.Key.SecondSpecifier is not null) - { - illegalcharactersInSecondSpecifier = validationStructuredEntry.Key.SecondSpecifier.Where(c => illegalCharacters.Contains(c)); - } - - if (illegalcharactersInFirstSpecifier.Any() || illegalcharactersInSecondSpecifier.Any()) - { - int languageSectionLength = validationStructuredEntry.Key.Language is not null ? validationStructuredEntry.Key.Language.Length + 3 : 1; - - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationStructuredEntry.KeyStartLineIndex, - validationStructuredEntry.Key.Keyword.Length + languageSectionLength, - validationStructuredEntry.LineChangeIndexes); - - char[] characters = [..illegalcharactersInFirstSpecifier, ..illegalcharactersInSecondSpecifier]; - return new ValidationFeedbackItem(validationStructuredEntry, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.IllegalCharactersInSpecifierPart, - feedbackIndexes.Key, - feedbackIndexes.Value, - string.Join(", ", characters))); - } - else - { - return null; - } - } - - /// - /// If there is no value section, a new is returned. - /// - /// The validationKeyValuePair to validate. - /// The syntax configuration for the PX file. - /// A if there is no value section, null otherwise. - public static ValidationFeedbackItem? EntryWithoutValue(ValidationEntry validationEntry, PxFileSyntaxConf syntaxConf) - { - int[] keywordSeparatorIndeces = SyntaxValidationUtilityMethods.FindKeywordSeparatorIndeces(validationEntry.EntryString, syntaxConf); - - if (keywordSeparatorIndeces.Length == 0) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationEntry.KeyStartLineIndex, - validationEntry.EntryString.Length, - validationEntry.LineChangeIndexes); - - return new ValidationFeedbackItem(validationEntry, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.EntryWithoutValue, - feedbackIndexes.Key, - feedbackIndexes.Value)); - } - else if (keywordSeparatorIndeces.Length > 1) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationEntry.KeyStartLineIndex, - keywordSeparatorIndeces[1], - validationEntry.LineChangeIndexes); - - return new ValidationFeedbackItem(validationEntry, new ValidationFeedback( - ValidationFeedbackLevel.Error, - ValidationFeedbackRule.EntryWithMultipleValues, - feedbackIndexes.Key, - feedbackIndexes.Value)); - } - else - { - return null; - } - } - - /// - /// If the language parameter is not compliant with ISO 639 or BCP 47, a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if the language parameter is not compliant with ISO 639 or BCP 47, null otherwise. - public static ValidationFeedbackItem? IncompliantLanguage (ValidationStructuredEntry validationStructuredEntry, PxFileSyntaxConf syntaxConf) - { - // Running this validation is relevant only for objects with a language - if (validationStructuredEntry.Key.Language is null) - { - return null; - } - - string lang = validationStructuredEntry.Key.Language; - - bool iso639OrMsLcidCompliant = CultureInfo.GetCultures(CultureTypes.AllCultures).ToList().Exists(c => c.Name == lang || c.ThreeLetterISOLanguageName == lang); - bool bcp47Compliant = Bcp47Codes.Codes.Contains(lang); - - if (iso639OrMsLcidCompliant || bcp47Compliant) - { - return null; - } - else - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationStructuredEntry.KeyStartLineIndex, - validationStructuredEntry.Key.Keyword.Length + 1, - validationStructuredEntry.LineChangeIndexes); - - return new ValidationFeedbackItem(validationStructuredEntry, new ValidationFeedback( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.IncompliantLanguage, - feedbackIndexes.Key, - feedbackIndexes.Value - )); - } - } - - /// - /// If the specifier contains unrecommended characters, a new is returned. - /// - /// The to validate. - /// The syntax configuration for the PX file. - /// A if the specifier contains unrecommended characters, null otherwise. - public static ValidationFeedbackItem? KeywordContainsUnderscore (ValidationStructuredEntry validationStructuredEntry, PxFileSyntaxConf syntaxConf) - { - int underscoreIndex = validationStructuredEntry.Key.Keyword.IndexOf('_'); - if (underscoreIndex != -1) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationStructuredEntry.KeyStartLineIndex, - underscoreIndex, - validationStructuredEntry.LineChangeIndexes); - - return new ValidationFeedbackItem(validationStructuredEntry, new ValidationFeedback( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.KeywordContainsUnderscore, - feedbackIndexes.Key, - feedbackIndexes.Value)); - } - else - { - return null; - } - } - - /// - /// If the specifier is not in upper case, a new is returned. - /// - /// The to validate - /// The syntax configuration for the PX file. - /// A if the specifier is not in upper case, otherwise null - public static ValidationFeedbackItem? KeywordIsNotInUpperCase(ValidationStructuredEntry validationStructuredEntry, PxFileSyntaxConf syntaxConf) - { - string keyword = validationStructuredEntry.Key.Keyword; - string uppercaseKeyword = keyword.ToUpper(CultureInfo.InvariantCulture); - - if (uppercaseKeyword != keyword) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationStructuredEntry.KeyStartLineIndex, - 0, - validationStructuredEntry.LineChangeIndexes); - - return new ValidationFeedbackItem(validationStructuredEntry, new ValidationFeedback( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.KeywordIsNotInUpperCase, - feedbackIndexes.Key, - feedbackIndexes.Value)); - } - else - { - return null; - } - } - - /// - /// If the specifier is excessively long, a new is returned. - /// - /// The object to validate - /// The syntax configuration for the PX file. - /// A if the specifier is excessively long, otherwise null - public static ValidationFeedbackItem? KeywordIsExcessivelyLong(ValidationStructuredEntry validationStructuredEntry, PxFileSyntaxConf syntaxConf) - { - const int keywordLengthRecommendedLimit = 20; - string keyword = validationStructuredEntry.Key.Keyword; - - if (keyword.Length > keywordLengthRecommendedLimit) - { - KeyValuePair feedbackIndexes = SyntaxValidationUtilityMethods.GetLineAndCharacterIndex( - validationStructuredEntry.KeyStartLineIndex, - keyword.Length, - validationStructuredEntry.LineChangeIndexes); - - return new ValidationFeedbackItem(validationStructuredEntry, new ValidationFeedback( - ValidationFeedbackLevel.Warning, - ValidationFeedbackRule.KeywordExcessivelyLong, - feedbackIndexes.Key, - feedbackIndexes.Value)); - } - else - { - return null; - } - } - } -} diff --git a/Px.Utils/Validation/SyntaxValidation/SyntaxValidationResult.cs b/Px.Utils/Validation/SyntaxValidation/SyntaxValidationResult.cs index 01824898..cbb17daf 100644 --- a/Px.Utils/Validation/SyntaxValidation/SyntaxValidationResult.cs +++ b/Px.Utils/Validation/SyntaxValidation/SyntaxValidationResult.cs @@ -3,11 +3,11 @@ /// /// Represents the result of a syntax validation operation. This struct contains a validation report and a list of structured validation entries. /// - /// An array of objects produced by the syntax validation operation. + /// A dictionary of amd objects produced by the syntax validation operation. /// A list of objects produced by the syntax validation operation. /// The row number where the data section starts in the file. /// The stream position where the data section starts in the file. - public class SyntaxValidationResult(ValidationFeedbackItem[] feedbackItems, List result, int dataStartRow, int dataStartStreamPosition) : ValidationResult(feedbackItems) + public class SyntaxValidationResult(ValidationFeedback feedbacks, List result, int dataStartRow, int dataStartStreamPosition) : ValidationResult(feedbacks) { /// /// Gets the list of objects produced by the syntax validation operation. diff --git a/Px.Utils/Validation/SyntaxValidation/SyntaxValidationUtilityMethods.cs b/Px.Utils/Validation/SyntaxValidation/SyntaxValidationUtilityMethods.cs index 7cb8e51c..af60d019 100644 --- a/Px.Utils/Validation/SyntaxValidation/SyntaxValidationUtilityMethods.cs +++ b/Px.Utils/Validation/SyntaxValidation/SyntaxValidationUtilityMethods.cs @@ -2,6 +2,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Globalization; +using System.Collections.Concurrent; namespace Px.Utils.Validation.SyntaxValidation { @@ -35,7 +36,7 @@ internal readonly struct ExtractSectionResult(string[] parameters, string remain /// public static class SyntaxValidationUtilityMethods { - private static readonly Dictionary regexPatterns = []; + private static readonly ConcurrentDictionary regexPatterns = []; private static readonly TimeSpan timeout = TimeSpan.FromMilliseconds(50); /// @@ -103,12 +104,12 @@ internal static ExtractSectionResult ExtractSectionFromString(string input, char /// Returns a boolean which is true if the input string is in a list format internal static bool IsStringListFormat(string input, char listDelimeter, char stringDelimeter) { - string[] list = input.Split(listDelimeter); - if (list.Length <= 1) + List items = GetListItemsFromString(input, listDelimeter, stringDelimeter); + if (items.Count <= 1) { return false; } - else if (Array.TrueForAll(list, x => x.TrimStart().StartsWith(stringDelimeter) && x.TrimEnd().EndsWith(stringDelimeter))) + else if (Array.TrueForAll([..items], x => x.TrimStart().StartsWith(stringDelimeter) && x.TrimEnd().EndsWith(stringDelimeter))) { return true; } @@ -118,6 +119,43 @@ internal static bool IsStringListFormat(string input, char listDelimeter, char s } } + /// + /// Splits a string into a list of items using a list delimeter and a string delimeter. + /// + /// Input string + /// Character used for separating list items + /// Character used for starting and ending a string + /// List of strings that represent the listed items + internal static List GetListItemsFromString(string input, char listDelimeter, char stringDelimeter) + { + List items = []; + StringBuilder itemBuilder = new(); + bool insideString = false; + for (int i = 0; i < input.Length; i++) + { + char currentCharacter = input[i]; + if (currentCharacter == stringDelimeter) + { + insideString = !insideString; + } + if (currentCharacter == listDelimeter && !insideString) + { + items.Add(itemBuilder.ToString()); + itemBuilder.Clear(); + } + else + { + itemBuilder.Append(currentCharacter); + } + } + if (itemBuilder.Length > 0) + { + items.Add(itemBuilder.ToString()); + } + + return items; + } + /// /// Determines whether a string is in a number format. /// @@ -358,7 +396,7 @@ internal static int GetLineChangesValidity(string value, PxFileSyntaxConf syntax { insideString = !insideString; } - if (!insideString && currentCharacter == syntaxConf.Symbols.Linebreak) + if (!insideString && currentCharacter == syntaxConf.Symbols.Linebreak && i > 1) { char symbolBefore = type is ValueType.ListOfStrings ? syntaxConf.Symbols.Key.ListSeparator : @@ -375,16 +413,6 @@ internal static int GetLineChangesValidity(string value, PxFileSyntaxConf syntax } return -1; } - - internal static string CleanValue(string value, PxFileSyntaxConf syntaxConf, ValueType? type) - { - if (type == null || type == ValueType.Boolean || type == ValueType.Number) - { - return value; - } - - return CleanString(value, syntaxConf).Replace(syntaxConf.Symbols.Key.StringDelimeter.ToString(), ""); - } private static bool GetTimeValueFormat(string input, out ValueType? valueFormat, PxFileSyntaxConf syntaxConf) { diff --git a/Px.Utils/Validation/SyntaxValidation/SyntaxValidator.cs b/Px.Utils/Validation/SyntaxValidation/SyntaxValidator.cs index 90c943d3..ebd2baa8 100644 --- a/Px.Utils/Validation/SyntaxValidation/SyntaxValidator.cs +++ b/Px.Utils/Validation/SyntaxValidation/SyntaxValidator.cs @@ -1,4 +1,6 @@ using Px.Utils.PxFile; +using Px.Utils.PxFile.Metadata; +using Px.Utils.Validation.DatabaseValidation; using System.Runtime.CompilerServices; using System.Text; @@ -7,21 +9,14 @@ namespace Px.Utils.Validation.SyntaxValidation /// /// Provides methods for validating the syntax of a PX file. Validation can be done using both synchronous and asynchronous methods. /// Additionally custom validation functions can be provided to be used during validation. - /// Stream of the PX file to be validated - /// Encoding of the PX file - /// Name of the PX file /// Object that stores syntax specific symbols and tokens for the PX file /// Object that contains any optional additional validation functions - /// Boolean value that determines whether the stream should be left open after validation. /// his is required if multiple validations are executed for the same stream. /// public class SyntaxValidator( - Stream stream, - Encoding encoding, - string filename, PxFileSyntaxConf? syntaxConf = null, - CustomSyntaxValidationFunctions? customValidationFunctions = null, - bool leaveStreamOpen = false) : IPxFileValidator, IPxFileValidatorAsync + CustomSyntaxValidationFunctions? customValidationFunctions = null) + : IPxFileStreamValidator, IPxFileStreamValidatorAsync { private const int _bufferSize = 4096; private int _dataSectionStartRow = -1; @@ -30,10 +25,21 @@ public class SyntaxValidator( /// /// Validates the syntax of a PX file's metadata. /// + /// Stream of the PX file to be validated + /// Name of the PX file + /// Encoding of the PX file. If not provided, the validator attempts to find the encoding. + /// File system to use for file operations. If not provided, default file system is used. /// A entry which contains a list of entries - /// and a list of entries accumulated during the validation. - public SyntaxValidationResult Validate() + /// and a dictionary of type accumulated during the validation. + public SyntaxValidationResult Validate( + Stream stream, + string filename, + Encoding? encoding = null, + IFileSystem? fileSystem = null) { + fileSystem ??= new LocalFileSystem(); + encoding ??= fileSystem.GetEncoding(stream); + SyntaxValidationFunctions validationFunctions = new(); IEnumerable stringValidationFunctions = validationFunctions.DefaultStringValidationFunctions; IEnumerable keyValueValidationFunctions = validationFunctions.DefaultKeyValueValidationFunctions; @@ -48,25 +54,37 @@ public SyntaxValidationResult Validate() syntaxConf ??= PxFileSyntaxConf.Default; - List validationFeedback = []; + ValidationFeedback validationFeedbacks = []; List stringEntries = BuildValidationEntries(stream, encoding, syntaxConf, filename, _bufferSize); - validationFeedback.AddRange(ValidateEntries(stringEntries, stringValidationFunctions, syntaxConf)); + validationFeedbacks.AddRange(ValidateEntries(stringEntries, stringValidationFunctions, syntaxConf)); List keyValuePairs = BuildKeyValuePairs(stringEntries, syntaxConf); - validationFeedback.AddRange(ValidateKeyValuePairs(keyValuePairs, keyValueValidationFunctions, syntaxConf)); + validationFeedbacks.AddRange(ValidateKeyValuePairs(keyValuePairs, keyValueValidationFunctions, syntaxConf)); List structuredEntries = BuildValidationStructureEntries(keyValuePairs, syntaxConf); - validationFeedback.AddRange(ValidateStructs(structuredEntries, structuredValidationFunctions, syntaxConf)); + validationFeedbacks.AddRange(ValidateStructs(structuredEntries, structuredValidationFunctions, syntaxConf)); - return new SyntaxValidationResult([.. validationFeedback], structuredEntries, _dataSectionStartRow, _dataSectionStartStreamPosition); + return new SyntaxValidationResult(validationFeedbacks, structuredEntries, _dataSectionStartRow, _dataSectionStartStreamPosition); } /// /// Asynchronously validates the syntax of a PX file's metadata. /// + /// Stream of the PX file to be validated + /// Name of the PX file + /// Encoding of the PX file. If not provided, the validator attempts to find the encoding. + /// File system to use for file operations. If not provided, default file system is used. /// An optional parameter that can be used to cancel the operation. - /// A task that contains a entry, which contains the structured validation entries - /// and a list of entries accumulated during the validation. - public async Task ValidateAsync(CancellationToken cancellationToken = default) + /// A task that contains a entry which contains a list of entries + /// and a dictionary of type accumulated during the validation. + public async Task ValidateAsync( + Stream stream, + string filename, + Encoding? encoding = null, + IFileSystem? fileSystem = null, + CancellationToken cancellationToken = default) { + fileSystem ??= new LocalFileSystem(); + encoding ??= await fileSystem.GetEncodingAsync(stream, cancellationToken); + SyntaxValidationFunctions validationFunctions = new(); IEnumerable stringValidationFunctions = validationFunctions.DefaultStringValidationFunctions; IEnumerable keyValueValidationFunctions = validationFunctions.DefaultKeyValueValidationFunctions; @@ -80,78 +98,103 @@ public async Task ValidateAsync(CancellationToken cancel } syntaxConf ??= PxFileSyntaxConf.Default; - List validationFeedback = []; + ValidationFeedback validationFeedbacks = []; List entries = await BuildValidationEntriesAsync(stream, encoding, syntaxConf, filename, _bufferSize, cancellationToken); - validationFeedback.AddRange(ValidateEntries(entries, stringValidationFunctions, syntaxConf)); + validationFeedbacks.AddRange(ValidateEntries(entries, stringValidationFunctions, syntaxConf)); List keyValuePairs = BuildKeyValuePairs(entries, syntaxConf); - validationFeedback.AddRange(ValidateKeyValuePairs(keyValuePairs, keyValueValidationFunctions, syntaxConf)); + validationFeedbacks.AddRange(ValidateKeyValuePairs(keyValuePairs, keyValueValidationFunctions, syntaxConf)); List structuredEntries = BuildValidationStructureEntries(keyValuePairs, syntaxConf); - validationFeedback.AddRange(ValidateStructs(structuredEntries, structuredValidationFunctions, syntaxConf)); + validationFeedbacks.AddRange(ValidateStructs(structuredEntries, structuredValidationFunctions, syntaxConf)); - return new SyntaxValidationResult([.. validationFeedback], structuredEntries, _dataSectionStartRow, _dataSectionStartStreamPosition); + return new SyntaxValidationResult(validationFeedbacks, structuredEntries, _dataSectionStartRow, _dataSectionStartStreamPosition); } #region Interface implementation - ValidationResult IPxFileValidator.Validate() - => Validate(); + ValidationResult IPxFileStreamValidator.Validate(Stream stream, string filename, Encoding? encoding, IFileSystem? fileSystem) + => Validate(stream, filename, encoding, fileSystem); - async Task IPxFileValidatorAsync.ValidateAsync(CancellationToken cancellationToken) - => await ValidateAsync(cancellationToken); + Task IPxFileStreamValidatorAsync.ValidateAsync( + Stream stream, + string filename, + Encoding? encoding, + IFileSystem? fileSystem, + CancellationToken cancellationToken) + => ValidateAsync(stream, filename, encoding, fileSystem, cancellationToken).ContinueWith(task => (ValidationResult)task.Result); #endregion - private static List ValidateEntries(IEnumerable entries, IEnumerable validationFunctions, PxFileSyntaxConf syntaxConf) + /// + /// Checks whether the end of the metadata section of a px file stream has been reached. + /// + /// Character currently being processed + /// object that contains symbols and tokens used for px file syntax + /// object that contains the currently processed px file entry + /// Whether the character currently processed is enclosed between string delimiters + /// True if the end of the metadata section has been reached, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsEndOfMetadataSection(char currentCharacter, PxFileSyntaxConf syntaxConf, StringBuilder entryBuilder, bool isProcessingString) { - List validationFeedback = []; + if (!isProcessingString && currentCharacter == syntaxConf.Symbols.KeywordSeparator) + { + string stringEntry = entryBuilder.ToString(); + // When DATA keyword is reached, metadata parsing is complete + return SyntaxValidationUtilityMethods.CleanString(stringEntry, syntaxConf).Equals(syntaxConf.Tokens.KeyWords.Data, StringComparison.Ordinal); + } + return false; + } + + private static ValidationFeedback ValidateEntries(IEnumerable entries, IEnumerable validationFunctions, PxFileSyntaxConf syntaxConf) + { + ValidationFeedback validationFeedback = []; foreach (ValidationEntry entry in entries) { foreach (EntryValidationFunction function in validationFunctions) { - ValidationFeedbackItem? feedback = function(entry, syntaxConf); + KeyValuePair? feedback = function(entry, syntaxConf); if (feedback is not null) { - validationFeedback.Add((ValidationFeedbackItem)feedback); + validationFeedback.Add((KeyValuePair )feedback); } } } return validationFeedback; } - private static List ValidateKeyValuePairs( + private static ValidationFeedback ValidateKeyValuePairs( IEnumerable kvpObjects, IEnumerable validationFunctions, PxFileSyntaxConf syntaxConf) { - List validationFeedback = []; + ValidationFeedback validationFeedback = []; foreach (ValidationKeyValuePair kvpObject in kvpObjects) { foreach (KeyValuePairValidationFunction function in validationFunctions) { - ValidationFeedbackItem? feedback = function(kvpObject, syntaxConf); + KeyValuePair? feedback = function(kvpObject, syntaxConf); if (feedback is not null) { - validationFeedback.Add((ValidationFeedbackItem)feedback); + validationFeedback.Add((KeyValuePair)feedback); } } } return validationFeedback; } - private static List ValidateStructs( + private static ValidationFeedback ValidateStructs( IEnumerable structuredEntries, IEnumerable validationFunctions, PxFileSyntaxConf syntaxConf) { - List validationFeedback = []; + ValidationFeedback validationFeedback = []; foreach (ValidationStructuredEntry structuredEntry in structuredEntries) { foreach (StructuredValidationFunction function in validationFunctions) { - ValidationFeedbackItem? feedback = function(structuredEntry, syntaxConf); + KeyValuePair? feedback = function(structuredEntry, syntaxConf); if (feedback is not null) { - validationFeedback.Add((ValidationFeedbackItem)feedback); + validationFeedback.Add((KeyValuePair)feedback); } } } @@ -165,6 +208,11 @@ private static List BuildKeyValuePairs(List(entry.EntryString, string.Empty), entry.KeyStartLineIndex, entry.LineChangeIndexes, -1); + } + // Key is the part of the entry string before the first keyword separator index string key = entry.EntryString[..keywordSeparatorIndeces[0]]; @@ -182,8 +230,7 @@ private static List BuildValidationStructureEntries(L { ValueType? valueType = SyntaxValidationUtilityMethods.GetValueTypeFromString(entry.KeyValuePair.Value, syntaxConf); ValidationStructuredEntryKey key = ParseStructuredValidationEntryKey(entry.KeyValuePair.Key, syntaxConf); - string value = SyntaxValidationUtilityMethods.CleanValue(entry.KeyValuePair.Value, syntaxConf, valueType); - return new ValidationStructuredEntry(entry.File, key, value, entry.KeyStartLineIndex, entry.LineChangeIndexes, entry.ValueStartIndex, valueType); + return new ValidationStructuredEntry(entry.File, key, entry.KeyValuePair.Value, entry.KeyStartLineIndex, entry.LineChangeIndexes, entry.ValueStartIndex, valueType); }).ToList(); } @@ -194,12 +241,10 @@ private List BuildValidationEntries(Stream stream, Encoding enc List lineChangeIndexes = []; int entryStartIndex = 0; int entryStartLineIndex = 0; - - using StreamReader reader = new(stream, encoding, leaveOpen: leaveStreamOpen); + using StreamReader reader = new(stream, encoding, leaveOpen: true); List entries = []; StringBuilder entryBuilder = new(); char[] buffer = new char[bufferSize]; - while (reader.Read(buffer, 0, bufferSize) > 0) { for (int i = 0; i < buffer.Length; i++) @@ -254,7 +299,7 @@ private async Task> BuildValidationEntriesAsync( int entryStartIndex = 0; int entryStartLineIndex = 0; - using StreamReader reader = new(stream, encoding, leaveOpen: leaveStreamOpen); + using StreamReader reader = new(stream, encoding, leaveOpen: true); List entries = []; StringBuilder entryBuilder = new(); char[] buffer = new char[bufferSize]; @@ -291,18 +336,6 @@ private async Task> BuildValidationEntriesAsync( return entries; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsEndOfMetadataSection(char currentCharacter, PxFileSyntaxConf syntaxConf, StringBuilder entryBuilder, bool isProcessingString) - { - if (!isProcessingString && currentCharacter == syntaxConf.Symbols.KeywordSeparator) - { - string stringEntry = entryBuilder.ToString(); - // When DATA keyword is reached, metadata parsing is complete - return SyntaxValidationUtilityMethods.CleanString(stringEntry, syntaxConf).Equals(syntaxConf.Tokens.KeyWords.Data, StringComparison.Ordinal); - } - return false; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void UpdateLineAndCharacter(char currentCharacter, PxFileSyntaxConf syntaxConf, ref int characterIndex, ref List linebreakIndexes, ref bool isProcessingString) { diff --git a/Px.Utils/Validation/ValidationFeedback.cs b/Px.Utils/Validation/ValidationFeedback.cs index 24232b57..6710ccfa 100644 --- a/Px.Utils/Validation/ValidationFeedback.cs +++ b/Px.Utils/Validation/ValidationFeedback.cs @@ -1,9 +1,54 @@ -namespace Px.Utils.Validation +using System.Collections.Concurrent; + +namespace Px.Utils.Validation { /// - /// Validation feedback represents the result of a validation operation. + /// Represents the result of a validation operation. Associates feedback levels and rules with instances of violations. + /// + public class ValidationFeedback() : ConcurrentDictionary> + { + /// + /// Adds a feedback item to the feedback dictionary. + /// + public void Add(KeyValuePair item) + { + if (!ContainsKey(item.Key)) + { + this[item.Key] = []; + } + this[item.Key].Add(item.Value); + } + + /// + /// Adds multiple feedback items to the feedback dictionary. + /// + /// Feedback key value pairs to add + public void AddRange(ConcurrentDictionary> feedbacks) + { + foreach (KeyValuePair> kvp in feedbacks) + { + if (!ContainsKey(kvp.Key)) + { + this[kvp.Key] = []; + } + this[kvp.Key].AddRange(kvp.Value); + } + } + + /// + /// Constructor that initializes the feedback dictionary with a single feedback item. + /// + /// Feedback key value pair to add + public ValidationFeedback(KeyValuePair item) : this() + { + Add(item); + } + } + + /// + /// Validation feedback contains information about validation errors or warnings that occurred during validation. /// - public readonly struct ValidationFeedback(ValidationFeedbackLevel level, ValidationFeedbackRule rule, int line = 0, int character = 0, string? additionalInfo = null) + public readonly struct ValidationFeedbackKey(ValidationFeedbackLevel level, ValidationFeedbackRule rule) { /// /// Enum that gets the level of the feedback. This can be used to categorize feedback items by severity. @@ -14,19 +59,27 @@ public readonly struct ValidationFeedback(ValidationFeedbackLevel level, Validat /// Enum that defines the type of validation feedback rule. Can be used to categorize feedback by rule type or for translations. /// public ValidationFeedbackRule Rule { get; } = rule; + } + /// + /// Stores information about a specific instance of a validation feedback rule violation. + /// + public readonly struct ValidationFeedbackValue(string filename, int? line = null, int? character = null, string? additionalInfo = null) + { /// - /// Index of the line the feedback is associated with. + /// Name of the file where the violation occurred. /// - public int Line { get; } = line; - + public string Filename { get; } = filename; /// - /// Index of the character where the issue related to the feedback starts. + /// Line number where the violation occurred. /// - public int Character { get; } = character; - + public int? Line { get; } = line; + /// + /// Character position where the violation occurred. + /// + public int? Character { get; } = character; /// - /// Any additional information can be stored into this property. + /// Additional information about the violation. /// public string? AdditionalInfo { get; } = additionalInfo; } diff --git a/Px.Utils/Validation/ValidationFeedbackItem.cs b/Px.Utils/Validation/ValidationFeedbackItem.cs deleted file mode 100644 index 4a1ab9a4..00000000 --- a/Px.Utils/Validation/ValidationFeedbackItem.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Px.Utils.Validation -{ - /// - /// A validation feedback item associates a with the feedback from validating that object. - /// - /// The that this feedback item is associated with. - /// The from validating the object. - public readonly struct ValidationFeedbackItem(ValidationObject validationObject, ValidationFeedback feedback) - { - - /// - /// Gets the that this feedback item is associated with. - /// - public ValidationObject ValidationObject { get; } = validationObject; - - - /// - /// Gets the from validating the ValidationObject. - /// - public ValidationFeedback Feedback { get; } = feedback; - } -} diff --git a/Px.Utils/Validation/ValidationObject.cs b/Px.Utils/Validation/ValidationObject.cs index 63bab6b0..893d6e9d 100644 --- a/Px.Utils/Validation/ValidationObject.cs +++ b/Px.Utils/Validation/ValidationObject.cs @@ -16,7 +16,7 @@ public class ValidationObject(string file, int keyStartLineIndex, int[] lineChan public int KeyStartLineIndex { get; } = keyStartLineIndex; /// - /// Character indexes of the line changes in the entry starting from the entry start- + /// Character indexes of the line changes in the entry starting from the entry start. /// public int[] LineChangeIndexes { get; } = lineChangeIndexes; } diff --git a/docs/README.md b/docs/README.md index 1e24c75a..0c4eb76d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -42,27 +42,24 @@ TBA #### Px file validation Px files can be validated either as a whole by using PxFileValidator or by using individual validators - SyntaxValidator, ContentValidator and DataValidator - for different parts of the file. Custom validation functions or validator classes can be added to the validation processes. - -##### PxFileValidator -PxFileValidator is a class that validates the whole px file including its data, metadata syntax and metadata contents. The class can be instantiated with the following parameters: +Validator classes implement either IPxFileStreamValidator or IPxFileStreamValidatorAsync interfaces for synchronous and asynchronous validation processes respectively. Custom validation functions must implement either the IPxFileValidator or IPxFileValidatorAsync, IValidator or IValidatorAsync interfaces. +IPxFileStreamValidator and IPxFileStreamValidatorAsync interfaces required the following parameters to run their Validate or ValidateAsync functions: - stream (Stream): The stream of the px file to be validated - filename (string): Name of the file to be validated - encoding (Encoding, optional): Encoding of the px file. Default is Encoding.Default -- syntaxConf (PxFileSyntaxConf, optional): Object that contains px file syntax configuration tokens and symbols. +- fileSystem (IFileSystem, optional): Object that defines the file system used for the validation process. Default file called LocalFileSystem system is used if none provided. +##### PxFileValidator (IPxFileStreamValidator, IPxFileStreamValidatorAsync) +PxFileValidator is a class that validates the whole px file including its data, metadata syntax and metadata contents. The class can be instantiated with the following parameters: +- syntaxConf (PxFileSyntaxConf, optional): Object that contains px file syntax configuration tokens and symbols. Custom validator objects can be injected by calling the SetCustomValidatorFunctions or SetCustomValidators methods of the PxFileValidator object. Custom validators must implement either the IPxFileValidator or IPxFileValidatorAsync interface. Custom validation methods are stored in CustomSyntaxValidationFunctions and CustomContentValidationFunctions objects for syntax and content validation processes respectively. - -Once the PxFileValidator object is instantiated, either the Validate or ValidateAsync method can be called to validate the px file. The Validate method returns a ValidationResult object that contains the validation results as ValidationFeedbackItem object array. +Once the PxFileValidator object is instantiated, either the Validate or ValidateAsync method can be called to validate the px file. The Validate method returns a ValidationResult object that contains the validation results as a key value pair containing information about the rule violations. ##### SyntaxValidator SyntaxValidator is a class that validates the syntax of a px file's metadata. It needs to be run before other validators, because both the ContentValidator and DataValidator require information from the SyntaxValidationResult object that SyntaxValidator Validate and ValidateAsync methods return. The class can be instantiated with the following parameters: -- stream (Stream): The stream of the px file to be validated -- encoding (Encoding): Encoding of the px file. -- filename (string): Name of the file to be validated - syntaxConf (PxFileSyntaxConf, optional): Object that contains px file syntax configuration tokens and symbols. - customValidationFunctions (CustomSyntaxValidationFunctions, optional): Object that contains custom validation functions for the syntax validation process. -- leaveStreamOpen (bool, optional): If true, the stream will not be closed after the validation process. Default is false. ##### ContentValidator ContentValidator class validates the integrity of the contents of a px file's metadata. It needs to be run after the SyntaxValidator, because it requires information from the SyntaxValidationResult object that SyntaxValidator Validate and ValidateAsync methods return. @@ -76,17 +73,23 @@ The class can be instantiated with the following parameters: ##### DataValidator DataValidator class is used to validate the data section of a px file. It needs to be run after the SyntaxValidator, because it requires information from both the SyntaxValidationResult and ContentValidationResult objects that SyntaxValidator and ContentValidator Validate and ValidateAsync methods return. The class can be instantiated with the following parameters: -- stream (Stream): Px file stream to be validated - rowLen (int): Length of one row of Px file data. ContentValidationResult object contains this information. - numOfRows (int): Amount of rows of Px file data. This information is also stored in ContentValidationResult object. -- filename (string): Name of the file being validated - startRow (long): The row number where the data section starts. This information is stored in the SyntaxValidationResult object. -- encoding (Encoding, optional): Encoding of the stream - conf (PxFileSyntaxConf, optional): Syntax configuration for the Px file #### Database validation -TBA - +Whole px file databases can be validated using DatabaseValidator class. Validation can be done by using the blocking Validate or asynchronous ValidateAsync methods. DatabaseValidator class can be instantiated using the following parameters: +- directoryPath (string): Path to the database root +- syntaxConf (PxFileSyntaxConf, optional): Syntax configuration for the Px file +- fileSystem (IFileSystem, optional): Object that defines the file system used for the validation process. Default file system is used if none provided +- customPxFileValidators (IDatabaseValidator, optional): Object containing validator functions ran for each px file within the database +- customAliasFileValidators (IDatabaseValidator, optional): Object containing validator functions ran for each alias file within the database +- customDirectoryValidators (IDatabaseValidator, optional): Object containing validator functions ran for each subdirectory within the database + +Database validation process validates each px file within the database and also the required structure and consistency of the database languages and encoding formats. The return object is a ValidationResult object that contains ValidationFeedback objects gathered during the validation process. +The database needs to contain alias files for each language used in the database for each folder that contains either subcategory folders or px files. If either languages or encoding formats differ between alias or px files, warnings are generated. + ### Data models TBA