diff --git a/src/Analyzer.Cli/CommandLineParser.cs b/src/Analyzer.Cli/CommandLineParser.cs index 0d0fa701..14139cc5 100644 --- a/src/Analyzer.Cli/CommandLineParser.cs +++ b/src/Analyzer.Cli/CommandLineParser.cs @@ -80,7 +80,9 @@ private Command SetupAnalyzeTemplateCommand() foreach (var evaluation in evaluations) { - Console.WriteLine($"{evaluation.RuleName}: {evaluation.RuleDescription}, Result: {evaluation.Passed.ToString()}"); + string resultString = GenerateResultString(evaluation, templateFilePath.FullName, parametersFilePath == null ? null : File.ReadAllText(parametersFilePath.FullName)); + + Console.WriteLine($"\n\n{evaluation.RuleName}: {evaluation.RuleDescription}\n\tResult: {evaluation.Passed} {resultString}"); } } catch (Exception exp) @@ -93,6 +95,34 @@ private Command SetupAnalyzeTemplateCommand() return analyzeTemplateCommand; } + private string GenerateResultString(Types.IEvaluation evaluation, string templateFilePath, string parametersFilePath) + { + string resultString = ""; + + if (!evaluation.Passed) + { + foreach (var innerEvaluation in evaluation.Evaluations) + { + resultString += GenerateResultString(innerEvaluation, templateFilePath, parametersFilePath); + + foreach (var result in innerEvaluation.Results) + { + if (!innerEvaluation.Passed) + { + resultString += $"\n\tFile: {templateFilePath}"; + if (parametersFilePath != null) + { + resultString += $"\n\tParameters File: {parametersFilePath}"; + } + resultString += $"\n\tLine: {result.LineNumber}\n\t{result.FailureMessage()}"; + } + } + } + } + + return resultString; + } + private Command SetupAnalyzeDirectoryCommand() { // Setup analyze-directory diff --git a/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs b/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs new file mode 100644 index 00000000..1bf2e6e5 --- /dev/null +++ b/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs @@ -0,0 +1,334 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Expressions; +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Operators; +using Microsoft.Azure.Templates.Analyzer.Types; +using Microsoft.Azure.Templates.Analyzer.Utilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.UnitTests +{ + [TestClass] + public class JsonRuleResultTests + { + [DataTestMethod] + [DataRow("EXPECTED_VALUE", "ACTUAL_VALUE", DisplayName = "JToken values are strings")] + [DataRow(1, 2, DisplayName = "JToken values are ints")] + [DataRow(true, false, DisplayName = "JToken values are bools")] + [DataRow(null, "ACTUAL_VALUE", DisplayName = "Expected value is null")] + [DataRow("EXPECTED_VALUE", null, DisplayName = "Actual value is null")] + [DataRow("ACTUAL_VALUE", "ACTUAL_VALUE", true, DisplayName = "NotEquals message shouldn't include the word \"not\"")] + public void FailureMessage_ValidJTokensForEqualsOperator_FailureMessageIsReturnedAsExpected(object expectedValue, object actualValue, bool isNegative = false) + { + // Arrange + var mockOperator = new Mock(); + mockOperator + .Setup(s => s.EvaluateExpression(It.IsAny())) + .Returns(false); + mockOperator.Object.SpecifiedValue = TestUtilities.ToJToken(expectedValue); + mockOperator.Object.FailureMessage = JsonRuleEngineConstants.EqualsFailureMessage; + mockOperator.Object.IsNegative = isNegative; + + var mockJsonPathResolver = new Mock(); + mockJsonPathResolver + .Setup(s => s.JToken) + .Returns(TestUtilities.ToJToken(actualValue)); + mockJsonPathResolver + .Setup(s => s.Path) + .Returns("some.path"); + + var mockLineNumberResolver = new Mock(); + mockLineNumberResolver + .Setup(s => s.ResolveLineNumber(mockJsonPathResolver.Object.Path)) + .Returns(0); + + var mockExpression = new LeafExpression(mockLineNumberResolver.Object, mockOperator.Object, new ExpressionCommonProperties { Path = "some.path" }); + + var result = new JsonRuleResult() + { + Passed = mockOperator.Object.EvaluateExpression(mockJsonPathResolver.Object.JToken), + JsonPath = mockJsonPathResolver.Object.Path, + LineNumber = mockLineNumberResolver.Object.ResolveLineNumber(mockJsonPathResolver.Object.Path), + Expression = mockExpression, + ActualValue = mockJsonPathResolver.Object.JToken + }; + + // Act + string failureMessage = result.FailureMessage(); + + // Assert + Assert.AreEqual($"Value \"{actualValue ?? "null"}\" found at \"some.path\" is {(isNegative ? "" : "not")} equal to \"{expectedValue ?? "null"}\".", failureMessage); + } + + [DataTestMethod] + [DataRow(true, false, DisplayName = "JToken at path does not exist")] + [DataRow(false, true, DisplayName = "JToken at path exists")] + [DataRow(true, null, DisplayName = "JToken at path is null")] + public void FailureMessage_ValidJTokensForExistsOperator_FailureMessageIsReturnedAsExpected(object expectedValue, object actualValue) + { + // Arrange + var mockOperator = new Mock(); + mockOperator + .Setup(s => s.EvaluateExpression(It.IsAny())) + .Returns(false); + mockOperator.Object.SpecifiedValue = TestUtilities.ToJToken(expectedValue); + mockOperator.Object.FailureMessage = JsonRuleEngineConstants.ExistsFailureMessage; + + var mockJsonPathResolver = new Mock(); + mockJsonPathResolver + .Setup(s => s.JToken) + .Returns(TestUtilities.ToJToken(actualValue)); + mockJsonPathResolver + .Setup(s => s.Path) + .Returns("some.path"); + + var mockLineNumberResolver = new Mock(); + mockLineNumberResolver + .Setup(s => s.ResolveLineNumber(mockJsonPathResolver.Object.Path)) + .Returns(0); + + var mockExpression = new LeafExpression(mockLineNumberResolver.Object, mockOperator.Object, new ExpressionCommonProperties { Path = "some.path" }); + + var result = new JsonRuleResult() + { + Passed = mockOperator.Object.EvaluateExpression(mockJsonPathResolver.Object.JToken), + JsonPath = mockJsonPathResolver.Object.Path, + LineNumber = mockLineNumberResolver.Object.ResolveLineNumber(mockJsonPathResolver.Object.Path), + Expression = mockExpression, + ActualValue = mockJsonPathResolver.Object.JToken + }; + + // Act + string failureMessage = result.FailureMessage(); + + // Assert + Assert.AreEqual($"Value found at \"some.path\" exists: {actualValue ?? "null"}, expected: {expectedValue}.", failureMessage); + } + + [DataTestMethod] + [DataRow(true, false, DisplayName = "JToken at path does not have a value")] + [DataRow(false, true, DisplayName = "JToken at path has a value")] + [DataRow(true, null, DisplayName = "JToken at path is null")] + public void FailureMessage_ValidJTokensForHasValueOperator_FailureMessageIsReturnedAsExpected(object expectedValue, object actualValue) + { + // Arrange + var mockOperator = new Mock(); + mockOperator + .Setup(s => s.EvaluateExpression(It.IsAny())) + .Returns(false); + mockOperator.Object.SpecifiedValue = TestUtilities.ToJToken(expectedValue); + mockOperator.Object.FailureMessage = JsonRuleEngineConstants.HasValueFailureMessage; + + var mockJsonPathResolver = new Mock(); + mockJsonPathResolver + .Setup(s => s.JToken) + .Returns(TestUtilities.ToJToken(actualValue)); + mockJsonPathResolver + .Setup(s => s.Path) + .Returns("some.path"); + + var mockLineNumberResolver = new Mock(); + mockLineNumberResolver + .Setup(s => s.ResolveLineNumber(mockJsonPathResolver.Object.Path)) + .Returns(0); + + var mockExpression = new LeafExpression(mockLineNumberResolver.Object, mockOperator.Object, new ExpressionCommonProperties { Path = "some.path" }); + + var result = new JsonRuleResult() + { + Passed = mockOperator.Object.EvaluateExpression(mockJsonPathResolver.Object.JToken), + JsonPath = mockJsonPathResolver.Object.Path, + LineNumber = mockLineNumberResolver.Object.ResolveLineNumber(mockJsonPathResolver.Object.Path), + Expression = mockExpression, + ActualValue = mockJsonPathResolver.Object.JToken + }; + + // Act + string failureMessage = result.FailureMessage(); + + // Assert + Assert.AreEqual($"Value found at \"some.path\" has a value: {actualValue ?? "null"}, expected: {expectedValue}.", failureMessage); + } + + [DataTestMethod] + [DataRow("value", "doesNotContain", DisplayName = "JToken at path does not match regex pattern")] + [DataRow("value", null, DisplayName = "JToken at path is null")] + public void FailureMessage_ValidJTokensForRegexOperator_FailureMessageIsReturnedAsExpected(object expectedValue, object actualValue) + { + // Arrange + var mockOperator = new Mock(); + mockOperator + .Setup(s => s.EvaluateExpression(It.IsAny())) + .Returns(false); + mockOperator.Object.SpecifiedValue = TestUtilities.ToJToken(expectedValue); + mockOperator.Object.FailureMessage = JsonRuleEngineConstants.RegexFailureMessage; + + var mockJsonPathResolver = new Mock(); + mockJsonPathResolver + .Setup(s => s.JToken) + .Returns(TestUtilities.ToJToken(actualValue)); + mockJsonPathResolver + .Setup(s => s.Path) + .Returns("some.path"); + + var mockLineNumberResolver = new Mock(); + mockLineNumberResolver + .Setup(s => s.ResolveLineNumber(mockJsonPathResolver.Object.Path)) + .Returns(0); + + var mockExpression = new LeafExpression(mockLineNumberResolver.Object, mockOperator.Object, new ExpressionCommonProperties { Path = "some.path" }); + + var result = new JsonRuleResult() + { + Passed = mockOperator.Object.EvaluateExpression(mockJsonPathResolver.Object.JToken), + JsonPath = mockJsonPathResolver.Object.Path, + LineNumber = mockLineNumberResolver.Object.ResolveLineNumber(mockJsonPathResolver.Object.Path), + Expression = mockExpression, + ActualValue = mockJsonPathResolver.Object.JToken + }; + + // Act + string failureMessage = result.FailureMessage(); + + // Assert + Assert.AreEqual($"Value \"{actualValue ?? "null"}\" found at \"some.path\" does not match regex pattern: \"{expectedValue}\".", failureMessage); + } + + [DataTestMethod] + [DataRow("value", null, DisplayName = "JToken to match is a string")] + [DataRow(1, null, DisplayName = "JToken to match is an int")] + [DataRow(.1, null, DisplayName = "JToken to match is a float")] + public void FailureMessage_ValidJTokensForInOperator_FailureMessageIsReturnedAsExpected(object expectedValue, object actualValue) + { + // Arrange + var mockOperator = new Mock(); + mockOperator + .Setup(s => s.EvaluateExpression(It.IsAny())) + .Returns(false); + mockOperator.Object.SpecifiedValue = TestUtilities.ToJToken(expectedValue); + mockOperator.Object.FailureMessage = JsonRuleEngineConstants.InFailureMessage; + + var mockJsonPathResolver = new Mock(); + mockJsonPathResolver + .Setup(s => s.JToken) + .Returns(TestUtilities.ToJToken(actualValue)); + mockJsonPathResolver + .Setup(s => s.Path) + .Returns("some.path"); + + var mockLineNumberResolver = new Mock(); + mockLineNumberResolver + .Setup(s => s.ResolveLineNumber(mockJsonPathResolver.Object.Path)) + .Returns(0); + + var mockExpression = new LeafExpression(mockLineNumberResolver.Object, mockOperator.Object, new ExpressionCommonProperties { Path = "some.path" }); + + var result = new JsonRuleResult() + { + Passed = mockOperator.Object.EvaluateExpression(mockJsonPathResolver.Object.JToken), + JsonPath = mockJsonPathResolver.Object.Path, + LineNumber = mockLineNumberResolver.Object.ResolveLineNumber(mockJsonPathResolver.Object.Path), + Expression = mockExpression, + ActualValue = mockJsonPathResolver.Object.JToken + }; + + // Act + string failureMessage = result.FailureMessage(); + + // Assert + Assert.AreEqual($"Value \"{expectedValue}\" is not in the list at path \"some.path\".", failureMessage); + } + + [TestMethod] + public void FailureMessage_ValidJTokensForAllOf_FailureMessageIsReturnedAsExpected() + { + // Arrange + var mockOperator = new Mock(); + mockOperator + .Setup(s => s.EvaluateExpression(It.IsAny())) + .Returns(false); + mockOperator.Object.SpecifiedValue = "EXPECTED_VALUE"; + mockOperator.Object.FailureMessage = JsonRuleEngineConstants.InFailureMessage; + + var mockJsonPathResolver = new Mock(); + mockJsonPathResolver + .Setup(s => s.JToken) + .Returns("ACTUAL_VALUE"); + mockJsonPathResolver + .Setup(s => s.Path) + .Returns("some.path"); + + var mockLineNumberResolver = new Mock(); + mockLineNumberResolver + .Setup(s => s.ResolveLineNumber(mockJsonPathResolver.Object.Path)) + .Returns(0); + + var mockLeafExpression = new LeafExpression(mockLineNumberResolver.Object, mockOperator.Object, new ExpressionCommonProperties { Path = "some.path" }); + + var mockStructuredExpression = new AllOfExpression(new Expression[] { mockLeafExpression }, new ExpressionCommonProperties { Path = "some.path" }); + + var result = new JsonRuleResult() + { + Passed = mockOperator.Object.EvaluateExpression(mockJsonPathResolver.Object.JToken), + JsonPath = mockJsonPathResolver.Object.Path, + LineNumber = mockLineNumberResolver.Object.ResolveLineNumber(mockJsonPathResolver.Object.Path), + Expression = mockStructuredExpression, + ActualValue = mockJsonPathResolver.Object.JToken + }; + + // Act + string failureMessage = result.FailureMessage(); + + // Assert + Assert.AreEqual("One or more evaluations were false for the following json property: \"some.path\".", failureMessage); + } + + [TestMethod] + public void FailureMessage_ValidJTokensForAnyOf_FailureMessageIsReturnedAsExpected() + { + // Arrange + var mockOperator = new Mock(); + mockOperator + .Setup(s => s.EvaluateExpression(It.IsAny())) + .Returns(false); + mockOperator.Object.SpecifiedValue = "EXPECTED_VALUE"; + mockOperator.Object.FailureMessage = JsonRuleEngineConstants.InFailureMessage; + + var mockJsonPathResolver = new Mock(); + mockJsonPathResolver + .Setup(s => s.JToken) + .Returns("ACTUAL_VALUE"); + mockJsonPathResolver + .Setup(s => s.Path) + .Returns("some.path"); + + var mockLineNumberResolver = new Mock(); + mockLineNumberResolver + .Setup(s => s.ResolveLineNumber(mockJsonPathResolver.Object.Path)) + .Returns(0); + + var mockLeafExpression = new LeafExpression(mockLineNumberResolver.Object, mockOperator.Object, new ExpressionCommonProperties { Path = "some.path" }); + + var mockStructuredExpression = new AnyOfExpression(new Expression[] { mockLeafExpression }, new ExpressionCommonProperties { Path = "some.path" }); + + var result = new JsonRuleResult() + { + Passed = mockOperator.Object.EvaluateExpression(mockJsonPathResolver.Object.JToken), + JsonPath = mockJsonPathResolver.Object.Path, + LineNumber = mockLineNumberResolver.Object.ResolveLineNumber(mockJsonPathResolver.Object.Path), + Expression = mockStructuredExpression, + ActualValue = mockJsonPathResolver.Object.JToken + }; + + // Act + string failureMessage = result.FailureMessage(); + + // Assert + Assert.AreEqual("No evaluations evaluted to true for the following json property: \"some.path\".", failureMessage); + } + } +} diff --git a/src/Analyzer.JsonRuleEngine.UnitTests/LeafExpressionTests.cs b/src/Analyzer.JsonRuleEngine.UnitTests/LeafExpressionTests.cs index ba8859c8..2f2e740b 100644 --- a/src/Analyzer.JsonRuleEngine.UnitTests/LeafExpressionTests.cs +++ b/src/Analyzer.JsonRuleEngine.UnitTests/LeafExpressionTests.cs @@ -103,7 +103,7 @@ public void Evaluate_ValidScope_ReturnsResultsOfOperatorEvaluation(string resour mockResourcesResolved.Verify(s => s.ResolveResourceType(It.IsAny()), Times.Never); // The original mock is returned from both mocks when calling Resolve for a path, so the JToken should always come from it. - mockJsonPathResolver.Verify(s => s.JToken, Times.Once); + mockJsonPathResolver.Verify(s => s.JToken, Times.Exactly(2)); mockResourcesResolved.Verify(s => s.JToken, Times.Never); mockLeafExpressionOperator.Verify(o => o.EvaluateExpression(It.Is(token => token == jsonToEvaluate)), Times.Once); diff --git a/src/Analyzer.JsonRuleEngine/Expressions/AllOfExpression.cs b/src/Analyzer.JsonRuleEngine/Expressions/AllOfExpression.cs index 1df1b87e..7f9db5a5 100644 --- a/src/Analyzer.JsonRuleEngine/Expressions/AllOfExpression.cs +++ b/src/Analyzer.JsonRuleEngine/Expressions/AllOfExpression.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; using Microsoft.Azure.Templates.Analyzer.Types; namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Expressions @@ -26,6 +27,7 @@ public AllOfExpression(Expression[] expressions, ExpressionCommonProperties comm : base(commonProperties) { this.AllOf = expressions ?? throw new ArgumentNullException(nameof(expressions)); + this.FailureMessage = JsonRuleEngineConstants.AllOfFailureMessage; } /// diff --git a/src/Analyzer.JsonRuleEngine/Expressions/AnyOfExpression.cs b/src/Analyzer.JsonRuleEngine/Expressions/AnyOfExpression.cs index a498907f..ad81d440 100644 --- a/src/Analyzer.JsonRuleEngine/Expressions/AnyOfExpression.cs +++ b/src/Analyzer.JsonRuleEngine/Expressions/AnyOfExpression.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; using Microsoft.Azure.Templates.Analyzer.Types; namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Expressions @@ -26,6 +27,7 @@ public AnyOfExpression(Expression[] expressions, ExpressionCommonProperties comm : base(commonProperties) { this.AnyOf = expressions ?? throw new ArgumentNullException(nameof(expressions)); + this.FailureMessage = JsonRuleEngineConstants.AnyOfFailureMessage; } /// diff --git a/src/Analyzer.JsonRuleEngine/Expressions/Expression.cs b/src/Analyzer.JsonRuleEngine/Expressions/Expression.cs index 4378cbe7..c2cd4e47 100644 --- a/src/Analyzer.JsonRuleEngine/Expressions/Expression.cs +++ b/src/Analyzer.JsonRuleEngine/Expressions/Expression.cs @@ -28,6 +28,11 @@ internal abstract class Expression /// public Expression Where { get; private set; } + /// + /// Gets the messsage which explains why the evaluation failed. + /// + public string FailureMessage { get; internal set; } + /// /// Initialization for the base Expression. /// @@ -37,7 +42,6 @@ internal Expression(ExpressionCommonProperties commonProperties) (this.ResourceType, this.Path, this.Where) = (commonProperties.ResourceType, commonProperties.Path, commonProperties.Where); } - /// /// Executes this against a template. /// diff --git a/src/Analyzer.JsonRuleEngine/Expressions/LeafExpression.cs b/src/Analyzer.JsonRuleEngine/Expressions/LeafExpression.cs index 34d77e0b..12164ade 100644 --- a/src/Analyzer.JsonRuleEngine/Expressions/LeafExpression.cs +++ b/src/Analyzer.JsonRuleEngine/Expressions/LeafExpression.cs @@ -28,6 +28,7 @@ public LeafExpression(ILineNumberResolver jsonLineNumberResolver, LeafExpression { this.jsonLineNumberResolver = jsonLineNumberResolver ?? throw new ArgumentNullException(nameof(jsonLineNumberResolver)); this.Operator = @operator ?? throw new ArgumentNullException(nameof(@operator)); + this.FailureMessage = Operator.FailureMessage; if (commonProperties.Path == null) throw new ArgumentException("Path property must not be null.", nameof(commonProperties)); } @@ -51,7 +52,8 @@ public override JsonRuleEvaluation Evaluate(IJsonPathResolver jsonScope) Passed = Operator.EvaluateExpression(scope.JToken), JsonPath = scope.Path, LineNumber = this.jsonLineNumberResolver.ResolveLineNumber(scope.Path), - Expression = this + Expression = this, + ActualValue = scope.JToken }; return result; diff --git a/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs b/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs new file mode 100644 index 00000000..c0c6db66 --- /dev/null +++ b/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants +{ + /// + /// Defines all constants used in JsonEngine + /// + internal class JsonRuleEngineConstants + { + internal const string ActualValuePlaceholder = "{actualValue}"; + internal const string PathPlaceholder = "{path}"; + internal const string ExpectedValuePlaceholder = "{expectedValue}"; + internal const string NegationPlaceholder = "{negation}"; + + internal static string EqualsFailureMessage = $"Value \"{ActualValuePlaceholder}\" found at \"{PathPlaceholder}\" is {NegationPlaceholder} equal to \"{ExpectedValuePlaceholder}\"."; + internal static string ExistsFailureMessage = $"Value found at \"{PathPlaceholder}\" exists: {ActualValuePlaceholder}, expected: {ExpectedValuePlaceholder}."; + internal static string HasValueFailureMessage = $"Value found at \"{PathPlaceholder}\" has a value: {ActualValuePlaceholder}, expected: {ExpectedValuePlaceholder}."; + internal static string RegexFailureMessage = $"Value \"{ActualValuePlaceholder}\" found at \"{PathPlaceholder}\" does not match regex pattern: \"{ExpectedValuePlaceholder}\"."; + internal static string InFailureMessage = $"Value \"{ExpectedValuePlaceholder}\" is not in the list at path \"{PathPlaceholder}\"."; + + internal static string AnyOfFailureMessage = $"No evaluations evaluted to true for the following json property: \"{PathPlaceholder}\"."; + internal static string AllOfFailureMessage = $"One or more evaluations were false for the following json property: \"{PathPlaceholder}\"."; + + } +} diff --git a/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs b/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs index 381edcf1..5f0c6b5b 100644 --- a/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs +++ b/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Expressions; using Microsoft.Azure.Templates.Analyzer.Types; +using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine { @@ -27,8 +29,32 @@ internal class JsonRuleResult : IResult internal string JsonPath { get; set; } /// - /// Gets the expression associated with this result + /// Gets the expression associated with this result. /// internal Expression Expression { get; set; } + + /// + /// Gets the actual value present at the specified path. + /// + internal JToken ActualValue { get; set; } + + /// + /// Gets the messsage which explains why the evaluation failed. + /// + public string FailureMessage() + { + string failureMessage = Expression.FailureMessage; + + if (Expression is LeafExpression) + { + string expectedValue = ((Expression as LeafExpression).Operator.SpecifiedValue == null || (Expression as LeafExpression).Operator.SpecifiedValue.ToObject() == null) ? "null" : (Expression as LeafExpression).Operator.SpecifiedValue.ToString(); + failureMessage = failureMessage.Replace(JsonRuleEngineConstants.ExpectedValuePlaceholder, expectedValue); + failureMessage = failureMessage.Replace(JsonRuleEngineConstants.NegationPlaceholder, (Expression as LeafExpression).Operator.IsNegative ? "" : "not"); + } + + string actualValue = (ActualValue == null || ActualValue.ToObject() == null) ? "null" : ActualValue.ToString(); + failureMessage = failureMessage.Replace(JsonRuleEngineConstants.ActualValuePlaceholder, actualValue); + return failureMessage.Replace(JsonRuleEngineConstants.PathPlaceholder, JsonPath); + } } } diff --git a/src/Analyzer.JsonRuleEngine/Operators/EqualsOperator.cs b/src/Analyzer.JsonRuleEngine/Operators/EqualsOperator.cs index 0996959f..2d5eb31e 100644 --- a/src/Analyzer.JsonRuleEngine/Operators/EqualsOperator.cs +++ b/src/Analyzer.JsonRuleEngine/Operators/EqualsOperator.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Operators @@ -23,6 +24,7 @@ public EqualsOperator(JToken specifiedValue, bool isNegative) { this.SpecifiedValue = specifiedValue ?? throw new ArgumentNullException(nameof(specifiedValue)); this.IsNegative = isNegative; + this.FailureMessage = $"{this.Name}: {JsonRuleEngineConstants.EqualsFailureMessage}"; } /// diff --git a/src/Analyzer.JsonRuleEngine/Operators/ExistsOperator.cs b/src/Analyzer.JsonRuleEngine/Operators/ExistsOperator.cs index fce68e69..51109400 100644 --- a/src/Analyzer.JsonRuleEngine/Operators/ExistsOperator.cs +++ b/src/Analyzer.JsonRuleEngine/Operators/ExistsOperator.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Operators @@ -30,6 +31,7 @@ public ExistsOperator(bool specifiedValue, bool isNegative) (this.SpecifiedValue, this.IsNegative) = (specifiedValue, isNegative); this.EffectiveValue = specifiedValue; + this.FailureMessage = $"{this.Name}: {JsonRuleEngineConstants.ExistsFailureMessage}"; } /// diff --git a/src/Analyzer.JsonRuleEngine/Operators/HasValueOperator.cs b/src/Analyzer.JsonRuleEngine/Operators/HasValueOperator.cs index cf3c462b..e2da16b7 100644 --- a/src/Analyzer.JsonRuleEngine/Operators/HasValueOperator.cs +++ b/src/Analyzer.JsonRuleEngine/Operators/HasValueOperator.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Operators @@ -30,6 +31,7 @@ public HasValueOperator(bool specifiedValue, bool isNegative) (this.SpecifiedValue, this.IsNegative) = (specifiedValue, isNegative); this.EffectiveValue = specifiedValue; + this.FailureMessage = $"{this.Name}: {JsonRuleEngineConstants.HasValueFailureMessage}"; } /// diff --git a/src/Analyzer.JsonRuleEngine/Operators/InOperator.cs b/src/Analyzer.JsonRuleEngine/Operators/InOperator.cs index 36460c0f..275314fc 100644 --- a/src/Analyzer.JsonRuleEngine/Operators/InOperator.cs +++ b/src/Analyzer.JsonRuleEngine/Operators/InOperator.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Operators @@ -23,6 +24,7 @@ public InOperator(JArray specifiedValue) { this.SpecifiedValue = specifiedValue; this.IsNegative = false; + this.FailureMessage = $"{this.Name}: {JsonRuleEngineConstants.InFailureMessage}"; } /// diff --git a/src/Analyzer.JsonRuleEngine/Operators/LeafExpressionOperator.cs b/src/Analyzer.JsonRuleEngine/Operators/LeafExpressionOperator.cs index 6b8eb888..5f33f8e3 100644 --- a/src/Analyzer.JsonRuleEngine/Operators/LeafExpressionOperator.cs +++ b/src/Analyzer.JsonRuleEngine/Operators/LeafExpressionOperator.cs @@ -25,6 +25,11 @@ internal abstract class LeafExpressionOperator /// public JToken SpecifiedValue { get; set; } + /// + /// Gets the failure message when the operator does not pass + /// + public string FailureMessage { get; set; } + /// /// Evaluates the specified JToken using the defined operation of this Operator. /// diff --git a/src/Analyzer.JsonRuleEngine/Operators/RegexOperator.cs b/src/Analyzer.JsonRuleEngine/Operators/RegexOperator.cs index bd37fa9f..16919069 100644 --- a/src/Analyzer.JsonRuleEngine/Operators/RegexOperator.cs +++ b/src/Analyzer.JsonRuleEngine/Operators/RegexOperator.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Text.RegularExpressions; +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Operators @@ -36,6 +37,8 @@ public RegexOperator(string regexPattern) { throw new System.ArgumentException($"Regex pattern is not valid.", e); } + + this.FailureMessage = $"{this.Name}: {JsonRuleEngineConstants.RegexFailureMessage}"; } /// diff --git a/src/Analyzer.Types/IResult.cs b/src/Analyzer.Types/IResult.cs index f81db3b9..a3e3096e 100644 --- a/src/Analyzer.Types/IResult.cs +++ b/src/Analyzer.Types/IResult.cs @@ -17,5 +17,10 @@ public interface IResult /// Gets the line number of the file where the rule was evaluated. /// public int LineNumber { get; } + + /// + /// Gets the messsage which explains why the evaluation failed. + /// + public string FailureMessage(); } }