From b2a2aefeb5593fed457b49bd04797e51effb0181 Mon Sep 17 00:00:00 2001 From: abaskk-msft Date: Tue, 23 Jul 2024 15:21:11 -0700 Subject: [PATCH] Make YamlValidator Producting Ready (#700) --- src/PASopa.sln | 13 +++++ ...PowerPlatform.PowerApps.Persistence.csproj | 16 +----- src/Persistence/YamlValidator/Constants.cs | 18 ------- src/Persistence/YamlValidator/SchemaLoader.cs | 29 ---------- .../YamlValidator/ValidationProcessor.cs | 40 -------------- .../YamlValidator/VerbosityData.cs | 25 --------- src/Persistence/YamlValidator/YamlLoader.cs | 49 ----------------- src/YamlValidator.Tests/TestBase.cs | 38 +++++++++++++ src/YamlValidator.Tests/VSTestBase.cs | 16 ++++++ .../ValidatorFactoryTest.cs | 20 +++++++ .../ValidatorTest.cs | 26 ++++----- .../YamlValidator.Tests.csproj | 39 ++++++++++++++ .../ControlWithInvalidProperty.yaml | 0 .../_TestData}/InvalidYaml/Empty.yaml | 0 .../_TestData}/InvalidYaml/EmptyArray.yaml | 0 .../InvalidYaml/NamelessObjectNoControl.yaml | 0 .../_TestData/InvalidYaml/NotYaml.yaml | 15 ++++++ .../InvalidYaml/ScreenWithNameNoColon.yaml | 0 .../InvalidYaml/ScreenWithNameNoValue.yaml | 0 .../ScreenWithoutControlProperty.yaml | 0 .../InvalidYaml/WrongControlDefinition.yaml | 0 .../ValidYaml/NamelessObjectWithControl.yaml | 0 .../SimpleNoRecursiveDefinition.yaml | 0 .../_TestData}/ValidYaml/ValidScreen1.yaml | 0 src/YamlValidator/Constants.cs | 15 ++++++ .../Extensions/ServiceCollectionExtensions.cs | 18 +++++++ .../IValidator.cs} | 5 +- src/YamlValidator/IValidatorFactory.cs | 9 ++++ src/YamlValidator/InternalsVisibleTo.cs | 6 +++ ...werPlatform.PowerApps.YamlValidator.csproj | 38 +++++++++++++ src/YamlValidator/SchemaLoader.cs | 53 +++++++++++++++++++ .../YamlValidator/Utility.cs | 6 --- .../YamlValidator/Validator.cs | 41 +++++++------- .../YamlValidator/ValidatorError.cs | 11 ++-- src/YamlValidator/ValidatorFactory.cs | 24 +++++++++ .../YamlValidator/ValidatorResults.cs | 1 + 36 files changed, 350 insertions(+), 221 deletions(-) delete mode 100644 src/Persistence/YamlValidator/Constants.cs delete mode 100644 src/Persistence/YamlValidator/SchemaLoader.cs delete mode 100644 src/Persistence/YamlValidator/ValidationProcessor.cs delete mode 100644 src/Persistence/YamlValidator/VerbosityData.cs delete mode 100644 src/Persistence/YamlValidator/YamlLoader.cs create mode 100644 src/YamlValidator.Tests/TestBase.cs create mode 100644 src/YamlValidator.Tests/VSTestBase.cs create mode 100644 src/YamlValidator.Tests/ValidatorFactoryTest.cs rename src/{Persistence.Tests/YamlValidator => YamlValidator.Tests}/ValidatorTest.cs (58%) create mode 100644 src/YamlValidator.Tests/YamlValidator.Tests.csproj rename src/{Persistence.Tests/_TestData/ValidatorTests => YamlValidator.Tests/_TestData}/InvalidYaml/ControlWithInvalidProperty.yaml (100%) rename src/{Persistence.Tests/_TestData/ValidatorTests => YamlValidator.Tests/_TestData}/InvalidYaml/Empty.yaml (100%) rename src/{Persistence.Tests/_TestData/ValidatorTests => YamlValidator.Tests/_TestData}/InvalidYaml/EmptyArray.yaml (100%) rename src/{Persistence.Tests/_TestData/ValidatorTests => YamlValidator.Tests/_TestData}/InvalidYaml/NamelessObjectNoControl.yaml (100%) create mode 100644 src/YamlValidator.Tests/_TestData/InvalidYaml/NotYaml.yaml rename src/{Persistence.Tests/_TestData/ValidatorTests => YamlValidator.Tests/_TestData}/InvalidYaml/ScreenWithNameNoColon.yaml (100%) rename src/{Persistence.Tests/_TestData/ValidatorTests => YamlValidator.Tests/_TestData}/InvalidYaml/ScreenWithNameNoValue.yaml (100%) rename src/{Persistence.Tests/_TestData/ValidatorTests => YamlValidator.Tests/_TestData}/InvalidYaml/ScreenWithoutControlProperty.yaml (100%) rename src/{Persistence.Tests/_TestData/ValidatorTests => YamlValidator.Tests/_TestData}/InvalidYaml/WrongControlDefinition.yaml (100%) rename src/{Persistence.Tests/_TestData/ValidatorTests => YamlValidator.Tests/_TestData}/ValidYaml/NamelessObjectWithControl.yaml (100%) rename src/{Persistence.Tests/_TestData/ValidatorTests => YamlValidator.Tests/_TestData}/ValidYaml/SimpleNoRecursiveDefinition.yaml (100%) rename src/{Persistence.Tests/_TestData/ValidatorTests => YamlValidator.Tests/_TestData}/ValidYaml/ValidScreen1.yaml (100%) create mode 100644 src/YamlValidator/Constants.cs create mode 100644 src/YamlValidator/Extensions/ServiceCollectionExtensions.cs rename src/{Persistence/YamlValidator/ValidationRequest.cs => YamlValidator/IValidator.cs} (62%) create mode 100644 src/YamlValidator/IValidatorFactory.cs create mode 100644 src/YamlValidator/InternalsVisibleTo.cs create mode 100644 src/YamlValidator/Microsoft.PowerPlatform.PowerApps.YamlValidator.csproj create mode 100644 src/YamlValidator/SchemaLoader.cs rename src/{Persistence => }/YamlValidator/Utility.cs (73%) rename src/{Persistence => }/YamlValidator/Validator.cs (55%) rename src/{Persistence => }/YamlValidator/ValidatorError.cs (81%) create mode 100644 src/YamlValidator/ValidatorFactory.cs rename src/{Persistence => }/YamlValidator/ValidatorResults.cs (94%) diff --git a/src/PASopa.sln b/src/PASopa.sln index a7347547..b8fea533 100644 --- a/src/PASopa.sln +++ b/src/PASopa.sln @@ -26,6 +26,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7361DB16-D53 Versions.props = Versions.props EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YamlValidator", "YamlValidator\Microsoft.PowerPlatform.PowerApps.YamlValidator.csproj", "{296D952B-C284-4AAB-9A79-59538CA7BF38}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YamlValidator.Tests", "YamlValidator.Tests\YamlValidator.Tests.csproj", "{E6180C81-59C1-4B25-90E0-E4390D455518}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -52,6 +56,14 @@ Global {8AB1C901-FE5E-44BF-AA21-B8F20A9D7CDD}.Debug|Any CPU.Build.0 = Debug|Any CPU {8AB1C901-FE5E-44BF-AA21-B8F20A9D7CDD}.Release|Any CPU.ActiveCfg = Release|Any CPU {8AB1C901-FE5E-44BF-AA21-B8F20A9D7CDD}.Release|Any CPU.Build.0 = Release|Any CPU + {296D952B-C284-4AAB-9A79-59538CA7BF38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {296D952B-C284-4AAB-9A79-59538CA7BF38}.Debug|Any CPU.Build.0 = Debug|Any CPU + {296D952B-C284-4AAB-9A79-59538CA7BF38}.Release|Any CPU.ActiveCfg = Release|Any CPU + {296D952B-C284-4AAB-9A79-59538CA7BF38}.Release|Any CPU.Build.0 = Release|Any CPU + {E6180C81-59C1-4B25-90E0-E4390D455518}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6180C81-59C1-4B25-90E0-E4390D455518}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6180C81-59C1-4B25-90E0-E4390D455518}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6180C81-59C1-4B25-90E0-E4390D455518}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -60,6 +72,7 @@ Global {8AD94CC0-7330-4880-A8E0-177B37CDB27B} = {4993E606-484B-46D9-892E-7AE9CE8D4423} {8AB1C901-FE5E-44BF-AA21-B8F20A9D7CDD} = {4993E606-484B-46D9-892E-7AE9CE8D4423} {7361DB16-D534-4E0E-8597-BE22317DEF47} = {794D8C68-BF6F-49C8-BCA5-AA52D8F45EF4} + {E6180C81-59C1-4B25-90E0-E4390D455518} = {4993E606-484B-46D9-892E-7AE9CE8D4423} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C936F8B1-DE7A-4401-95D5-5E199210F960} diff --git a/src/Persistence/Microsoft.PowerPlatform.PowerApps.Persistence.csproj b/src/Persistence/Microsoft.PowerPlatform.PowerApps.Persistence.csproj index 7f978dd4..f4748665 100644 --- a/src/Persistence/Microsoft.PowerPlatform.PowerApps.Persistence.csproj +++ b/src/Persistence/Microsoft.PowerPlatform.PowerApps.Persistence.csproj @@ -32,7 +32,7 @@ true true - $(NoWarn);NU1601;CA1822 + $(NoWarn);NU1601 @@ -43,18 +43,6 @@ - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/Persistence/YamlValidator/Constants.cs b/src/Persistence/YamlValidator/Constants.cs deleted file mode 100644 index c750ab6a..00000000 --- a/src/Persistence/YamlValidator/Constants.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; - -public static class Constants -{ - public const string FileTypeName = "file"; - public const string FolderTypeName = "folder"; - public const string YamlFileExtension = ".pa.yaml"; - public const string JsonFileExtension = ".json"; - - public const string Verbose = "verbose"; - - // runtime constants - // default schema path - public static readonly string DefaultSchemaPath = Path.Combine(".", "schema", "pa.yaml-schema.json"); -} diff --git a/src/Persistence/YamlValidator/SchemaLoader.cs b/src/Persistence/YamlValidator/SchemaLoader.cs deleted file mode 100644 index 48e65628..00000000 --- a/src/Persistence/YamlValidator/SchemaLoader.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Json.Schema; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; - -public class SchemaLoader -{ - private const string _schemaFolderPath = "subschemas"; - private static readonly string _schemaPath = Path.Combine(".", "YamlValidator", "schema", "pa.yaml-schema.json"); - - public JsonSchema Load() - { - var node = JsonSchema.FromFile(_schemaPath); - var schemaFolder = Path.GetDirectoryName(_schemaPath); - var subschemaPaths = Directory.GetFiles($@"{schemaFolder}{Path.DirectorySeparatorChar}{_schemaFolderPath}", - $"*{Constants.JsonFileExtension}"); - - foreach (var path in subschemaPaths) - { - var subschema = JsonSchema.FromFile(path); - SchemaRegistry.Global.Register(subschema); - } - - return node; - } -} - diff --git a/src/Persistence/YamlValidator/ValidationProcessor.cs b/src/Persistence/YamlValidator/ValidationProcessor.cs deleted file mode 100644 index 85f173c2..00000000 --- a/src/Persistence/YamlValidator/ValidationProcessor.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; - -public class ValidationProcessor -{ - private readonly YamlLoader _fileLoader; - private readonly SchemaLoader _schemaLoader; - private readonly Validator _validator; - - public ValidationProcessor(YamlLoader fileLoader, SchemaLoader schemaLoader, Validator validator) - { - _fileLoader = fileLoader; - _schemaLoader = schemaLoader; - _validator = validator; - } - - public void RunValidation(ValidationRequest inputData) - { - var path = inputData.FilePath; - var pathType = inputData.FilePathType; - - var yamlData = _fileLoader.Load(path, pathType); - var serializedSchema = _schemaLoader.Load(); - - foreach (var yamlFileData in yamlData) - { - Console.WriteLine($"Validating '{yamlFileData.Key}'"); - var result = _validator.Validate(serializedSchema, yamlFileData.Value); - Console.WriteLine($"Validation {(result.SchemaValid ? "Passed" : "Failed")}"); - - foreach (var error in result.TraversalResults) - { - Console.WriteLine($"{error}"); - } - Console.WriteLine(); - } - } -} diff --git a/src/Persistence/YamlValidator/VerbosityData.cs b/src/Persistence/YamlValidator/VerbosityData.cs deleted file mode 100644 index d8383614..00000000 --- a/src/Persistence/YamlValidator/VerbosityData.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -using System.Text.Json; -using Json.Schema; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; -public readonly record struct VerbosityData -{ - public EvaluationOptions EvalOptions { get; } - public JsonSerializerOptions JsonOutputOptions { get; } - - public VerbosityData(string verbosityLevel) - { - EvalOptions = new EvaluationOptions(); - JsonOutputOptions = new JsonSerializerOptions { Converters = { new EvaluationResultsJsonConverter() } }; - - if (verbosityLevel == Constants.Verbose) - { - EvalOptions.OutputFormat = OutputFormat.List; - return; - } - EvalOptions.OutputFormat = OutputFormat.Flag; - } -} - diff --git a/src/Persistence/YamlValidator/YamlLoader.cs b/src/Persistence/YamlValidator/YamlLoader.cs deleted file mode 100644 index 8e8d94e3..00000000 --- a/src/Persistence/YamlValidator/YamlLoader.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.ObjectModel; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; - -public class YamlLoader -{ - public IReadOnlyDictionary Load(string filePath, string pathType) - { - var deserializedYaml = new Dictionary(); - - if (pathType == Constants.FileTypeName) - { - var fileName = Path.GetFileName(filePath); - var yamlText = Utility.ReadFileData(filePath); - deserializedYaml.Add(fileName, yamlText); - } - else if (pathType == Constants.FolderTypeName) - { - // TODO: Determine if argument flag should be required to specify recursive folder search - try - { - var yamlFiles = Directory.EnumerateFiles(filePath, "*" + Constants.YamlFileExtension, SearchOption.AllDirectories); - foreach (var filename in yamlFiles) - { - var fileName = Path.GetFullPath(filename); - var yamlText = Utility.ReadFileData(filename); - deserializedYaml.Add(fileName, yamlText); - } - } - catch (UnauthorizedAccessException ex) - { - Console.WriteLine($"Unauthorized access exception: {ex.Message}"); - } - catch (IOException ex) - { - Console.WriteLine($"IO exception: {ex.Message}"); - } - } - else - { - throw new ArgumentException("Invalid path type"); - } - - return new ReadOnlyDictionary(deserializedYaml); - } -} diff --git a/src/YamlValidator.Tests/TestBase.cs b/src/YamlValidator.Tests/TestBase.cs new file mode 100644 index 00000000..90349fa7 --- /dev/null +++ b/src/YamlValidator.Tests/TestBase.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; + + +namespace Persistence.YamlValidator.Tests; +public abstract class TestBase : VSTestBase +{ + public static IServiceProvider ServiceProvider { get; set; } + public IValidatorFactory ValidatorFactory { get; private set; } + + static TestBase() + { + ServiceProvider = BuildServiceProvider(); + } + + public TestBase() + { + ValidatorFactory = ServiceProvider.GetRequiredService(); + } + + private static ServiceProvider BuildServiceProvider() + { + var serviceCollection = new ServiceCollection(); + var serviceProvider = ConfigureServices(serviceCollection); + + return serviceProvider; + } + + private static ServiceProvider ConfigureServices(IServiceCollection services) + { + services.AddPowerAppsPersistenceYamlValidator(); + + return services.BuildServiceProvider(); + } + +} diff --git a/src/YamlValidator.Tests/VSTestBase.cs b/src/YamlValidator.Tests/VSTestBase.cs new file mode 100644 index 00000000..d2a32f09 --- /dev/null +++ b/src/YamlValidator.Tests/VSTestBase.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Persistence.YamlValidator.Tests; + +/// +/// Represents a shared base class for any test class that uses the Visual Studio Test Framework. +/// +/// +/// DO NOT add any setup/tear down logic to this class, as not all tests may require it. +/// The preferred approach is to use a different derived base class for tests that require setup/tear down logic specific to some shared scenarios. +/// +public abstract class VSTestBase +{ + public required TestContext TestContext { get; set; } +} diff --git a/src/YamlValidator.Tests/ValidatorFactoryTest.cs b/src/YamlValidator.Tests/ValidatorFactoryTest.cs new file mode 100644 index 00000000..1c6c3cd3 --- /dev/null +++ b/src/YamlValidator.Tests/ValidatorFactoryTest.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; + +namespace Persistence.YamlValidator.Tests; + +[TestClass] +public class ValidatorFactoryTest : TestBase +{ + [TestMethod] + public void GetValidatorTest() + { + var factory = new ValidatorFactory(); + var validator = factory.CreateValidator(); + + Assert.IsNotNull(validator); + Assert.IsInstanceOfType(validator, typeof(IValidator)); + } +} diff --git a/src/Persistence.Tests/YamlValidator/ValidatorTest.cs b/src/YamlValidator.Tests/ValidatorTest.cs similarity index 58% rename from src/Persistence.Tests/YamlValidator/ValidatorTest.cs rename to src/YamlValidator.Tests/ValidatorTest.cs index 20030b95..56f99dc7 100644 --- a/src/Persistence.Tests/YamlValidator/ValidatorTest.cs +++ b/src/YamlValidator.Tests/ValidatorTest.cs @@ -1,30 +1,25 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Json.Schema; using Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; -namespace Persistence.Tests.YamlValidator; +namespace Persistence.YamlValidator.Tests; [TestClass] -public class ValidatorTest +public class ValidatorTest : TestBase { - private static readonly string _validPath = Path.Combine(".", "_TestData", "ValidatorTests", "ValidYaml") + + private static readonly string _validPath = Path.Combine(".", "_TestData", "ValidYaml") + Path.DirectorySeparatorChar; - private static readonly string _invalidPath = Path.Combine(".", "_TestData", "ValidatorTests", "InvalidYaml") + + private static readonly string _invalidPath = Path.Combine(".", "_TestData", "InvalidYaml") + Path.DirectorySeparatorChar; - private readonly JsonSchema _schema; - private readonly Validator _yamlValidator; + private readonly IValidator _yamlValidator; public ValidatorTest() { - var schemaFileLoader = new SchemaLoader(); - _schema = schemaFileLoader.Load(); - var resultVerbosity = new VerbosityData(Constants.Verbose); - _yamlValidator = new Validator(resultVerbosity.EvalOptions, resultVerbosity.JsonOutputOptions); + _yamlValidator = ValidatorFactory.CreateValidator(); } [TestMethod] @@ -34,8 +29,8 @@ public ValidatorTest() public void TestValidationValidYaml(string filename) { - var rawYaml = Utility.ReadFileData($@"{_validPath}{filename}"); - var result = _yamlValidator.Validate(_schema, rawYaml); + var rawYaml = File.ReadAllText($@"{_validPath}{filename}"); + var result = _yamlValidator.Validate(rawYaml); Assert.IsTrue(result.SchemaValid); } @@ -48,10 +43,11 @@ public void TestValidationValidYaml(string filename) [DataRow("EmptyArray.yaml")] [DataRow("Empty.yaml")] [DataRow("NamelessObjectNoControl.yaml")] + [DataRow("NotYaml.yaml")] public void TestValidationInvalidYaml(string filename) { - var rawYaml = Utility.ReadFileData($@"{_invalidPath}{filename}"); - var result = _yamlValidator.Validate(_schema, rawYaml); + var rawYaml = File.ReadAllText($@"{_invalidPath}{filename}"); + var result = _yamlValidator.Validate(rawYaml); Assert.IsFalse(result.SchemaValid); } } diff --git a/src/YamlValidator.Tests/YamlValidator.Tests.csproj b/src/YamlValidator.Tests/YamlValidator.Tests.csproj new file mode 100644 index 00000000..5cab08d7 --- /dev/null +++ b/src/YamlValidator.Tests/YamlValidator.Tests.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + Latest + enable + enable + + false + true + + true + ../../35MSSharedLib1024.snk + true + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + diff --git a/src/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/ControlWithInvalidProperty.yaml b/src/YamlValidator.Tests/_TestData/InvalidYaml/ControlWithInvalidProperty.yaml similarity index 100% rename from src/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/ControlWithInvalidProperty.yaml rename to src/YamlValidator.Tests/_TestData/InvalidYaml/ControlWithInvalidProperty.yaml diff --git a/src/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/Empty.yaml b/src/YamlValidator.Tests/_TestData/InvalidYaml/Empty.yaml similarity index 100% rename from src/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/Empty.yaml rename to src/YamlValidator.Tests/_TestData/InvalidYaml/Empty.yaml diff --git a/src/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/EmptyArray.yaml b/src/YamlValidator.Tests/_TestData/InvalidYaml/EmptyArray.yaml similarity index 100% rename from src/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/EmptyArray.yaml rename to src/YamlValidator.Tests/_TestData/InvalidYaml/EmptyArray.yaml diff --git a/src/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/NamelessObjectNoControl.yaml b/src/YamlValidator.Tests/_TestData/InvalidYaml/NamelessObjectNoControl.yaml similarity index 100% rename from src/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/NamelessObjectNoControl.yaml rename to src/YamlValidator.Tests/_TestData/InvalidYaml/NamelessObjectNoControl.yaml diff --git a/src/YamlValidator.Tests/_TestData/InvalidYaml/NotYaml.yaml b/src/YamlValidator.Tests/_TestData/InvalidYaml/NotYaml.yaml new file mode 100644 index 00000000..d4dc3834 --- /dev/null +++ b/src/YamlValidator.Tests/_TestData/InvalidYaml/NotYaml.yaml @@ -0,0 +1,15 @@ +{ + features => [ + { + name => lorem ipsum, + points => [ + "bullet 1", + "bullet 2" + ] + }, + { + name => lorem ipsum 2, + description => lorem ipsum 3 + } + ] +} diff --git a/src/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/ScreenWithNameNoColon.yaml b/src/YamlValidator.Tests/_TestData/InvalidYaml/ScreenWithNameNoColon.yaml similarity index 100% rename from src/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/ScreenWithNameNoColon.yaml rename to src/YamlValidator.Tests/_TestData/InvalidYaml/ScreenWithNameNoColon.yaml diff --git a/src/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/ScreenWithNameNoValue.yaml b/src/YamlValidator.Tests/_TestData/InvalidYaml/ScreenWithNameNoValue.yaml similarity index 100% rename from src/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/ScreenWithNameNoValue.yaml rename to src/YamlValidator.Tests/_TestData/InvalidYaml/ScreenWithNameNoValue.yaml diff --git a/src/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/ScreenWithoutControlProperty.yaml b/src/YamlValidator.Tests/_TestData/InvalidYaml/ScreenWithoutControlProperty.yaml similarity index 100% rename from src/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/ScreenWithoutControlProperty.yaml rename to src/YamlValidator.Tests/_TestData/InvalidYaml/ScreenWithoutControlProperty.yaml diff --git a/src/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/WrongControlDefinition.yaml b/src/YamlValidator.Tests/_TestData/InvalidYaml/WrongControlDefinition.yaml similarity index 100% rename from src/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/WrongControlDefinition.yaml rename to src/YamlValidator.Tests/_TestData/InvalidYaml/WrongControlDefinition.yaml diff --git a/src/Persistence.Tests/_TestData/ValidatorTests/ValidYaml/NamelessObjectWithControl.yaml b/src/YamlValidator.Tests/_TestData/ValidYaml/NamelessObjectWithControl.yaml similarity index 100% rename from src/Persistence.Tests/_TestData/ValidatorTests/ValidYaml/NamelessObjectWithControl.yaml rename to src/YamlValidator.Tests/_TestData/ValidYaml/NamelessObjectWithControl.yaml diff --git a/src/Persistence.Tests/_TestData/ValidatorTests/ValidYaml/SimpleNoRecursiveDefinition.yaml b/src/YamlValidator.Tests/_TestData/ValidYaml/SimpleNoRecursiveDefinition.yaml similarity index 100% rename from src/Persistence.Tests/_TestData/ValidatorTests/ValidYaml/SimpleNoRecursiveDefinition.yaml rename to src/YamlValidator.Tests/_TestData/ValidYaml/SimpleNoRecursiveDefinition.yaml diff --git a/src/Persistence.Tests/_TestData/ValidatorTests/ValidYaml/ValidScreen1.yaml b/src/YamlValidator.Tests/_TestData/ValidYaml/ValidScreen1.yaml similarity index 100% rename from src/Persistence.Tests/_TestData/ValidatorTests/ValidYaml/ValidScreen1.yaml rename to src/YamlValidator.Tests/_TestData/ValidYaml/ValidScreen1.yaml diff --git a/src/YamlValidator/Constants.cs b/src/YamlValidator/Constants.cs new file mode 100644 index 00000000..e571e29d --- /dev/null +++ b/src/YamlValidator/Constants.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; + +public static class Constants +{ + public const string YamlFileExtension = ".pa.yaml"; + public const string JsonFileExtension = ".json"; + + public const string notYamlError = "File is not YAML"; + public const string emptyYamlError = "Empty YAML file"; + + public const string subNamespace = "YamlValidator"; +} diff --git a/src/YamlValidator/Extensions/ServiceCollectionExtensions.cs b/src/YamlValidator/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..a27a804c --- /dev/null +++ b/src/YamlValidator/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; + +public static class ServiceCollectionExtensions +{ + /// + /// registers the MSAPP persistence services + /// + /// the services collection instance. + public static void AddPowerAppsPersistenceYamlValidator(this IServiceCollection services) + { + services.AddSingleton(); + } +} + + diff --git a/src/Persistence/YamlValidator/ValidationRequest.cs b/src/YamlValidator/IValidator.cs similarity index 62% rename from src/Persistence/YamlValidator/ValidationRequest.cs rename to src/YamlValidator/IValidator.cs index 568b16d8..10008c71 100644 --- a/src/Persistence/YamlValidator/ValidationRequest.cs +++ b/src/YamlValidator/IValidator.cs @@ -3,4 +3,7 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; -public readonly record struct ValidationRequest(string FilePath, string FilePathType); +public interface IValidator +{ + ValidatorResults Validate(string yamlString); +} diff --git a/src/YamlValidator/IValidatorFactory.cs b/src/YamlValidator/IValidatorFactory.cs new file mode 100644 index 00000000..ee8721f0 --- /dev/null +++ b/src/YamlValidator/IValidatorFactory.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; + +public interface IValidatorFactory +{ + IValidator CreateValidator(); +} diff --git a/src/YamlValidator/InternalsVisibleTo.cs b/src/YamlValidator/InternalsVisibleTo.cs new file mode 100644 index 00000000..d7c66fc7 --- /dev/null +++ b/src/YamlValidator/InternalsVisibleTo.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("YamlValidator.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] diff --git a/src/YamlValidator/Microsoft.PowerPlatform.PowerApps.YamlValidator.csproj b/src/YamlValidator/Microsoft.PowerPlatform.PowerApps.YamlValidator.csproj new file mode 100644 index 00000000..23e6f915 --- /dev/null +++ b/src/YamlValidator/Microsoft.PowerPlatform.PowerApps.YamlValidator.csproj @@ -0,0 +1,38 @@ + + + + netstandard2.0;net8.0 + Latest + enable + enable + true + ../../35MSSharedLib1024.snk + true + + + + + + + $(OutDir)\$(TargetFramework) + + + + + + + + + + + + + + + + + schema\%(RecursiveDir)%(Filename)%(Extension) + + + + \ No newline at end of file diff --git a/src/YamlValidator/SchemaLoader.cs b/src/YamlValidator/SchemaLoader.cs new file mode 100644 index 00000000..540a4685 --- /dev/null +++ b/src/YamlValidator/SchemaLoader.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Json.Schema; + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; + +internal class SchemaLoader +{ + private const string _schemaFolderPath = "schema"; + private const string _subschemaFolderPath = "subschemas"; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Suppress to make classes stateless")] + public JsonSchema Load() + { + var assembly = typeof(SchemaLoader).Assembly; + + JsonSchema? node = null; + foreach (var file in assembly.GetManifestResourceNames()) + { + var fileStream = assembly.GetManifestResourceStream(file); + var assemblyName = assembly.GetName().Name; + if (fileStream == null) + { + throw new IOException($"Resource {file} could not found in assembly {assemblyName}"); + } + using var streamReader = new StreamReader(fileStream); + var jsonSchemaString = streamReader.ReadToEnd(); + var schema = JsonSchema.FromText(jsonSchemaString); + + // assembly name is Microsoft.PowerPlatform.PowerApps.Persistence + // subNamespace is YamlValidator, schemas live in the linked schema folder + var rootFileName = $"{assemblyName}.{_schemaFolderPath}"; + + if (file.StartsWith($"{rootFileName}.{_subschemaFolderPath}.", StringComparison.Ordinal)) + { + // these virtual uri's are used to resolve $ref's in the schema, they aren't + // represented like this in the dll + schema.BaseUri = new Uri($"file://{_schemaFolderPath}/{_subschemaFolderPath}/"); + SchemaRegistry.Global.Register(schema); + continue; + } + schema.BaseUri = new Uri($"file://{_schemaFolderPath}"); + node = schema; + } + if (node == null) + { + throw new InvalidDataException("Schema was not able to be read into memory"); + } + return node; + } +} + diff --git a/src/Persistence/YamlValidator/Utility.cs b/src/YamlValidator/Utility.cs similarity index 73% rename from src/Persistence/YamlValidator/Utility.cs rename to src/YamlValidator/Utility.cs index 7ccc089b..630548c1 100644 --- a/src/Persistence/YamlValidator/Utility.cs +++ b/src/YamlValidator/Utility.cs @@ -7,12 +7,6 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; public class Utility { - public static string ReadFileData(string filePath) - { - var yamlData = File.ReadAllText(filePath); - return yamlData; - } - public static YamlStream MakeYamlStream(string yamlString) { var stream = new YamlStream(); diff --git a/src/Persistence/YamlValidator/Validator.cs b/src/YamlValidator/Validator.cs similarity index 55% rename from src/Persistence/YamlValidator/Validator.cs rename to src/YamlValidator/Validator.cs index 06d0c4c7..e3154d2e 100644 --- a/src/Persistence/YamlValidator/Validator.cs +++ b/src/YamlValidator/Validator.cs @@ -3,41 +3,43 @@ using Json.Schema; using Yaml2JsonNode; -using System.Text.Json; +using YamlDotNet.Core; +using YamlDotNet.RepresentationModel; namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; -public class Validator +internal class Validator : IValidator { private readonly EvaluationOptions _verbosityOptions; - private readonly JsonSerializerOptions _serializerOptions; - - public Validator(EvaluationOptions options, JsonSerializerOptions resultSerializeOptions) + private readonly JsonSchema _schema; + public Validator(EvaluationOptions options, JsonSchema schema) { // to do: add verbosity flag and allow users to choose verbosity of evaluation _verbosityOptions = options; - _serializerOptions = resultSerializeOptions; + _schema = schema; } - - public ValidatorResults Validate(JsonSchema schema, string yamlFileData) + public ValidatorResults Validate(string yamlFileData) { - var yamlStream = Utility.MakeYamlStream(yamlFileData); + YamlStream yamlStream; + try + { + yamlStream = Utility.MakeYamlStream(yamlFileData); + } + catch (YamlException) + { + return new ValidatorResults(false, new List { new(Constants.notYamlError) }); + } + var jsonData = yamlStream.Documents.Count > 0 ? yamlStream.Documents[0].ToJsonNode() : null; // here we say that empty yaml is serialized as null json if (jsonData == null) { - return new ValidatorResults(false, new List { new("Empty YAML file") }); + return new ValidatorResults(false, new List { new(Constants.emptyYamlError) }); } - var results = schema.Evaluate(jsonData, _verbosityOptions); - - // not used but may help if we ever need to serialize the evaluation results into json format to feed into - // a VSCode extension or other tool - var output = JsonSerializer.Serialize(results, _serializerOptions); + var results = _schema.Evaluate(jsonData, _verbosityOptions); var schemaValidity = results.IsValid; - // TBD: filter actual errors versus false positives - // we look for errors that are not valid, have errors, and have an instance location (i.e are not oneOf errors) var yamlValidatorErrors = new List(); if (!schemaValidity) { @@ -46,7 +48,10 @@ public ValidatorResults Validate(JsonSchema schema, string yamlFileData) node.HasErrors).ToList(); foreach (var trace in traceList) { - yamlValidatorErrors.Add(new ValidatorError(trace)); + var instanceLocation = trace.InstanceLocation.ToString(); + var schemaPath = trace.EvaluationPath.ToString(); + var errors = trace.Errors; + yamlValidatorErrors.Add(new ValidatorError(instanceLocation, schemaPath, errors)); } } diff --git a/src/Persistence/YamlValidator/ValidatorError.cs b/src/YamlValidator/ValidatorError.cs similarity index 81% rename from src/Persistence/YamlValidator/ValidatorError.cs rename to src/YamlValidator/ValidatorError.cs index 0ced0cfb..32f9b5ef 100644 --- a/src/Persistence/YamlValidator/ValidatorError.cs +++ b/src/YamlValidator/ValidatorError.cs @@ -1,20 +1,19 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Json.Schema; - namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; + public class ValidatorError { public string InstanceLocation { get; } public string SchemaPath { get; } public IReadOnlyDictionary? Errors { get; } - public ValidatorError(EvaluationResults results) + public ValidatorError(string instancePath, string schemaPath, IReadOnlyDictionary? errors) { - InstanceLocation = results.InstanceLocation.ToString(); - SchemaPath = results.EvaluationPath.ToString(); - Errors = results.Errors; + InstanceLocation = instancePath; + SchemaPath = schemaPath; + Errors = errors; } public ValidatorError(string error) diff --git a/src/YamlValidator/ValidatorFactory.cs b/src/YamlValidator/ValidatorFactory.cs new file mode 100644 index 00000000..d90e908e --- /dev/null +++ b/src/YamlValidator/ValidatorFactory.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Json.Schema; + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; + +internal class ValidatorFactory : IValidatorFactory +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "required by IValidatorFactory interface")] + public IValidator CreateValidator() + { + // register schema in from memory into global schema registry + var schemaLoader = new SchemaLoader(); + var serializedSchema = schemaLoader.Load(); + + var evalOptions = new EvaluationOptions + { + OutputFormat = OutputFormat.List + }; + + return new Validator(evalOptions, serializedSchema); + } +} diff --git a/src/Persistence/YamlValidator/ValidatorResults.cs b/src/YamlValidator/ValidatorResults.cs similarity index 94% rename from src/Persistence/YamlValidator/ValidatorResults.cs rename to src/YamlValidator/ValidatorResults.cs index e7565148..f3b9e221 100644 --- a/src/Persistence/YamlValidator/ValidatorResults.cs +++ b/src/YamlValidator/ValidatorResults.cs @@ -15,6 +15,7 @@ public ValidatorResults(bool schemaValid, IReadOnlyList traversa TraversalResults = FilterErrors(traversalResults); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Suppress to make classes stateless")] // This will filter out the false positives that are not relevant to the error output, when the validation is false private ReadOnlyCollection FilterErrors(IReadOnlyList traversalResults) {