Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/Analyzer.Cli/CommandLineParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
334 changes: 334 additions & 0 deletions src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public void Evaluate_ValidScope_ReturnsResultsOfOperatorEvaluation(string resour
mockResourcesResolved.Verify(s => s.ResolveResourceType(It.IsAny<string>()), 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<JToken>(token => token == jsonToEvaluate)), Times.Once);
Expand Down
2 changes: 2 additions & 0 deletions src/Analyzer.JsonRuleEngine/Expressions/AllOfExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,6 +27,7 @@ public AllOfExpression(Expression[] expressions, ExpressionCommonProperties comm
: base(commonProperties)
{
this.AllOf = expressions ?? throw new ArgumentNullException(nameof(expressions));
this.FailureMessage = JsonRuleEngineConstants.AllOfFailureMessage;
}

/// <summary>
Expand Down
2 changes: 2 additions & 0 deletions src/Analyzer.JsonRuleEngine/Expressions/AnyOfExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,6 +27,7 @@ public AnyOfExpression(Expression[] expressions, ExpressionCommonProperties comm
: base(commonProperties)
{
this.AnyOf = expressions ?? throw new ArgumentNullException(nameof(expressions));
this.FailureMessage = JsonRuleEngineConstants.AnyOfFailureMessage;
}

/// <summary>
Expand Down
6 changes: 5 additions & 1 deletion src/Analyzer.JsonRuleEngine/Expressions/Expression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ internal abstract class Expression
/// </summary>
public Expression Where { get; private set; }

/// <summary>
/// Gets the messsage which explains why the evaluation failed.
/// </summary>
public string FailureMessage { get; internal set; }

/// <summary>
/// Initialization for the base Expression.
/// </summary>
Expand All @@ -37,7 +42,6 @@ internal Expression(ExpressionCommonProperties commonProperties)
(this.ResourceType, this.Path, this.Where) = (commonProperties.ResourceType, commonProperties.Path, commonProperties.Where);
}


/// <summary>
/// Executes this <see cref="Expression"/> against a template.
/// </summary>
Expand Down
4 changes: 3 additions & 1 deletion src/Analyzer.JsonRuleEngine/Expressions/LeafExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand All @@ -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;
Expand Down
26 changes: 26 additions & 0 deletions src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants
{
/// <summary>
/// Defines all constants used in JsonEngine
/// </summary>
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}\".";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better if each of these was implemented as a method directly in the corresponding Operator, and the placeholders would be filled in using parameters to the method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, maybe we could update how EvaluateExpression() works, and either create the string and have it ready in another property after evaluation, or return the string as part of the function call.


}
}
28 changes: 27 additions & 1 deletion src/Analyzer.JsonRuleEngine/JsonRuleResult.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -27,8 +29,32 @@ internal class JsonRuleResult : IResult
internal string JsonPath { get; set; }

/// <summary>
/// Gets the expression associated with this result
/// Gets the expression associated with this result.
/// </summary>
internal Expression Expression { get; set; }

/// <summary>
/// Gets the actual value present at the specified path.
/// </summary>
internal JToken ActualValue { get; set; }

/// <summary>
/// Gets the messsage which explains why the evaluation failed.
/// </summary>
public string FailureMessage()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As much as possible, I think all this logic should be in the Operators and Expressions. I think it would be easier to understand and write if the failure messages were built right where the evaluations happen.

{
string failureMessage = Expression.FailureMessage;

if (Expression is LeafExpression)
{
string expectedValue = ((Expression as LeafExpression).Operator.SpecifiedValue == null || (Expression as LeafExpression).Operator.SpecifiedValue.ToObject<object>() == 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<object>() == null) ? "null" : ActualValue.ToString();
failureMessage = failureMessage.Replace(JsonRuleEngineConstants.ActualValuePlaceholder, actualValue);
return failureMessage.Replace(JsonRuleEngineConstants.PathPlaceholder, JsonPath);
}
}
}
2 changes: 2 additions & 0 deletions src/Analyzer.JsonRuleEngine/Operators/EqualsOperator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}";
}

/// <summary>
Expand Down
2 changes: 2 additions & 0 deletions src/Analyzer.JsonRuleEngine/Operators/ExistsOperator.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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}";
}

/// <summary>
Expand Down
2 changes: 2 additions & 0 deletions src/Analyzer.JsonRuleEngine/Operators/HasValueOperator.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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}";
}

/// <summary>
Expand Down
2 changes: 2 additions & 0 deletions src/Analyzer.JsonRuleEngine/Operators/InOperator.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -23,6 +24,7 @@ public InOperator(JArray specifiedValue)
{
this.SpecifiedValue = specifiedValue;
this.IsNegative = false;
this.FailureMessage = $"{this.Name}: {JsonRuleEngineConstants.InFailureMessage}";
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ internal abstract class LeafExpressionOperator
/// </summary>
public JToken SpecifiedValue { get; set; }

/// <summary>
/// Gets the failure message when the operator does not pass
/// </summary>
public string FailureMessage { get; set; }

/// <summary>
/// Evaluates the specified JToken using the defined operation of this <c>Operator</c>.
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions src/Analyzer.JsonRuleEngine/Operators/RegexOperator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -36,6 +37,8 @@ public RegexOperator(string regexPattern)
{
throw new System.ArgumentException($"Regex pattern is not valid.", e);
}

this.FailureMessage = $"{this.Name}: {JsonRuleEngineConstants.RegexFailureMessage}";
}

/// <summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Analyzer.Types/IResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,10 @@ public interface IResult
/// Gets the line number of the file where the rule was evaluated.
/// </summary>
public int LineNumber { get; }

/// <summary>
/// Gets the messsage which explains why the evaluation failed.
/// </summary>
public string FailureMessage();
}
}