diff --git a/Forge.TreeWalker.UnitTests/test/TreeSchemaValidatorTests.cs b/Forge.TreeWalker.UnitTests/test/TreeSchemaValidatorTests.cs index bc93a59..acb818d 100644 --- a/Forge.TreeWalker.UnitTests/test/TreeSchemaValidatorTests.cs +++ b/Forge.TreeWalker.UnitTests/test/TreeSchemaValidatorTests.cs @@ -3,7 +3,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // // -// Tests the TreeSchemaValidator class. +// Tests for the TreeSchemaValidator class. // //----------------------------------------------------------------------- @@ -18,7 +18,6 @@ namespace Microsoft.Forge.TreeWalker.UnitTests using Newtonsoft.Json; using Newtonsoft.Json.Schema; using Microsoft.Forge.TreeWalker; - using System.Threading.Tasks; [TestClass] public class TreeSchemaValidatorTests @@ -53,59 +52,67 @@ namespace Microsoft.Forge.TreeWalker.UnitTests invalidSchemaWithErrorContent = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, "test\\InvalidTestSchemas\\InvalidTestSchemaErrorContent.json")); invalidSchemaDirectoryPath = "test\\ExampleSchemas\\TardigradeSchema.json"; } + [TestMethod] public void Test_ValidateSchemaInForgeTreeWithCustomRulesInString() { - var res = ForgeSchemaValidator.ValidateSchema(treeSchema, stringRules, out List errorList); + bool res = ForgeSchemaValidator.ValidateSchema(treeSchema, stringRules, out List errorList); Assert.AreEqual(true, res); Assert.AreEqual(0, errorList.Count); } + [TestMethod] public void Test_ValidateSchemaInForgeTreeWithCustomRulesInJSchema() { - var res = ForgeSchemaValidator.ValidateSchema(treeSchema, jschemaRules, out List errorList); + bool res = ForgeSchemaValidator.ValidateSchema(treeSchema, jschemaRules, out List errorList); Assert.AreEqual(true, res); Assert.AreEqual(0, errorList.Count); } + [TestMethod] public void Test_ValidateSchemaInForgeTreeListWithCustomRulesInString() { - var res = ForgeSchemaValidator.ValidateSchema(treeSchema, stringRules, out List errorList); + bool res = ForgeSchemaValidator.ValidateSchema(treeSchema, stringRules, out List errorList); Assert.AreEqual(true, res); Assert.AreEqual(0, errorList.Count); } + [TestMethod] public void Test_ValidateSchemaInForgeTreeListWithCustomRulesInJSchema() { - var res = ForgeSchemaValidator.ValidateSchema(treeSchema, jschemaRules, out List errorList); + bool res = ForgeSchemaValidator.ValidateSchema(treeSchema, jschemaRules, out List errorList); Assert.AreEqual(true, res); Assert.AreEqual(0, errorList.Count); } + [TestMethod] - public void Test_ValidateSchemaInStringWithCustomRulesInString() + public void Test_ValidateSchemaInStringWithCustomRules() { - var res = ForgeSchemaValidator.ValidateSchemaString(stringSchema, stringRules, out List errorList); + bool res = ForgeSchemaValidator.ValidateSchemaString(stringSchema, stringRules, false, out List errorList); Assert.AreEqual(true, res); Assert.AreEqual(0, errorList.Count); } + [TestMethod] public void Test_ValidateSchemaInStringWithCustomRulesInJSchema() { - var res = ForgeSchemaValidator.ValidateSchemaString(stringSchema, jschemaRules, out List errorList); + bool res = ForgeSchemaValidator.ValidateSchemaString(stringSchema, jschemaRules, false, out List errorList); Assert.AreEqual(true, res); Assert.AreEqual(0, errorList.Count); } + [TestMethod] public void Test_ValidateSchemasInStringListWithCustomRulesInString() { - var res = ForgeSchemaValidator.ValidateSchemaString(stringSchemaList, stringRules, out List errorList); + bool res = ForgeSchemaValidator.ValidateSchemaString(stringSchemaList, stringRules, false, out List errorList); Assert.AreEqual(true, res); Assert.AreEqual(0, errorList.Count); } + [TestMethod] public void Test_ValidateSchemasInStringListWithCustomRulesInJSchema() { - var res = ForgeSchemaValidator.ValidateSchemaString(stringSchemaList, jschemaRules, out List errorList); + bool res = ForgeSchemaValidator.ValidateSchemaString(stringSchemaList, jschemaRules, false, out List errorList); Assert.AreEqual(true, res); Assert.AreEqual(0, errorList.Count); } @@ -113,59 +120,96 @@ namespace Microsoft.Forge.TreeWalker.UnitTests [TestMethod] public void Test_ValidateSchemaFromPathWithCustomRulesInString() { - var res = ForgeSchemaValidator.ValidateSchemaInPath(schemaPath, stringRules, out List errorList); + bool res = ForgeSchemaValidator.ValidateSchemaInPath(schemaPath, stringRules, false, out List errorList); Assert.AreEqual(true, res); Assert.AreEqual(0, errorList.Count); } + [TestMethod] public void Test_ValidateSchemaFromPathWithCustomRulesInJSchema() { - var res = ForgeSchemaValidator.ValidateSchemaInPath(schemaPath, jschemaRules, out List errorList); + bool res = ForgeSchemaValidator.ValidateSchemaInPath(schemaPath, jschemaRules, false, out List errorList); Assert.AreEqual(true, res); Assert.AreEqual(0, errorList.Count); } + [TestMethod] public void Test_ValidateSchemasFromDirectoryListWithCustomRulesInString() { - var res = ForgeSchemaValidator.ValidateMultipleSchemasInPath(schemaDirectoryPath, stringRules, out List errorList); + bool res = ForgeSchemaValidator.ValidateMultipleSchemasInPath(schemaDirectoryPath, stringRules, false, out List errorList); Assert.AreEqual(true, res); Assert.AreEqual(0, errorList.Count); } + [TestMethod] public void Test_ValidateSchemasFromDirectoryListWithCustomRulesInJSchema() { - var res = ForgeSchemaValidator.ValidateMultipleSchemasInPath(schemaDirectoryPath, jschemaRules, out List errorList); + bool res = ForgeSchemaValidator.ValidateMultipleSchemasInPath(schemaDirectoryPath, jschemaRules, false, out List errorList); Assert.AreEqual(true, res); Assert.AreEqual(0, errorList.Count); } + [TestMethod] public void Test_ValidateInvalidSchemaInString() { - var res = ForgeSchemaValidator.ValidateSchemaString(invalidSchemaNotTree, jschemaRules, out List errorList); - Assert.AreEqual(false, res); - Assert.AreEqual("Required property 'Type' not found in JSON. Path 'Tree.Root', line 5, position 9.", errorList.First()); + bool res = ForgeSchemaValidator.ValidateSchemaString(stringSchema, jschemaRules, false, out List errorList); + Assert.AreEqual(true, res); + Assert.AreEqual(0, errorList.Count); } + [TestMethod] public void Test_ValidateInvalidSchemaDirectoryPath() { - var res = ForgeSchemaValidator.ValidateMultipleSchemasInPath(invalidSchemaDirectoryPath, jschemaRules,out List errorList); + bool res = ForgeSchemaValidator.ValidateMultipleSchemasInPath(invalidSchemaDirectoryPath, jschemaRules, false, out List errorList); Assert.AreEqual(false, res); Assert.AreEqual("The directory name is invalid.\r\n", errorList.First()); } + [TestMethod] public void Test_ValidateSchemaWithErrorContent() { - var res = ForgeSchemaValidator.ValidateSchemaString(invalidSchemaWithErrorContent, jschemaRules, out List errorList); + bool res = ForgeSchemaValidator.ValidateSchemaString(invalidSchemaWithErrorContent, jschemaRules, false, out List errorList); Assert.AreEqual(false, res); Assert.AreEqual("JSON is valid against no schemas from 'oneOf'. line: 3, position: 17", errorList.First()); } + + [TestMethod] + public void Test_ValidateInvalidSchemaAsDictionary() + { + bool res = ForgeSchemaValidator.ValidateSchemaString(invalidSchemaWithErrorContent, jschemaRules, true, out List errorList); + Assert.AreEqual(false, res); + Assert.AreEqual("JSON is valid against no schemas from 'oneOf'. line: 3, position: 17", errorList.First()); + } + + [TestMethod] + public void Test_ValidateValidSchemaAsDictionary() + { + bool res = ForgeSchemaValidator.ValidateSchemaString(invalidSchemaWithErrorContent, jschemaRules, true, out List errorList); + Assert.AreEqual(false, res); + Assert.AreEqual("JSON is valid against no schemas from 'oneOf'. line: 3, position: 17", errorList.First()); + } + + [TestMethod] + public void Test_ValidateSchemasFromDirectoryListAsDictionaryWithCustomRulesInString() + { + bool res = ForgeSchemaValidator.ValidateMultipleSchemasInPath(schemaDirectoryPath, stringRules, true, out List errorList); + Assert.AreEqual(false, res); + Assert.AreEqual("An item with the same key has already been added.", errorList.First()); + } + [TestMethod] public void Test_GetLinkedJSchemaRules() { - var linkedRules = ForgeSchemaValidator.GetLinkedJSchemaRules(stringRules, stringRules, "//ForgeSchemaValidationRules.json", out string error); - Assert.AreEqual("", error); - ForgeSchemaValidator.ValidateSchema(treeSchema, linkedRules, out List errorList); - Assert.AreEqual(0, errorList.Count); + try + { + JSchema linkedRules = ForgeSchemaValidator.GetLinkedJSchemaRules(stringRules, stringRules, "//ForgeSchemaValidationRules.json"); + ForgeSchemaValidator.ValidateSchema(treeSchema, linkedRules, out List errorList); + Assert.AreEqual(0, errorList.Count); + } + catch (Exception ex) + { + Assert.Fail("Expected no exception, but got: " + ex.Message); + } } } } diff --git a/Forge.TreeWalker/src/ForgeSchemaValidator.cs b/Forge.TreeWalker/src/ForgeSchemaValidator.cs index 7211cf9..5673ba0 100644 --- a/Forge.TreeWalker/src/ForgeSchemaValidator.cs +++ b/Forge.TreeWalker/src/ForgeSchemaValidator.cs @@ -3,200 +3,170 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // // -// The ForgeSchemaValidator class implements the ITreeSchemaValidator interface. +// The ForgeSchemaValidator class. // //----------------------------------------------------------------------- + namespace Microsoft.Forge.TreeWalker { using System; using System.Collections.Generic; using System.IO; - using System.Threading.Tasks; using Microsoft.Forge.DataContracts; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Schema; + /// - /// The ForgeSchemaValidator class implements the validation method that tests input schemas with custom rules from input. + /// The ForgeSchemaValidator class implements the validation method that tests input schemas with input custom rules. /// public static class ForgeSchemaValidator { + /// - /// The GetLinkedJSchema method that creates the linked rules in JSchema. + /// Linked rules in JSchema type. /// /// The rules to be included in the parent rules /// The parent rules to absorb the child rules /// The address of childRules - /// The result of schema combination. ErrorMessage would be set if it throw exceptions - public static JSchema GetLinkedJSchemaRules(string childRules, string parentRules, string referenceUri, out string errorMessage) + /// The result of schema combination. + public static JSchema GetLinkedJSchemaRules(string childRules, string parentRules, string referenceUri) { - try - { - JSchemaPreloadedResolver resolver = new JSchemaPreloadedResolver(); - resolver.Add(new Uri(referenceUri), childRules); - errorMessage = ""; - return JSchema.Parse(parentRules, resolver); - } - catch(Exception e) - { - errorMessage = e.Message; - return null; - } + JSchemaPreloadedResolver resolver = new JSchemaPreloadedResolver(); + resolver.Add(new Uri(referenceUri), childRules); + + return JSchema.Parse(parentRules, resolver); } /// - /// The validate task that validate the input schema with custom rules in string. + /// Validates the ForgeTree schema with the given rules. /// /// The schema to be validated - /// The rules used to validate input schemas - /// The result of schema validation. The errorList would contain error message if validation fails - public static bool ValidateSchema(ForgeTree schema, string rules, out List errorList) + /// The rules used to validate input schemas is only allowed in string or JSchema type + /// /// The result of schema validation. The errorList would contain error message if validation fails + public static bool ValidateSchema(ForgeTree schema, object rules, out List errorList) { - return Validate(new List { SerializeForgeTree(schema) }, JSchema.Parse(rules), out errorList); + return Validate(new List { SerializeToJObject(schema) }, rules, out errorList); } + /// - /// The validate task that validate the input schema with custom rules in string. - /// - /// The schema to be validated - /// The rules used to validate input schemas - /// The result of schema validation. The errorList would contain error message if validation fails - public static bool ValidateSchema(ForgeTree schema, JSchema rules, out List errorList) - { - return Validate(new List { SerializeForgeTree(schema) }, rules, out errorList); - } - /// - /// The validate task that validate multiple input schemas with custom rules in string. + /// Validates single or multiple schemas in Dictionary with the given rules. /// /// The schemas to be validated - /// The rules used to validate input schemas + /// The rules used to validate input schemas is only allowed in string or JSchema type + /// True if the custom rules is to handle the whole dictionary /// The result of schema validation. The errorList would contain error message if validation fails - public static bool ValidateSchemas(Dictionary schemas, string rules, out List errorList) + public static bool ValidateSchemas(Dictionary schemas, object rules, bool validateAsDictionary, out List errorList) { - return Validate(ConvertDictionaryToForgeTreeList(schemas), JSchema.Parse(rules), out errorList); + return Validate(ConvertDictionaryToJObjectList(schemas, validateAsDictionary), rules, out errorList); } + /// - /// The validate task that check the input schema in string with custom rules in string. + /// Validates single or multiple schemas in string with the given rules. /// /// The schema to be validated - /// The rules used to validate input schemas + /// The rules used to validate input schemas is only allowed in string or JSchema type + /// True if the custom rules is to handle the whole dictionary /// The result of schema validation. The errorList would contain error message if validation fails - public static bool ValidateSchemaString(string schema, string rules, out List errorList) + public static bool ValidateSchemaString(string schema, object rules, bool validateAsDictionary, out List errorList) { - var schemaList = ConvertStringToJObjectList(schema, out errorList); - return CheckConvertErrorAndValidate(JSchema.Parse(rules), ref errorList, schemaList); + List schemaList = ConvertStringToJObjectList(schema, out errorList, validateAsDictionary); + + return CheckConvertErrorAndValidate(rules, ref errorList, schemaList); } /// - /// The validate task that validate the schema in the input file path with custom rules in string. + /// Validates single or multiple schemas from input path with the given rules. /// /// The path that contains a schema file - /// The rules used to validate input schemas + /// The rules used to validate input schemas is only allowed in string or JSchema type + /// True if the custom rules is to handle the whole dictionary /// The result of schema validation. The errorList would contain error message if validation fails - public static bool ValidateSchemaInPath(string path, string rules, out List errorList) + public static bool ValidateSchemaInPath(string path, object rules, bool validateAsDictionary, out List errorList) { - var schemas = GetSchemaFromPath(path, out errorList); - return CheckConvertErrorAndValidate(JSchema.Parse(rules), ref errorList, schemas); - } + List schemas = GetSchemaFromPath(path, out errorList, validateAsDictionary); - /// - /// The validate task that validate all schemas in a directory with custom rules in string. - /// - /// The path that contains a schemas directory - /// The rules used to validate input schemas - /// The result of schema validation. The errorList would contain error message if validation fails - public static bool ValidateMultipleSchemasInPath(string path, string rules, out List errorList) - { - var schemas = GetAllSchemasInDirectory(path, out errorList); - return CheckConvertErrorAndValidate(JSchema.Parse(rules), ref errorList, schemas); - } - /// - /// The validate task that validate multiple input schemas with custom rules in JSchema. - /// - /// The schemas to be validated - /// The rules used to validate input schemas - /// The result of schema validation. The errorList would contain error message if validation fails - public static bool ValidateSchemas(Dictionary schemas, JSchema rules, out List errorList) - { - return Validate(ConvertDictionaryToForgeTreeList(schemas), rules, out errorList); - } - /// - /// The validate task that validate the schema in the input file path with custom rules in JSchema. - /// - /// The path that contains a schema file - /// The rules used to validate input schemas - /// The result of schema validation. The errorList would contain error message if validation fails - public static bool ValidateSchemaInPath(string path, JSchema rules, out List errorList) - { - var schema = GetSchemaFromPath(path, out errorList); - return CheckConvertErrorAndValidate(rules, ref errorList, schema); - } - /// - /// The validate task that validate all schemas in a directory with custom rules in JSchema. - /// - /// The path that contains a schemas directory - /// The rules used to validate input schemas - /// The result of schema validation. The errorList would contain error message if validation fails - public static bool ValidateMultipleSchemasInPath(string path, JSchema rules, out List errorList) - { - var schemas = GetAllSchemasInDirectory(path, out errorList); return CheckConvertErrorAndValidate(rules, ref errorList, schemas); } /// - /// The validate task that check the input schema in string with custom rules in JSchema. + /// Validates single or multiple schemas from input path with the given rules. /// - /// The schema to be validated - /// The rules used to validate input schemas + /// The path that contains a schemas directory + /// The rules used to validate input schemas is only allowed in string or JSchema type + /// True if the custom rules is to handle the whole dictionary /// The result of schema validation. The errorList would contain error message if validation fails - public static bool ValidateSchemaString(string schema, JSchema rules, out List errorList) + public static bool ValidateMultipleSchemasInPath(string path, object rules, bool validateAsDictionary, out List errorList) { - var schemaList = ConvertStringToJObjectList(schema, out errorList); - if (errorList.Count > 0) - { - return false; - } - var res = Validate(schemaList, rules, out errorList); - return res; + List schemas = GetAllSchemasInDirectory(path, out errorList, validateAsDictionary); + + return CheckConvertErrorAndValidate(rules, ref errorList, schemas); } - private static List ConvertDictionaryToForgeTreeList(Dictionary schemas) + + private static List ConvertDictionaryToJObjectList(Dictionary schemas, bool validateAsDictionary) { - var schemaList = new List(); - foreach (var item in schemas.Values) - schemaList.Add(SerializeForgeTree(item)); + List schemaList = new List(); + + if (validateAsDictionary) + { + schemaList.Add(SerializeToJObject(schemas)); + } + else + { + foreach (ForgeTree item in schemas.Values) + { + schemaList.Add(SerializeToJObject(item)); + } + } + return schemaList; } - private static List ConvertStringToJObjectList(string schema, out List errorList) + private static List ConvertStringToJObjectList(string schema, out List errorList, bool validateAsDictionary) { - var schemaList = new List(); + List schemaList = new List(); errorList = new List(); + try { + //There could be three possible cases: + //1. TreeName mapped to the ForgeTree in the dictionary and custom rules handle dictionary. + //2. There are only ForgeTree without matching forge tree name custom rules handle ForgeTree list. Dictionary forgeTrees = JsonConvert.DeserializeObject>(schema); - foreach (var kvp in forgeTrees) + + if (validateAsDictionary) { - ForgeTree forgeTree = kvp.Value; - if (forgeTree.Tree == null) + schemaList.Add(JObject.Parse(schema)); + } + else + { + foreach (KeyValuePair kvp in forgeTrees) { - // Deserialize into Dictionary does not throw exception but will have null "Tree" property if schema is just a ForgeTree. - // try to deserialize string to forge tree directly - JsonConvert.DeserializeObject(schema); - JObject res = JObject.Parse(schema); - schemaList.Add(res); - break; + ForgeTree forgeTree = kvp.Value; + + if (forgeTree.Tree == null) + { + // Deserialize into Dictionary does not throw exception but will have null "Tree" property if schema is just a ForgeTree. + // try to deserialize string to forge tree directly + JsonConvert.DeserializeObject(schema); + schemaList.Add(JObject.Parse(schema)); + break; + } + + schemaList.Add(SerializeToJObject(forgeTree)); } - schemaList.Add(SerializeForgeTree(forgeTree)); } } catch (Exception e) { errorList.Add(e.Message); } + return schemaList; } - private static JObject SerializeForgeTree(ForgeTree forgeTree) + private static JObject SerializeToJObject(object forgeTree) { string stringSchema = JsonConvert.SerializeObject( forgeTree, @@ -205,79 +175,130 @@ namespace Microsoft.Forge.TreeWalker DefaultValueHandling = DefaultValueHandling.Ignore, // Prevent default values from getting added to serialized json schema. Converters = new List { new Newtonsoft.Json.Converters.StringEnumConverter() } // Use string enum values instead of numerical. }); + return JObject.Parse(stringSchema); } - private static List GetSchemaFromPath(string path, out List errorList) + private static List GetSchemaFromPath(string path, out List errorList, bool validateAsDictionary) { errorList = new List(); + try { - var schema = File.ReadAllText(path); - var res = ConvertStringToJObjectList(schema, out List convertError); + string schema = File.ReadAllText(path); + + List res = ConvertStringToJObjectList(schema, out List convertError, validateAsDictionary); errorList = convertError; + return res; } catch (Exception e) { errorList.Add(e.Message); + return new List(); } } - private static List GetAllSchemasInDirectory(string path, out List errorList) + private static List GetAllSchemasInDirectory(string path, out List errorList, bool validateAsDictionary) { - var schemaList = new List(); + List schemaList = new List(); errorList = new List(); + try { string[] Files = Directory.GetFiles(path); - var schemalist = new List(); - foreach (string file in Files) - { - var schemasInFile = GetSchemaFromPath(file, out errorList); - if (errorList.Count > 0) + List schemalist = new List(); + + if (validateAsDictionary) { + Dictionary combinedDictionary = new Dictionary(); + + foreach (string file in Files) { - break; + string schema = File.ReadAllText(file); + Dictionary schemaDictionary = JsonConvert.DeserializeObject>(schema); + + foreach (KeyValuePair item in schemaDictionary) + { + combinedDictionary.Add(item.Key, item.Value); + } + } + + return new List { SerializeToJObject(combinedDictionary) }; + } + else + { + foreach (string file in Files) + { + List schemasInFile = GetSchemaFromPath(file, out errorList, validateAsDictionary); + + if (errorList.Count > 0) + { + break; + } + + schemasInFile.ForEach(n => schemaList.Add(n)); } - schemasInFile.ForEach(n => schemaList.Add(n)); } } catch (Exception e) { errorList.Add(e.Message); } + return schemaList; } - private static bool CheckConvertErrorAndValidate(JSchema rules, ref List errorList, List schemaList) + private static bool CheckConvertErrorAndValidate(Object rules, ref List errorList, List schemaList) { if (errorList.Count > 0) { return false; } + return Validate(schemaList, rules, out errorList); } - private static bool Validate(List schemas, JSchema rules, out List errorList) + private static bool Validate(List schemas, Object rules, out List errorList) { errorList = new List(); + JSchema jSchemaRules = null; + + if (rules is string) + { + jSchemaRules = JSchema.Parse((string)rules); + } + else if (rules is JSchema) + { + jSchemaRules = (JSchema)rules; + } + else + { + errorList.Add("Rules type could only be string or JSchema"); + + return false; + } + if (schemas.Count == 0) { errorList.Add("Can't find target schema to test or file type is not supported"); + return false; } - foreach (var schema in schemas) + + foreach (JObject schema in schemas) { - if (!schema.IsValid(rules, out IList errorDetail)) + if (!schema.IsValid(jSchemaRules, out IList errorDetail)) { - foreach (var error in errorDetail) + foreach (ValidationError error in errorDetail) { errorList.Add(error.Message + " line: " + error.LineNumber + ", position: " + error.LinePosition); } + return false; } } + return true; } }