diff --git a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs index 96c0ce501..18f1a59d6 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs +++ b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs @@ -77,7 +77,18 @@ internal static string ArgumentNullOrWhiteSpace { return ResourceManager.GetString("ArgumentNullOrWhiteSpace", resourceCulture); } } - + + /// + /// Looks up a localized string similar to The argument '{0}' is null.. + /// + internal static string ArgumentNull + { + get + { + return ResourceManager.GetString("ArgumentNull", resourceCulture); + } + } + /// /// Looks up a localized string similar to The filed name '{0}' of extension doesn't begin with x-.. /// diff --git a/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs b/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs index a0aee12e7..64f901c53 100644 --- a/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs +++ b/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs @@ -317,7 +317,7 @@ private void Validate(object item, Type type) type = typeof(IOpenApiReferenceable); } - var rules = _ruleSet.FindRules(type); + var rules = _ruleSet.FindRules(type.Name); foreach (var rule in rules) { rule.Evaluate(this as IValidationContext, item); diff --git a/src/Microsoft.OpenApi/Validations/ValidationRuleSet.cs b/src/Microsoft.OpenApi/Validations/ValidationRuleSet.cs index 11bc39f04..c34d4a451 100644 --- a/src/Microsoft.OpenApi/Validations/ValidationRuleSet.cs +++ b/src/Microsoft.OpenApi/Validations/ValidationRuleSet.cs @@ -4,7 +4,6 @@ using System; using System.Linq; using System.Reflection; -using System.Collections; using System.Collections.Generic; using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Properties; @@ -15,23 +14,44 @@ namespace Microsoft.OpenApi.Validations /// /// The rule set of the validation. /// - public sealed class ValidationRuleSet : IEnumerable + public sealed class ValidationRuleSet { - private readonly IDictionary> _rules = new Dictionary>(); + private readonly IDictionary> _rulesDictionary = new Dictionary>(); private static ValidationRuleSet _defaultRuleSet; private readonly IList _emptyRules = new List(); /// - /// Retrieve the rules that are related to a specific type + /// Gets the keys in this rule set. /// - /// The type that is to be validated - /// Either the rules related to the type, or an empty list. - public IList FindRules(Type type) + public ICollection Keys => _rulesDictionary.Keys; + + /// + /// Gets the rules in this rule set. + /// + public IList Rules => _rulesDictionary.Values.SelectMany(v => v).ToList(); + + /// + /// Gets the number of elements contained in this rule set. + /// + public int Count => _rulesDictionary.Count; + + /// + /// Initializes a new instance of the class. + /// + public ValidationRuleSet() + { + } + + /// + /// Retrieve the rules that are related to a specific key. + /// + /// The key of the rules to search for. + /// Either the rules related to the given key, or an empty list. + public IList FindRules(string key) { - IList results = null; - _rules.TryGetValue(type, out results); + _rulesDictionary.TryGetValue(key, out var results); return results ?? _emptyRules; } @@ -67,10 +87,22 @@ public static ValidationRuleSet GetEmptyRuleSet() } /// - /// Initializes a new instance of the class. + /// Add validation rules to the rule set. /// - public ValidationRuleSet() + /// The rule set to add validation rules to. + /// The validation rules to be added to the rules set. + /// Throws a null argument exception if the arguments are null. + public static void AddValidationRules(ValidationRuleSet ruleSet, IDictionary> rules) { + if (ruleSet == null || rules == null) + { + throw new OpenApiException(SRResource.ArgumentNull); + } + + foreach (var rule in rules) + { + ruleSet.Add(rule.Key, rule.Value); + } } /// @@ -86,7 +118,7 @@ public ValidationRuleSet(ValidationRuleSet ruleSet) foreach (ValidationRule rule in ruleSet) { - Add(rule); + Add(rule.ElementType.Name, rule); } } @@ -94,71 +126,161 @@ public ValidationRuleSet(ValidationRuleSet ruleSet) /// Initializes a new instance of the class. /// /// Rules to be contained in this ruleset. - public ValidationRuleSet(IEnumerable rules) + public ValidationRuleSet(IDictionary> rules) { if (rules == null) { return; } - foreach (ValidationRule rule in rules) + foreach (var rule in rules) { - Add(rule); + Add(rule.Key, rule.Value); } } /// - /// Gets the rules in this rule set. + /// Add the new rule into the rule set. /// - public IEnumerable Rules + /// The key for the rule. + /// The list of rules. + public void Add(string key, IList rules) { - get + foreach (var rule in rules) { - return _rules.Values.SelectMany(v => v); + Add(key, rule); } } /// - /// Add the new rule into the rule set. + /// Add a new rule into the rule set. /// + /// The key for the rule. /// The rule. - public void Add(ValidationRule rule) + /// Exception thrown when rule already exists. + public void Add(string key, ValidationRule rule) { - if (!_rules.ContainsKey(rule.ElementType)) + if (!_rulesDictionary.ContainsKey(key)) { - _rules[rule.ElementType] = new List(); + _rulesDictionary[key] = new List(); } - if (_rules[rule.ElementType].Contains(rule)) + if (_rulesDictionary[key].Contains(rule)) { throw new OpenApiException(SRResource.Validation_RuleAddTwice); } - _rules[rule.ElementType].Add(rule); + _rulesDictionary[key].Add(rule); } /// - /// Get the enumerator. + /// Updates an existing rule with a new one. /// - /// The enumerator. - public IEnumerator GetEnumerator() + /// The key of the existing rule. + /// The new rule. + /// The old rule. + /// true, if the update was successful; otherwise false. + public bool Update(string key, ValidationRule newRule, ValidationRule oldRule) { - foreach (var ruleList in _rules.Values) + if (_rulesDictionary.TryGetValue(key, out var currentRules)) { - foreach (var rule in ruleList) - { - yield return rule; - } + currentRules.Add(newRule); + return currentRules.Remove(oldRule); } + return false; + } + + /// + /// Removes a collection of rules. + /// + /// The key of the collection of rules to be removed. + /// true if the collection of rules with the provided key is removed; otherwise, false. + public bool Remove(string key) + { + return _rulesDictionary.Remove(key); + } + + /// + /// Removes a rule by key. + /// + /// The key of the rule to be removed. + /// The rule to be removed. + /// true if the rule is successfully removed; otherwise, false. + public bool Remove(string key, ValidationRule rule) + { + if (_rulesDictionary.TryGetValue(key, out IList validationRules)) + { + return validationRules.Remove(rule); + } + + return false; + } + + /// + /// Removes the first rule that matches the provided rule from the list of rules. + /// + /// The rule to be removed. + /// true if the rule is successfully removed; otherwise, false. + public bool Remove(ValidationRule rule) + { + return _rulesDictionary.Values.FirstOrDefault(x => x.Remove(rule)) is not null; + } + + /// + /// Clears all rules in this rule set. + /// + public void Clear() + { + _rulesDictionary.Clear(); + } + + /// + /// Determines whether the rule set contains an element with the specified key. + /// + /// The key to locate in the rule set. + /// true if the rule set contains an element with the key; otherwise, false. + public bool ContainsKey(string key) + { + return _rulesDictionary.ContainsKey(key); + } + + /// + /// Determines whether the provided rule is contained in the specified key in the rule set. + /// + /// The key to locate. + /// The rule to locate. + /// + public bool Contains(string key, ValidationRule rule) + { + return _rulesDictionary.TryGetValue(key, out IList validationRules) && validationRules.Contains(rule); + } + + /// + /// Gets the rules associated with the specified key. + /// + /// The key whose rules to get. + /// When this method returns, the rules associated with the specified key, if the + /// key is found; otherwise, an empty object. + /// This parameter is passed uninitialized. + /// true if the specified key has rules. + public bool TryGetValue(string key, out IList rules) + { + return _rulesDictionary.TryGetValue(key, out rules); } /// /// Get the enumerator. /// /// The enumerator. - IEnumerator IEnumerable.GetEnumerator() + public IEnumerator GetEnumerator() { - return this.GetEnumerator(); + foreach (var ruleList in _rulesDictionary.Values) + { + foreach (var rule in ruleList) + { + yield return rule; + } + } } private static ValidationRuleSet BuildDefaultRuleSet() @@ -179,7 +301,7 @@ private static ValidationRuleSet BuildDefaultRuleSet() ValidationRule rule = propertyValue as ValidationRule; if (rule != null) { - ruleSet.Add(rule); + ruleSet.Add(rule.ElementType.Name, rule); } } diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 2948950f7..c12a59de5 100755 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -1222,15 +1222,27 @@ namespace Microsoft.OpenApi.Validations { protected ValidationRule() { } } - public sealed class ValidationRuleSet : System.Collections.Generic.IEnumerable, System.Collections.IEnumerable + public sealed class ValidationRuleSet { public ValidationRuleSet() { } public ValidationRuleSet(Microsoft.OpenApi.Validations.ValidationRuleSet ruleSet) { } - public ValidationRuleSet(System.Collections.Generic.IEnumerable rules) { } - public System.Collections.Generic.IEnumerable Rules { get; } - public void Add(Microsoft.OpenApi.Validations.ValidationRule rule) { } - public System.Collections.Generic.IList FindRules(System.Type type) { } + public ValidationRuleSet(System.Collections.Generic.IDictionary> rules) { } + public int Count { get; } + public System.Collections.Generic.ICollection Keys { get; } + public System.Collections.Generic.IList Rules { get; } + public void Add(string key, Microsoft.OpenApi.Validations.ValidationRule rule) { } + public void Add(string key, System.Collections.Generic.IList rules) { } + public void Clear() { } + public bool Contains(string key, Microsoft.OpenApi.Validations.ValidationRule rule) { } + public bool ContainsKey(string key) { } + public System.Collections.Generic.IList FindRules(string key) { } public System.Collections.Generic.IEnumerator GetEnumerator() { } + public bool Remove(Microsoft.OpenApi.Validations.ValidationRule rule) { } + public bool Remove(string key) { } + public bool Remove(string key, Microsoft.OpenApi.Validations.ValidationRule rule) { } + public bool TryGetValue(string key, out System.Collections.Generic.IList rules) { } + public bool Update(string key, Microsoft.OpenApi.Validations.ValidationRule newRule, Microsoft.OpenApi.Validations.ValidationRule oldRule) { } + public static void AddValidationRules(Microsoft.OpenApi.Validations.ValidationRuleSet ruleSet, System.Collections.Generic.IDictionary> rules) { } public static Microsoft.OpenApi.Validations.ValidationRuleSet GetDefaultRuleSet() { } public static Microsoft.OpenApi.Validations.ValidationRuleSet GetEmptyRuleSet() { } } diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiValidatorTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiValidatorTests.cs index ef036a56b..85420890c 100644 --- a/test/Microsoft.OpenApi.Tests/Services/OpenApiValidatorTests.cs +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiValidatorTests.cs @@ -108,7 +108,7 @@ public void ValidateCustomExtension() { var ruleset = ValidationRuleSet.GetDefaultRuleSet(); - ruleset.Add( + ruleset.Add(typeof(OpenApiAny).Name, new ValidationRule( (context, item) => { diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiReferenceValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiReferenceValidationTests.cs index 3ed365c8d..43576475d 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiReferenceValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiReferenceValidationTests.cs @@ -1,11 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; @@ -67,7 +64,14 @@ public void ReferencedSchemaShouldOnlyBeValidatedOnce() }; // Act - var errors = document.Validate(new ValidationRuleSet() { new AlwaysFailRule() }); + var rules = new Dictionary>() + { + { typeof(OpenApiSchema).Name, + new List() { new AlwaysFailRule() } + } + }; + + var errors = document.Validate(new ValidationRuleSet(rules)); // Assert @@ -97,8 +101,15 @@ public void UnresolvedReferenceSchemaShouldNotBeValidated() } }; - // Act - var errors = document.Validate(new ValidationRuleSet() { new AlwaysFailRule() }); + // Act + var rules = new Dictionary>() + { + { typeof(AlwaysFailRule).Name, + new List() { new AlwaysFailRule() } + } + }; + + var errors = document.Validate(new ValidationRuleSet(rules)); // Assert Assert.True(errors.Count() == 0); @@ -147,7 +158,14 @@ public void UnresolvedSchemaReferencedShouldNotBeValidated() }; // Act - var errors = document.Validate(new ValidationRuleSet() { new AlwaysFailRule() }); + var rules = new Dictionary>() + { + { typeof(AlwaysFailRule).Name, + new List() { new AlwaysFailRule() } + } + }; + + var errors = document.Validate(new ValidationRuleSet(rules)); // Assert Assert.True(errors.Count() == 0); diff --git a/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs index 5124375ac..7685f80ca 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs @@ -1,50 +1,191 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Collections.Generic; using System.Linq; +using Microsoft.OpenApi.Models; using Xunit; -using Xunit.Abstractions; namespace Microsoft.OpenApi.Validations.Tests { public class ValidationRuleSetTests { - private readonly ITestOutputHelper _output; + private readonly ValidationRule _contactValidationRule = new ValidationRule( + (context, item) => { }); - public ValidationRuleSetTests(ITestOutputHelper output) + private readonly ValidationRule _headerValidationRule = new ValidationRule( + (context, item) => { }); + + private readonly ValidationRule _parameterValidationRule = new ValidationRule( + (context, item) => { }); + + private readonly IDictionary> _rulesDictionary; + + public ValidationRuleSetTests() + { + _rulesDictionary = new Dictionary>() + { + {"contact", new List { _contactValidationRule } }, + {"header", new List { _headerValidationRule } }, + {"parameter", new List { _parameterValidationRule } } + }; + } + + [Fact] + public void RuleSetConstructorsReturnsTheCorrectRules() { - _output = output; + // Arrange & Act + var ruleSet_1 = ValidationRuleSet.GetDefaultRuleSet(); + var ruleSet_2 = new ValidationRuleSet(ValidationRuleSet.GetDefaultRuleSet()); + var ruleSet_3 = new ValidationRuleSet(_rulesDictionary); + var ruleSet_4 = new ValidationRuleSet(); + + // Assert + Assert.NotNull(ruleSet_1?.Rules); + Assert.NotNull(ruleSet_2?.Rules); + Assert.NotNull(ruleSet_3?.Rules); + Assert.NotNull(ruleSet_4); + + Assert.NotEmpty(ruleSet_1.Rules); + Assert.NotEmpty(ruleSet_2.Rules); + Assert.NotEmpty(ruleSet_3.Rules); + Assert.Empty(ruleSet_4.Rules); + + // Update the number if you add new default rule(s). + Assert.Equal(22, ruleSet_1.Rules.Count); + Assert.Equal(22, ruleSet_2.Rules.Count); + Assert.Equal(3, ruleSet_3.Rules.Count); } [Fact] - public void DefaultRuleSetReturnsTheCorrectRules() + public void RemoveValidatioRuleGivenTheValidationRuleWorks() { // Arrange - var ruleSet = new ValidationRuleSet(); + var ruleSet = new ValidationRuleSet(_rulesDictionary); + var responseValidationRule = new ValidationRule((context, item) => { }); + + // Act and Assert + Assert.True(ruleSet.Remove(_contactValidationRule)); + Assert.False(ruleSet.Rules.Contains(_contactValidationRule)); + Assert.False(ruleSet.Remove(_contactValidationRule)); // rule already removed + } + + [Fact] + public void RemoveValidationRuleGivenTheKeyAndValidationRuleWorks() + { + // Arrange + var ruleSet = new ValidationRuleSet(_rulesDictionary); // Act + ruleSet.Remove("contact", _contactValidationRule); + ruleSet.Remove("parameter", _headerValidationRule); // validation rule not in parameter key; shouldn't remove + ruleSet.Remove("foo", _parameterValidationRule); // key does not exist; shouldn't remove + var rules = ruleSet.Rules; // Assert - Assert.NotNull(rules); - Assert.Empty(rules); + Assert.False(rules.Contains(_contactValidationRule)); + Assert.True(rules.Contains(_headerValidationRule)); + Assert.True(rules.Contains(_parameterValidationRule)); } [Fact] - public void DefaultRuleSetPropertyReturnsTheCorrectRules() + public void RemoveRulesGivenAKeyWorks() { - // Arrange & Act - var ruleSet = ValidationRuleSet.GetDefaultRuleSet(); - Assert.NotNull(ruleSet); // guard + // Arrange + var ruleSet = new ValidationRuleSet(_rulesDictionary); + var responseValidationRule = new ValidationRule((context, item) => { }); + ruleSet.Add("response", new List { responseValidationRule }); + Assert.True(ruleSet.ContainsKey("response")); + Assert.True(ruleSet.Rules.Contains(responseValidationRule)); // guard - var rules = ruleSet.Rules; + // Act + ruleSet.Remove("response"); // Assert - Assert.NotNull(rules); - Assert.NotEmpty(rules); + Assert.False(ruleSet.ContainsKey("response")); + } - // Update the number if you add new default rule(s). - Assert.Equal(22, rules.Count()); + [Fact] + public void AddNewValidationRuleWorks() + { + // Arrange + var ruleSet = new ValidationRuleSet(_rulesDictionary); + var responseValidationRule = new ValidationRule((context, item) => { }); + var tagValidationRule = new ValidationRule((context, item) => { }); + var pathsValidationRule = new ValidationRule((context, item) => { }); + + // Act + ruleSet.Add("response", new List { responseValidationRule }); + ruleSet.Add("tag", new List { tagValidationRule }); + var rulesDictionary = new Dictionary>() + { + {"paths", new List { pathsValidationRule } } + }; + + ValidationRuleSet.AddValidationRules(ruleSet, rulesDictionary); + + // Assert + Assert.True(ruleSet.ContainsKey("response")); + Assert.True(ruleSet.ContainsKey("tag")); + Assert.True(ruleSet.ContainsKey("paths")); + Assert.True(ruleSet.Rules.Contains(responseValidationRule)); + Assert.True(ruleSet.Rules.Contains(tagValidationRule)); + Assert.True(ruleSet.Rules.Contains(pathsValidationRule)); + } + + [Fact] + public void UpdateValidationRuleWorks() + { + // Arrange + var ruleSet = new ValidationRuleSet(_rulesDictionary); + var responseValidationRule = new ValidationRule((context, item) => { }); + ruleSet.Add("response", new List { responseValidationRule }); + + // Act + var pathsValidationRule = new ValidationRule((context, item) => { }); + ruleSet.Update("response", pathsValidationRule, responseValidationRule); + + // Assert + Assert.True(ruleSet.Contains("response", pathsValidationRule)); + Assert.False(ruleSet.Contains("response", responseValidationRule)); + } + + [Fact] + public void TryGetValueWorks() + { + // Arrange + var ruleSet = new ValidationRuleSet(_rulesDictionary); + + // Act + ruleSet.TryGetValue("contact", out var validationRules); + + // Assert + Assert.True(validationRules.Any()); + Assert.True(validationRules.Contains(_contactValidationRule)); + } + + [Fact] + public void ClearAllRulesWorks() + { + // Arrange + var ruleSet = new ValidationRuleSet(); + var tagValidationRule = new ValidationRule((context, item) => { }); + var pathsValidationRule = new ValidationRule((context, item) => { }); + var rulesDictionary = new Dictionary>() + { + {"paths", new List { pathsValidationRule } }, + {"tag", new List { tagValidationRule } } + }; + + ValidationRuleSet.AddValidationRules(ruleSet, rulesDictionary); + Assert.NotEmpty(ruleSet.Rules); + + // Act + ruleSet.Clear(); + + // Assert + Assert.Empty(ruleSet.Rules); } } }