diff --git a/src/Persistence.Tests/YamlValidator/ValidatorFactoryTest.cs b/src/Persistence.Tests/YamlValidator/ValidatorFactoryTest.cs new file mode 100644 index 00000000..9aeaa354 --- /dev/null +++ b/src/Persistence.Tests/YamlValidator/ValidatorFactoryTest.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; + +namespace Persistence.Tests.YamlValidator; + +[TestClass] +public class ValidatorFactoryTest +{ + [TestMethod] + public void GetValidatorTest() + { + var factory = new ValidatorFactory(); + var validator = factory.GetValidator(); + + Assert.IsNotNull(validator); + Assert.IsInstanceOfType(validator, typeof(Validator)); + } +} diff --git a/src/Persistence.Tests/YamlValidator/ValidatorTest.cs b/src/Persistence.Tests/YamlValidator/ValidatorTest.cs index 20030b95..5ab11d68 100644 --- a/src/Persistence.Tests/YamlValidator/ValidatorTest.cs +++ b/src/Persistence.Tests/YamlValidator/ValidatorTest.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Json.Schema; using Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; namespace Persistence.Tests.YamlValidator; @@ -16,15 +15,12 @@ public class ValidatorTest private static readonly string _invalidPath = Path.Combine(".", "_TestData", "ValidatorTests", "InvalidYaml") + Path.DirectorySeparatorChar; - private readonly JsonSchema _schema; private readonly Validator _yamlValidator; public ValidatorTest() { - var schemaFileLoader = new SchemaLoader(); - _schema = schemaFileLoader.Load(); - var resultVerbosity = new VerbosityData(Constants.Verbose); - _yamlValidator = new Validator(resultVerbosity.EvalOptions, resultVerbosity.JsonOutputOptions); + var validatorFactory = new ValidatorFactory(); + _yamlValidator = validatorFactory.GetValidator(); } [TestMethod] @@ -34,8 +30,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 +44,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/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/NotYaml.yaml b/src/Persistence.Tests/_TestData/ValidatorTests/InvalidYaml/NotYaml.yaml new file mode 100644 index 00000000..d4dc3834 --- /dev/null +++ b/src/Persistence.Tests/_TestData/ValidatorTests/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/YamlValidator/Constants.cs b/src/Persistence/YamlValidator/Constants.cs index c750ab6a..93ca9d58 100644 --- a/src/Persistence/YamlValidator/Constants.cs +++ b/src/Persistence/YamlValidator/Constants.cs @@ -5,14 +5,9 @@ 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"); + public const string notYamlError = "File is not YAML"; + public const string emptyYamlError = "Empty YAML file"; } diff --git a/src/Persistence/YamlValidator/SchemaLoader.cs b/src/Persistence/YamlValidator/SchemaLoader.cs index 48e65628..d5b3f881 100644 --- a/src/Persistence/YamlValidator/SchemaLoader.cs +++ b/src/Persistence/YamlValidator/SchemaLoader.cs @@ -7,14 +7,14 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; public class SchemaLoader { - private const string _schemaFolderPath = "subschemas"; + private const string _subschemaFolderPath = "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}", + var subschemaPaths = Directory.GetFiles($@"{schemaFolder}{Path.DirectorySeparatorChar}{_subschemaFolderPath}", $"*{Constants.JsonFileExtension}"); foreach (var path in subschemaPaths) diff --git a/src/Persistence/YamlValidator/Utility.cs b/src/Persistence/YamlValidator/Utility.cs index 7ccc089b..630548c1 100644 --- a/src/Persistence/YamlValidator/Utility.cs +++ b/src/Persistence/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/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/ValidationRequest.cs b/src/Persistence/YamlValidator/ValidationRequest.cs deleted file mode 100644 index 568b16d8..00000000 --- a/src/Persistence/YamlValidator/ValidationRequest.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; - -public readonly record struct ValidationRequest(string FilePath, string FilePathType); diff --git a/src/Persistence/YamlValidator/Validator.cs b/src/Persistence/YamlValidator/Validator.cs index 06d0c4c7..5d85537a 100644 --- a/src/Persistence/YamlValidator/Validator.cs +++ b/src/Persistence/YamlValidator/Validator.cs @@ -4,40 +4,45 @@ using Json.Schema; using Yaml2JsonNode; using System.Text.Json; +using YamlDotNet.Core; +using YamlDotNet.RepresentationModel; namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; - public class Validator { private readonly EvaluationOptions _verbosityOptions; private readonly JsonSerializerOptions _serializerOptions; - - public Validator(EvaluationOptions options, JsonSerializerOptions resultSerializeOptions) + private readonly JsonSchema _schema; + public Validator(EvaluationOptions options, JsonSerializerOptions resultSerializeOptions, 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) { diff --git a/src/Persistence/YamlValidator/ValidatorFactory.cs b/src/Persistence/YamlValidator/ValidatorFactory.cs new file mode 100644 index 00000000..f1b4448d --- /dev/null +++ b/src/Persistence/YamlValidator/ValidatorFactory.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Json.Schema; +using System.Text.Json; + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; + +public class ValidatorFactory +{ + public Validator GetValidator() + { + // register schema in from memory into global schema registry + var schemaLoader = new SchemaLoader(); + var serializedSchema = schemaLoader.Load(); + + var evalOptions = new EvaluationOptions + { + OutputFormat = OutputFormat.List + }; + + // pass in serailization options for validator results object to json + // This is unused for now but can be useful for producing raw json validation results which can be consumed elsewhere + var resultSerializeOptions = new JsonSerializerOptions + { + Converters = { new EvaluationResultsJsonConverter() } + }; + + return new Validator(evalOptions, resultSerializeOptions, serializedSchema); + } +} 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); - } -}