Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Rule validation #2

Merged
merged 9 commits into from
Apr 11, 2020
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -554,3 +554,6 @@ typings/


# End of https://www.gitignore.io/api/csharp,aspnetcore,visualstudio

# Specific ones added
coverage-outputs/**
8 changes: 3 additions & 5 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,15 @@ services: mongodb
install:
- sh: dotnet tool install --global dotnet-sonarscanner
before_build:
- sh: dotnet sonarscanner begin /k:pikenikes_rules-framework /v:$APPVEYOR_BUILD_VERSION /o:pikenikes-github /d:sonar.host.url=https://sonarcloud.io /d:sonar.login=8f9e9412fb5ebcf5ea6f9a6b285b288e4645f866 /d:sonar.coverage.exclusions="tests/*Tests/**" /d:sonar.cs.opencover.reportsPaths="*.opencover.xml"
- sh: dotnet sonarscanner begin /k:pikenikes_rules-framework /v:$APPVEYOR_BUILD_VERSION /o:pikenikes-github /d:sonar.host.url=https://sonarcloud.io /d:sonar.login=8f9e9412fb5ebcf5ea6f9a6b285b288e4645f866 /d:sonar.coverage.exclusions="tests/*Tests/**" /d:sonar.cs.opencover.reportsPaths="coverage-outputs/coverage.opencover.xml" /d:sonar.branch.name=$APPVEYOR_REPO_BRANCH
build_script:
- sh: dotnet restore rules-framework.sln --no-cache
- sh: dotnet build rules-framework.sln --no-restore
- sh: dotnet pack src/Rules.Framework/Rules.Framework.csproj --no-build --include-symbols
- sh: dotnet pack src/Rules.Framework.Providers.MongoDb/Rules.Framework.Providers.MongoDb.csproj --no-build --include-symbols
test_script:
- sh: dotnet test tests/Rules.Framework.Tests --no-build /p:CollectCoverage=true /p:CoverletOutputFormat="opencover" /p:CoverletOutput=../../rules-framework-tests-coverage
- sh: dotnet test tests/Rules.Framework.Providers.MongoDb.Tests --no-build /p:CollectCoverage=true /p:CoverletOutputFormat="opencover" /p:CoverletOutput=../../rules-framework-providers-mongodb-tests-coverage
- sh: dotnet test tests/Rules.Framework.IntegrationTests --no-build /p:CollectCoverage=true /p:CoverletOutputFormat="opencover" /p:CoverletOutput=../../rules-framework-integration-tests-coverage
- sh: dotnet test tests/Rules.Framework.Providers.MongoDb.IntegrationTests --no-build /p:CollectCoverage=true /p:CoverletOutputFormat="opencover" /p:CoverletOutput=../../rules-framework-providers-mongodb-integration-tests-coverage
- sh: dotnet test rules-framework.sln --no-build /p:CollectCoverage=true /p:CoverletOutputFormat="json%2copencover" /p:CoverletOutput=../../coverage-outputs/ /p:MergeWith=../../coverage-outputs/coverage.json -m:1
- sh: rm coverage-outputs/coverage.json
after_test:
- sh: dotnet sonarscanner end /d:sonar.login=8f9e9412fb5ebcf5ea6f9a6b285b288e4645f866
artifacts:
Expand Down
17 changes: 17 additions & 0 deletions run-tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
$globalTools = dotnet tool list -g
$reportGeneratorTool = $globalTools | Select-String -Pattern "dotnet-reportgenerator-globaltool"

If (!$reportGeneratorTool) {
dotnet tool install --global dotnet-reportgenerator-globaltool
}

$reportTimestamp = [DateTime]::UtcNow.ToString("yyyyMMdd_hhmmss")
$currentDir = (Get-Location).Path
$coverageDir = "$currentDir\\coverage-outputs\\$reportTimestamp\\"

dotnet test .\rules-framework.sln /p:CollectCoverage=true /p:CoverletOutputFormat="opencover%2cjson" /p:CoverletOutput=$coverageDir /p:MergeWith="$coverageDir/coverage.json" -m:1

$coverageFile = "$($coverageDir)coverage.opencover.xml"
$coverageReportFolder = "$($currentDir)\\coverage-outputs\\report\\"
reportgenerator -reports:$coverageFile -targetdir:$coverageReportFolder
# start "$($coverageReportFolder)index.htm"
13 changes: 12 additions & 1 deletion src/Rules.Framework/Builder/RuleBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
namespace Rules.Framework.Builder
{
using System;
using System.Linq;
using FluentValidation.Results;
using Rules.Framework.Builder.Validation;
using Rules.Framework.Core;

internal class RuleBuilder<TContentType, TConditionType> : IRuleBuilder<TContentType, TConditionType>
Expand All @@ -24,7 +27,15 @@ public RuleBuilderResult<TContentType, TConditionType> Build()
RootCondition = this.rootCondition
};

return RuleBuilderResult.Success(rule);
RuleValidator<TContentType, TConditionType> ruleValidator = new RuleValidator<TContentType, TConditionType>();
ValidationResult validationResult = ruleValidator.Validate(rule);

if (validationResult.IsValid)
{
return RuleBuilderResult.Success(rule);
}

return RuleBuilderResult.Failure<TContentType, TConditionType>(validationResult.Errors.Select(ve => ve.ErrorMessage).ToList());
}

public IRuleBuilder<TContentType, TConditionType> WithCondition(IConditionNode<TConditionType> condition)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace Rules.Framework.Builder.Validation
{
using FluentValidation;
using Rules.Framework.Core;
using Rules.Framework.Core.ConditionNodes;

internal class ComposedConditionNodeValidator<TConditionType> : AbstractValidator<ComposedConditionNode<TConditionType>>
{
private readonly ValueConditionNodeValidator<bool, TConditionType> booleanConditionNodeValidator;
private readonly ValueConditionNodeValidator<decimal, TConditionType> decimalConditionNodeValidator;
private readonly ValueConditionNodeValidator<int, TConditionType> integerConditionNodeValidator;
private readonly ValueConditionNodeValidator<string, TConditionType> stringConditionNodeValidator;

public ComposedConditionNodeValidator()
{
this.integerConditionNodeValidator = new ValueConditionNodeValidator<int, TConditionType>();
this.decimalConditionNodeValidator = new ValueConditionNodeValidator<decimal, TConditionType>();
this.stringConditionNodeValidator = new ValueConditionNodeValidator<string, TConditionType>();
this.booleanConditionNodeValidator = new ValueConditionNodeValidator<bool, TConditionType>();

this.RuleFor(c => c.LogicalOperator).IsContainedOn(LogicalOperators.And, LogicalOperators.Or);
this.RuleForEach(c => c.ChildConditionNodes).Custom((cn, cc) => cn.PerformValidation(new ConditionNodeValidationArgs<TConditionType>
{
BooleanConditionNodeValidator = this.booleanConditionNodeValidator,
ComposedConditionNodeValidator = this,
CustomContext = cc,
DecimalConditionNodeValidator = this.decimalConditionNodeValidator,
IntegerConditionNodeValidator = this.integerConditionNodeValidator,
StringConditionNodeValidator = this.stringConditionNodeValidator
}));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Rules.Framework.Builder.Validation
{
using FluentValidation.Validators;

internal class ConditionNodeValidationArgs<TConditionType>
{
public ValueConditionNodeValidator<bool, TConditionType> BooleanConditionNodeValidator { get; set; }
public ComposedConditionNodeValidator<TConditionType> ComposedConditionNodeValidator { get; set; }
public CustomContext CustomContext { get; set; }
public ValueConditionNodeValidator<decimal, TConditionType> DecimalConditionNodeValidator { get; set; }
public ValueConditionNodeValidator<int, TConditionType> IntegerConditionNodeValidator { get; set; }
public ValueConditionNodeValidator<string, TConditionType> StringConditionNodeValidator { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
namespace Rules.Framework.Builder.Validation
{
using FluentValidation.Results;
using Rules.Framework.Core;
using Rules.Framework.Core.ConditionNodes;

internal static class ConditionNodeValidationExtensions
{
public static void PerformValidation<TConditionType>(this IConditionNode<TConditionType> conditionNode, ConditionNodeValidationArgs<TConditionType> conditionNodeValidationArgs)
{
ValidationResult validationResult;
switch (conditionNode)
{
case ComposedConditionNode<TConditionType> composedConditionNode:
validationResult = conditionNodeValidationArgs.ComposedConditionNodeValidator.Validate(composedConditionNode);
break;

case IntegerConditionNode<TConditionType> integerConditionNode:
validationResult = conditionNodeValidationArgs.IntegerConditionNodeValidator.Validate(integerConditionNode);
break;

case DecimalConditionNode<TConditionType> decimalConditionNode:
validationResult = conditionNodeValidationArgs.DecimalConditionNodeValidator.Validate(decimalConditionNode);
break;

case StringConditionNode<TConditionType> stringConditionNode:
validationResult = conditionNodeValidationArgs.StringConditionNodeValidator.Validate(stringConditionNode);
break;

case BooleanConditionNode<TConditionType> booleanConditionNode:
validationResult = conditionNodeValidationArgs.BooleanConditionNodeValidator.Validate(booleanConditionNode);
break;

default:
return;
}

if (!validationResult.IsValid)
{
foreach (ValidationFailure validationFailure in validationResult.Errors)
{
conditionNodeValidationArgs.CustomContext.AddFailure(validationFailure);
}
}
}
}
}
38 changes: 38 additions & 0 deletions src/Rules.Framework/Builder/Validation/RuleValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace Rules.Framework.Builder.Validation
{
using FluentValidation;
using Rules.Framework.Core;

internal class RuleValidator<TContentType, TConditionType> : AbstractValidator<Rule<TContentType, TConditionType>>
{
private readonly ValueConditionNodeValidator<bool, TConditionType> booleanConditionNodeValidator;
private readonly ComposedConditionNodeValidator<TConditionType> composedConditionNodeValidator;
private readonly ValueConditionNodeValidator<decimal, TConditionType> decimalConditionNodeValidator;
private readonly ValueConditionNodeValidator<int, TConditionType> integerConditionNodeValidator;
private readonly ValueConditionNodeValidator<string, TConditionType> stringConditionNodeValidator;

public RuleValidator()
{
this.composedConditionNodeValidator = new ComposedConditionNodeValidator<TConditionType>();
this.integerConditionNodeValidator = new ValueConditionNodeValidator<int, TConditionType>();
this.decimalConditionNodeValidator = new ValueConditionNodeValidator<decimal, TConditionType>();
this.stringConditionNodeValidator = new ValueConditionNodeValidator<string, TConditionType>();
this.booleanConditionNodeValidator = new ValueConditionNodeValidator<bool, TConditionType>();

this.RuleFor(r => r.ContentContainer).NotNull();
this.RuleFor(r => r.DateBegin).NotEmpty();
this.RuleFor(r => r.DateEnd).GreaterThanOrEqualTo(r => r.DateBegin).When(r => r.DateEnd != null);
this.RuleFor(r => r.Name).NotNull().NotEmpty();
this.RuleFor(r => r.Priority).GreaterThan(0);
this.RuleFor(r => r.RootCondition).Custom((cn, cc) => cn.PerformValidation(new ConditionNodeValidationArgs<TConditionType>
{
BooleanConditionNodeValidator = this.booleanConditionNodeValidator,
ComposedConditionNodeValidator = this.composedConditionNodeValidator,
CustomContext = cc,
DecimalConditionNodeValidator = this.decimalConditionNodeValidator,
IntegerConditionNodeValidator = this.integerConditionNodeValidator,
StringConditionNodeValidator = this.stringConditionNodeValidator
}));
}
}
}
19 changes: 19 additions & 0 deletions src/Rules.Framework/Builder/Validation/ValidationExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation;

namespace Rules.Framework.Builder.Validation
{
internal static class ValidationExtensions
{
public static IRuleBuilderOptions<T, TProperty> IsContainedOn<T, TProperty>(this IRuleBuilderInitial<T, TProperty> ruleBuilderInitial, IEnumerable<TProperty> values)
{
return ruleBuilderInitial.Must((p) => values.Contains(p));
}

public static IRuleBuilderOptions<T, TProperty> IsContainedOn<T, TProperty>(this IRuleBuilderInitial<T, TProperty> ruleBuilderInitial, params TProperty[] values)
{
return ruleBuilderInitial.IsContainedOn((IEnumerable<TProperty>)values);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Rules.Framework.Builder.Validation
{
using System;
using FluentValidation;
using Rules.Framework.Core;
using Rules.Framework.Core.ConditionNodes;

internal class ValueConditionNodeValidator<T, TConditionType> : AbstractValidator<ValueConditionNodeTemplate<T, TConditionType>>
where T : IComparable<T>
{
public ValueConditionNodeValidator()
{
this.RuleFor(c => c.ConditionType).NotEmpty();
this.RuleFor(c => c.ConditionType).IsInEnum().When(c => c.ConditionType is { } && c.ConditionType.GetType().IsEnum);
this.RuleFor(c => c.DataType).IsInEnum();
this.RuleFor(c => c.DataType).Equal(DataTypes.Integer).When(c => c.Operand is int);
this.RuleFor(c => c.DataType).Equal(DataTypes.String).When(c => c.Operand is string);
this.RuleFor(c => c.DataType).Equal(DataTypes.Decimal).When(c => c.Operand is decimal);
this.RuleFor(c => c.DataType).Equal(DataTypes.Boolean).When(c => c.Operand is bool);
this.RuleFor(c => c.Operator).IsInEnum();
this.RuleFor(c => c.Operator).IsContainedOn(Operators.Equal, Operators.NotEqual).When(c => c.DataType == DataTypes.String);
this.RuleFor(c => c.Operator).IsContainedOn(Operators.Equal, Operators.NotEqual).When(c => c.DataType == DataTypes.Boolean);
}
}
}
1 change: 1 addition & 0 deletions src/Rules.Framework/Rules.Framework.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
5 changes: 5 additions & 0 deletions tests/Rules.Framework.IntegrationTests/AssemblyMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: ExcludeFromCodeCoverage]
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<LangVersion>8.0</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.msbuild" Version="2.8.0">
<PackageReference Include="coverlet.collector" Version="1.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="5.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.0.0" />
<PackageReference Include="MSTest.TestFramework" Version="2.0.0" />
<PackageReference Include="coverlet.msbuild" Version="2.8.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.0.0.9566">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ namespace Rules.Framework.IntegrationTests.Tests.Scenario1
using System;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Rules.Framework.Core;
using Xunit;

[TestClass]
public class BodyMassIndexTests
{
[TestMethod]
[Fact]
public async Task BodyMassIndex_NoConditions_ReturnsDefaultFormula()
{
// Arrange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ namespace Rules.Framework.IntegrationTests.Tests.Scenario2
using System;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Rules.Framework.Core;
using Xunit;

[TestClass]
public class CarInsuranceAdvisorTests
{
[TestMethod]
[Fact]
public async Task BodyMassIndex_NoConditions_ReturnsDefaultFormula()
{
// Arrange
Expand Down
Loading