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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+