diff --git a/.version/PipelineAssemblyInfo.cs b/.version/PipelineAssemblyInfo.cs index b1f2f756..f5cc4c28 100644 --- a/.version/PipelineAssemblyInfo.cs +++ b/.version/PipelineAssemblyInfo.cs @@ -5,4 +5,4 @@ using System.Reflection; [assembly: AssemblyVersion("0.0.0.0")] [assembly: AssemblyFileVersion("0.0.0.0")] -[assembly: AssemblyInformationalVersion("0.0.0.0-dev-00000000")] +[assembly: AssemblyInformationalVersion("0.0.0.0-dev-00000000")] \ No newline at end of file diff --git a/docs/pa.yaml-schema.json b/docs/pa.yaml-schema.json index cb97c8de..23bb5437 100644 --- a/docs/pa.yaml-schema.json +++ b/docs/pa.yaml-schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/microsoft/PowerApps-Tooling/master/docs/pa.yaml-schema.json", + "$id": "pa.yaml-schema.json", "title": "Microsoft Power Apps", "description": "Canvas YAML", "oneOf": [ diff --git a/docs/subschemas/control-property-schema.json b/docs/subschemas/control-property-schema.json index 6d7b3f6b..748af747 100644 --- a/docs/subschemas/control-property-schema.json +++ b/docs/subschemas/control-property-schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/microsoft/PowerApps-Tooling/master/docs/control-type-schema.json", + "$id": "control-property-schema.json", "title": "Microsoft Power Apps Properties", "description": "The properties of the control", "type": "object", diff --git a/docs/subschemas/control-type-schema.json b/docs/subschemas/control-type-schema.json index 081e9761..2aac5d7d 100644 --- a/docs/subschemas/control-type-schema.json +++ b/docs/subschemas/control-type-schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/microsoft/PowerApps-Tooling/master/docs/control-type-schema.json", + "$id": "control-type-schema.json", "title": "Microsoft Power Apps Control Type", "description": "The type of the control", "type": "string", diff --git a/src/PASopa.sln b/src/PASopa.sln index a7347547..f16b903d 100644 --- a/src/PASopa.sln +++ b/src/PASopa.sln @@ -20,6 +20,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerPlatform.Pow EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.Tests", "Persistence.Tests\Persistence.Tests.csproj", "{8AB1C901-FE5E-44BF-AA21-B8F20A9D7CDD}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YamlValidator", "YamlValidator\YamlValidator.csproj", "{F0AD11CE-E634-4945-A6B1-7866CDE0059C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YamlValidator.Tests", "YamlValidator.Tests\YamlValidator.Tests.csproj", "{8BA5DD4B-9423-4827-AF37-540E0300DB9A}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7361DB16-D534-4E0E-8597-BE22317DEF47}" ProjectSection(SolutionItems) = preProject Directory.Build.props = Directory.Build.props @@ -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 + {F0AD11CE-E634-4945-A6B1-7866CDE0059C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0AD11CE-E634-4945-A6B1-7866CDE0059C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0AD11CE-E634-4945-A6B1-7866CDE0059C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0AD11CE-E634-4945-A6B1-7866CDE0059C}.Release|Any CPU.Build.0 = Release|Any CPU + {8BA5DD4B-9423-4827-AF37-540E0300DB9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8BA5DD4B-9423-4827-AF37-540E0300DB9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8BA5DD4B-9423-4827-AF37-540E0300DB9A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8BA5DD4B-9423-4827-AF37-540E0300DB9A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -59,6 +71,7 @@ Global GlobalSection(NestedProjects) = preSolution {8AD94CC0-7330-4880-A8E0-177B37CDB27B} = {4993E606-484B-46D9-892E-7AE9CE8D4423} {8AB1C901-FE5E-44BF-AA21-B8F20A9D7CDD} = {4993E606-484B-46D9-892E-7AE9CE8D4423} + {8BA5DD4B-9423-4827-AF37-540E0300DB9A} = {4993E606-484B-46D9-892E-7AE9CE8D4423} {7361DB16-D534-4E0E-8597-BE22317DEF47} = {794D8C68-BF6F-49C8-BCA5-AA52D8F45EF4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/YamlValidator.Tests/ValidatorTest.cs b/src/YamlValidator.Tests/ValidatorTest.cs new file mode 100644 index 00000000..3408da83 --- /dev/null +++ b/src/YamlValidator.Tests/ValidatorTest.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Json.Schema; +using Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; + +namespace Persistence.Tests.YamlValidator; + +[TestClass] +public class ValidatorTest +{ + private const string _validPath = @".\_TestData\ValidYaml"; + private const string _invalidPath = @".\_TestData\InvalidYaml"; + private const string _schemaPath = @"..\YamlValidator\schema\pa.yaml-schema.json"; + + private readonly JsonSchema _schema; + private readonly Validator _yamlValidator; + + public ValidatorTest() + { + var schemaFileLoader = new SchemaLoader(); + _schema = schemaFileLoader.Load(_schemaPath); + var resultVerbosity = new VerbosityData(Constants.Verbose); + _yamlValidator = new Validator(resultVerbosity.EvalOptions, resultVerbosity.JsonOutputOptions); + } + + [TestMethod] + [DataRow($@"{_invalidPath}\ScreenWithNameNoColon.yaml", false)] + [DataRow($@"{_invalidPath}\ScreenWithNameNoValue.yaml", false)] + [DataRow($@"{_invalidPath}\ScreenWithoutControlProperty.yaml", false)] + [DataRow($@"{_invalidPath}\WrongControlDefinition.yaml", false)] + [DataRow($@"{_invalidPath}\ControlWithInvalidProperty.yaml", false)] + [DataRow($@"{_invalidPath}\EmptyArray.yaml", false)] + [DataRow($@"{_invalidPath}\Empty.yaml", false)] + [DataRow($@"{_invalidPath}\NamelessObjectNoControl.yaml", false)] + [DataRow($@"{_validPath}\NamelessObjectWithControl.yaml", true)] + [DataRow($@"{_validPath}\ValidScreen1.yaml", true)] + [DataRow($@"{_validPath}\SimpleNoRecursiveDefinition.yaml", true)] + + public void TestValidation(string filepath, bool expectedResult) + { + var rawYaml = Utility.ReadFileData($@"{filepath}"); + var result = _yamlValidator.Validate(_schema, rawYaml); + Assert.IsTrue(result.SchemaValid == expectedResult); + } +} diff --git a/src/YamlValidator.Tests/YamlValidator.Tests.csproj b/src/YamlValidator.Tests/YamlValidator.Tests.csproj new file mode 100644 index 00000000..d77e2bc4 --- /dev/null +++ b/src/YamlValidator.Tests/YamlValidator.Tests.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + false + true + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + diff --git a/src/YamlValidator.Tests/_TestData/InvalidYaml/ControlWithInvalidProperty.yaml b/src/YamlValidator.Tests/_TestData/InvalidYaml/ControlWithInvalidProperty.yaml new file mode 100644 index 00000000..999ea429 --- /dev/null +++ b/src/YamlValidator.Tests/_TestData/InvalidYaml/ControlWithInvalidProperty.yaml @@ -0,0 +1,3 @@ +Screen2: + Control: Screen + InvalidProperty: "invalid" diff --git a/src/YamlValidator.Tests/_TestData/InvalidYaml/Empty.yaml b/src/YamlValidator.Tests/_TestData/InvalidYaml/Empty.yaml new file mode 100644 index 00000000..e69de29b diff --git a/src/YamlValidator.Tests/_TestData/InvalidYaml/EmptyArray.yaml b/src/YamlValidator.Tests/_TestData/InvalidYaml/EmptyArray.yaml new file mode 100644 index 00000000..c59ec775 --- /dev/null +++ b/src/YamlValidator.Tests/_TestData/InvalidYaml/EmptyArray.yaml @@ -0,0 +1,4 @@ +- +- +- +- diff --git a/src/YamlValidator.Tests/_TestData/InvalidYaml/NamelessObjectNoControl.yaml b/src/YamlValidator.Tests/_TestData/InvalidYaml/NamelessObjectNoControl.yaml new file mode 100644 index 00000000..397db75f --- /dev/null +++ b/src/YamlValidator.Tests/_TestData/InvalidYaml/NamelessObjectNoControl.yaml @@ -0,0 +1 @@ +: diff --git a/src/YamlValidator.Tests/_TestData/InvalidYaml/ScreenWithNameNoColon.yaml b/src/YamlValidator.Tests/_TestData/InvalidYaml/ScreenWithNameNoColon.yaml new file mode 100644 index 00000000..9daeafb9 --- /dev/null +++ b/src/YamlValidator.Tests/_TestData/InvalidYaml/ScreenWithNameNoColon.yaml @@ -0,0 +1 @@ +test diff --git a/src/YamlValidator.Tests/_TestData/InvalidYaml/ScreenWithNameNoValue.yaml b/src/YamlValidator.Tests/_TestData/InvalidYaml/ScreenWithNameNoValue.yaml new file mode 100644 index 00000000..e901b4d3 --- /dev/null +++ b/src/YamlValidator.Tests/_TestData/InvalidYaml/ScreenWithNameNoValue.yaml @@ -0,0 +1 @@ +test: diff --git a/src/YamlValidator.Tests/_TestData/InvalidYaml/ScreenWithoutControlProperty.yaml b/src/YamlValidator.Tests/_TestData/InvalidYaml/ScreenWithoutControlProperty.yaml new file mode 100644 index 00000000..eef6736d --- /dev/null +++ b/src/YamlValidator.Tests/_TestData/InvalidYaml/ScreenWithoutControlProperty.yaml @@ -0,0 +1,2 @@ +Screen: + ComponentName: "test" diff --git a/src/YamlValidator.Tests/_TestData/InvalidYaml/WrongControlDefinition.yaml b/src/YamlValidator.Tests/_TestData/InvalidYaml/WrongControlDefinition.yaml new file mode 100644 index 00000000..b0bf6878 --- /dev/null +++ b/src/YamlValidator.Tests/_TestData/InvalidYaml/WrongControlDefinition.yaml @@ -0,0 +1,2 @@ +TestScreen: + "abcd" diff --git a/src/YamlValidator.Tests/_TestData/ValidYaml/NamelessObjectWithControl.yaml b/src/YamlValidator.Tests/_TestData/ValidYaml/NamelessObjectWithControl.yaml new file mode 100644 index 00000000..b714ba34 --- /dev/null +++ b/src/YamlValidator.Tests/_TestData/ValidYaml/NamelessObjectWithControl.yaml @@ -0,0 +1,2 @@ +: + Control: "bdbd" diff --git a/src/YamlValidator.Tests/_TestData/ValidYaml/SimpleNoRecursiveDefinition.yaml b/src/YamlValidator.Tests/_TestData/ValidYaml/SimpleNoRecursiveDefinition.yaml new file mode 100644 index 00000000..616601da --- /dev/null +++ b/src/YamlValidator.Tests/_TestData/ValidYaml/SimpleNoRecursiveDefinition.yaml @@ -0,0 +1,2 @@ +LoginPage: + Control: "Button" diff --git a/src/YamlValidator.Tests/_TestData/ValidYaml/ValidScreen1.yaml b/src/YamlValidator.Tests/_TestData/ValidYaml/ValidScreen1.yaml new file mode 100644 index 00000000..0ed69b8a --- /dev/null +++ b/src/YamlValidator.Tests/_TestData/ValidYaml/ValidScreen1.yaml @@ -0,0 +1,30 @@ +Screen2: + Control: Screen + Children: + - ButtonCanvas2: + Control: Button + Properties: + OnSelect: =Navigate(Screen1) + Text: ="Back" + Height: =53 + Width: =172 + X: =632 + Y: =550 + - TextCanvas1: + Control: Text + Properties: + Align: ='TextCanvas.Align'.Center + Size: =50 + Text: ="Hello" + Height: =91 + Width: =368 + X: =517 + Y: =44 + - Image1: + Control: Image + Properties: + Image: ='pexels-pixabay-417173' + Height: =361 + Width: =466 + X: =447 + Y: =135 diff --git a/src/YamlValidator/Constants.cs b/src/YamlValidator/Constants.cs new file mode 100644 index 00000000..e377efd7 --- /dev/null +++ b/src/YamlValidator/Constants.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; +public class Constants +{ + public const string FileTypeName = "file"; + public const string FolderTypeName = "folder"; + public const string YamlFileExtension = ".yaml"; + public const string YmlFileExtension = ".yml"; + public const string JsonFileExtension = ".json"; + + public const string Verbose = "verbose"; +} diff --git a/src/YamlValidator/InputProcessor.cs b/src/YamlValidator/InputProcessor.cs new file mode 100644 index 00000000..c3e8337e --- /dev/null +++ b/src/YamlValidator/InputProcessor.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.CommandLine; + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; +public class InputProcessor +{ + + private static void ProcessFiles(string path, string schema, string pathType) + { + // read only records + var filePathInfo = new ValidationRequest(path, schema, pathType); + var verbosityInfo = new VerbosityData(Constants.Verbose); + + var validator = new Validator(verbosityInfo.EvalOptions, verbosityInfo.JsonOutputOptions); + var schemaLoader = new SchemaLoader(); + var fileLoader = new YamlLoader(); + var orchestrator = new Orchestrator(fileLoader, schemaLoader, validator); + orchestrator.RunValidation(filePathInfo); + } + public static RootCommand GetRootCommand() + { + + var pathOption = new Option( + name: "--path", + description: "The path to the input yaml file or directory of yaml files" + ) + { IsRequired = true }; + + pathOption.AddValidator(result => + { + var inputFilePath = result.GetValueForOption(pathOption); + + // either file or folder must be passed + var pathType = string.Empty; + if (string.IsNullOrEmpty(inputFilePath)) + { + result.ErrorMessage = "The input is invalid, input must be a filepath to a yaml file \\" + + "or a folder path to a folder of yaml files"; + } + else if (!Directory.Exists(inputFilePath) && !File.Exists(inputFilePath)) + { + result.ErrorMessage = "The input path does not exist"; + } + else if (Directory.Exists(inputFilePath)) + { + if (Directory.GetFiles(inputFilePath, $"*{Constants.YamlFileExtension}").Length == 0) + { + result.ErrorMessage = "The input folder does not contain any yaml files"; + } + } + else if (File.Exists(inputFilePath)) + { + if (Path.GetExtension(inputFilePath) != Constants.YamlFileExtension) + { + result.ErrorMessage = "The input file must be a yaml file"; + } + } + }); + + // assume local schema file exists in nuget package, use relative filepath for now + var schemaOption = new Option( + name: "--schema", + description: "The path to the schema json file", + getDefaultValue: () => @".\schema\pa.yaml-schema.json" + ); + + schemaOption.AddValidator(result => + { + var schemaPath = result.GetValueForOption(schemaOption); + if (string.IsNullOrEmpty(schemaPath)) + { + result.ErrorMessage = "Schema option selected, but no schema was provided"; + } + else if (Path.GetExtension(schemaPath) != Constants.JsonFileExtension) + { + result.ErrorMessage = "The schema file must be a json file"; + } + else if (!File.Exists(schemaPath)) + { + result.ErrorMessage = "The schema file does not exist"; + } + }); + + // define root + var rootCommand = new RootCommand("YAML validator cli-tool"); + + // validate command + var validateCommand = new Command("validate", "Validate the input yaml file") + { + pathOption, + schemaOption + }; + + validateCommand.SetHandler((pathOptionVal, schemaOptionVal) => + { + var pathType = File.GetAttributes(pathOptionVal).HasFlag(FileAttributes.Directory) ? Constants.FolderTypeName : + Constants.FileTypeName; + ProcessFiles(pathOptionVal, schemaOptionVal, pathType); + + }, pathOption, schemaOption); + + rootCommand.AddCommand(validateCommand); + + return rootCommand; + + } +} diff --git a/src/YamlValidator/Orchestrator.cs b/src/YamlValidator/Orchestrator.cs new file mode 100644 index 00000000..71d04971 --- /dev/null +++ b/src/YamlValidator/Orchestrator.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; +public class Orchestrator +{ + private readonly YamlLoader _fileLoader; + private readonly SchemaLoader _schemaLoader; + private readonly Validator _validator; + + public Orchestrator(YamlLoader fileLoader, SchemaLoader schemaLoader, Validator validator) + { + _fileLoader = fileLoader; + _schemaLoader = schemaLoader; + _validator = validator; + } + + public void RunValidation(ValidationRequest inputData) + { + var schemaPath = inputData.SchemaPath; + var path = inputData.FilePath; + var pathType = inputData.FilePathType; + + var yamlData = _fileLoader.Load(path, pathType); + var serializedSchema = _schemaLoader.Load(schemaPath); + + foreach (var yamlFileData in yamlData) + { + Console.WriteLine($"Validation for {yamlFileData.Key}"); + var result = _validator.Validate(serializedSchema, yamlFileData.Value); + Console.WriteLine($"Validation Result: {result.SchemaValid}"); + foreach (var error in result.TraversalResults) + { + Console.WriteLine($"{error}"); + } + Console.WriteLine(); + } + } + + + +} diff --git a/src/YamlValidator/Program.cs b/src/YamlValidator/Program.cs new file mode 100644 index 00000000..c35c35d9 --- /dev/null +++ b/src/YamlValidator/Program.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.CommandLine; + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; + +public class Program +{ + private static void Main(string[] args) + { + var inputProcessor = InputProcessor.GetRootCommand(); + inputProcessor.Invoke(args); + } +} diff --git a/src/YamlValidator/SchemaLoader.cs b/src/YamlValidator/SchemaLoader.cs new file mode 100644 index 00000000..edce53c7 --- /dev/null +++ b/src/YamlValidator/SchemaLoader.cs @@ -0,0 +1,28 @@ +// 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"; + + public JsonSchema Load(string schemaPath) + { + var node = JsonSchema.FromFile(schemaPath); + var schemaFolder = Path.GetDirectoryName(schemaPath); + var subschemaPaths = Directory.GetFiles($@"{schemaFolder}\{_schemaFolderPath}", + $"*{Constants.JsonFileExtension}"); + + foreach (var path in subschemaPaths) + { + var subschema = JsonSchema.FromFile(path); + SchemaRegistry.Global.Register(subschema); + } + + return node; + } + +} + diff --git a/src/YamlValidator/Utility.cs b/src/YamlValidator/Utility.cs new file mode 100644 index 00000000..c3ab709a --- /dev/null +++ b/src/YamlValidator/Utility.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using YamlDotNet.RepresentationModel; + +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(); + stream.Load(new StringReader(yamlString)); + return stream; + } +} diff --git a/src/YamlValidator/ValidationRequest.cs b/src/YamlValidator/ValidationRequest.cs new file mode 100644 index 00000000..46199731 --- /dev/null +++ b/src/YamlValidator/ValidationRequest.cs @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; +public readonly record struct ValidationRequest(string FilePath, string SchemaPath, string FilePathType); diff --git a/src/YamlValidator/Validator.cs b/src/YamlValidator/Validator.cs new file mode 100644 index 00000000..22e99756 --- /dev/null +++ b/src/YamlValidator/Validator.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Json.Schema; +using Yaml2JsonNode; +using System.Text.Json; + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; + +public class Validator +{ + private readonly EvaluationOptions _verbosityOptions; + private readonly JsonSerializerOptions _serializerOptions; + + + public Validator(EvaluationOptions options, JsonSerializerOptions resultSerializeOptions) + { + // to do: add verbosity flag and allow users to choose verbosity of evaluation + _verbosityOptions = options; + _serializerOptions = resultSerializeOptions; + + } + + public ValidatorResults Validate(JsonSchema schema, string yamlFileData) + { + var yamlStream = Utility.MakeYamlStream(yamlFileData); + 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") }); + } + 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 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) + { + IReadOnlyList traceList = results.Details.Where( + node => !node.IsValid && + node.HasErrors).ToList(); + foreach (var trace in traceList) + { + yamlValidatorErrors.Add(new ValidatorError(trace)); + } + } + IReadOnlyList fileErrors = yamlValidatorErrors; + var finalResults = new ValidatorResults(results.IsValid, fileErrors); + return finalResults; + + } +} diff --git a/src/YamlValidator/ValidatorError.cs b/src/YamlValidator/ValidatorError.cs new file mode 100644 index 00000000..3afec499 --- /dev/null +++ b/src/YamlValidator/ValidatorError.cs @@ -0,0 +1,39 @@ +// 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) + { + InstanceLocation = results.InstanceLocation.ToString(); + SchemaPath = results.EvaluationPath.ToString(); + Errors = results.Errors; + } + public ValidatorError(string error) + { + InstanceLocation = ""; + SchemaPath = ""; + Errors = new Dictionary { { "", error } }; + } + + public override string ToString() + { + var errString = ""; + if (Errors != null) + { + foreach (var error in Errors) + { + var errType = string.IsNullOrEmpty(error.Key) ? "Error" : error.Key; + errString += $"\t{errType}: {error.Value}\n"; + } + } + return $"InstanceLocation: {InstanceLocation}\nSchemaPath: {SchemaPath}\nErrors:\n{errString}"; + } +} diff --git a/src/YamlValidator/ValidatorResults.cs b/src/YamlValidator/ValidatorResults.cs new file mode 100644 index 00000000..400e7a52 --- /dev/null +++ b/src/YamlValidator/ValidatorResults.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.YamlValidator; +public class ValidatorResults +{ + public bool SchemaValid { get; } + public IReadOnlyList TraversalResults { get; } + + public ValidatorResults(bool schemaValid, IReadOnlyList traversalResults) + { + SchemaValid = schemaValid; + TraversalResults = traversalResults; + + } +} diff --git a/src/YamlValidator/VerbosityData.cs b/src/YamlValidator/VerbosityData.cs new file mode 100644 index 00000000..d8383614 --- /dev/null +++ b/src/YamlValidator/VerbosityData.cs @@ -0,0 +1,25 @@ +// 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/YamlValidator/YamlLoader.cs b/src/YamlValidator/YamlLoader.cs new file mode 100644 index 00000000..500da4d7 --- /dev/null +++ b/src/YamlValidator/YamlLoader.cs @@ -0,0 +1,33 @@ +// 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); + return new ReadOnlyDictionary(deserializedYaml); + } + + // to do: address edge case of .yml files + var files = Directory.GetFiles(filePath, $"*{Constants.YamlFileExtension}"); + foreach (var file in files) + { + var fileName = Path.GetFileName(file); + var yamlText = Utility.ReadFileData(file); + deserializedYaml.Add(fileName, yamlText); + } + + return new ReadOnlyDictionary(deserializedYaml); + } + +} diff --git a/src/YamlValidator/YamlValidator.csproj b/src/YamlValidator/YamlValidator.csproj new file mode 100644 index 00000000..e6653634 --- /dev/null +++ b/src/YamlValidator/YamlValidator.csproj @@ -0,0 +1,37 @@ + + + + Exe + net8.0 + enable + enable + + + + + + 1591, CA1822 + + + + 1591, CA1822 + + + + + + + + + + + + + + + + + + + +