Skip to content

Commit

Permalink
add RuleBuilder to ResultType.Validation package
Browse files Browse the repository at this point in the history
add IsSuccess extension method to ResultType
add IsFailure extension method to ResultType
  • Loading branch information
MNie committed Jan 23, 2020
1 parent f82d831 commit eda3f44
Show file tree
Hide file tree
Showing 12 changed files with 414 additions and 15 deletions.
2 changes: 1 addition & 1 deletion src/ResultType.Validation/ResultType.Validation.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>2.0.0</Version>
<Version>2.0.3</Version>
<Authors>Michał Niegrzybowski</Authors>
<Company>Michał Niegrzybowski - MNie</Company>
<PackageId>MNie.ResultType.Validation</PackageId>
Expand Down
8 changes: 6 additions & 2 deletions src/ResultType.Validation/Rule/IRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
{
using Results;

public interface IRule
public interface IRule : IRule<Unit>
{
IResult<Unit> Apply();
}

public interface IRule<TValidate>
{
IResult<TValidate> Apply();
}
}
1 change: 0 additions & 1 deletion src/ResultType.Validation/Rule/Rule.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
namespace ResultType.Validation.Rule
{
using System;

using Factories;
using Results;

Expand Down
148 changes: 148 additions & 0 deletions src/ResultType.Validation/Rule/RuleBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
namespace ResultType.Validation.Rule
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Extensions;
using Factories;
using Results;
using ResultType.Operations;

public static class RuleBuilder
{
public static RuleBuilder<TType> Build<TType>(Func<TType> func)
{
try
{
return new RuleBuilder<TType>(func().ToSuccess());
}
catch (Exception e)
{
return new RuleBuilder<TType>(ResultFactory.CreateFailure<TType>(new BuildError($"Build of {typeof(TType).Name} failed with {e.Message}")));
}
}
}

public class RuleBuilder<TType>
{
private class Predicate
{
public readonly Func<bool> ToCheck;
public readonly MemberInfo For;
public readonly string CustomMessage = "Validation is incorrect";

public Predicate(MemberInfo @for, Func<bool> toCheck)
{
For = @for;
ToCheck = toCheck;
}

public Predicate(MemberInfo @for, Func<bool> toCheck, string customMessage)
: this(@for, toCheck) =>
CustomMessage = customMessage;
}

private readonly IResult<TType> _underInvestigation;
private readonly IList<Predicate> _predicates = new List<Predicate>();

internal RuleBuilder(IResult<TType> underInvestigation) => _underInvestigation = underInvestigation;

private static MemberInfo FindProperty(LambdaExpression lambdaExpression)
{
Expression expressionToCheck = lambdaExpression;

var done = false;

while (!done)
{
switch (expressionToCheck.NodeType)
{
case ExpressionType.Convert:
expressionToCheck = ((UnaryExpression)expressionToCheck).Operand;
break;
case ExpressionType.Lambda:
expressionToCheck = ((LambdaExpression)expressionToCheck).Body;
break;
case ExpressionType.MemberAccess:
var memberExpression = ((MemberExpression)expressionToCheck);

if (memberExpression.Expression.NodeType != ExpressionType.Parameter &&
memberExpression.Expression.NodeType != ExpressionType.Convert)
{
throw new ArgumentException(
$"Expression '{lambdaExpression}' must resolve to top-level member and not any child object's fields/properties.",
nameof(lambdaExpression));
}

var member = memberExpression.Member;

return member;
default:
done = true;
break;
}
}
throw new Exception("Invalid state.");
}

public RuleBuilder<TType> For<TMember>(Expression<Func<TType, TMember>> func, Predicate<TMember> predicate)
{
var memberInfo = FindProperty(func);
_predicates.Add(
new Predicate(
memberInfo,
() =>
_underInvestigation switch
{
Success<TType> s => predicate(GetValue<TMember>(s.Payload, memberInfo)),
_ => true
}
)
);
return this;
}

public RuleBuilder<TType> For<TMember>(Expression<Func<TType, TMember>> func, Predicate<TMember> predicate, string customMessage)
{
var memberInfo = FindProperty(func);
_predicates.Add(
new Predicate(
memberInfo,
() =>
_underInvestigation switch
{
Success<TType> s => predicate(GetValue<TMember>(s.Payload, memberInfo)),
_ => true
},
customMessage)
);
return this;
}

private TMember GetValue<TMember>(TType investigated, MemberInfo info)
{
return info switch
{
FieldInfo fi => (TMember) fi.GetValue(investigated),
PropertyInfo pi => (TMember) pi.GetValue(investigated),
_ => throw new Exception("not supported type")
};
}

public IResult<TType> Apply() =>
_underInvestigation
.Bind(investigated =>
{
var result = _predicates
.Select(y => new {fieldName = y.For.Name, value = y.ToCheck(), msg = y.CustomMessage})
.Where(y => !y.value)
.ToList();
return result.Any()
? ResultFactory.CreateFailure<TType>(new ValidationError(string.Join($"{Environment.NewLine}and ",
result.Select(y => $"{y.fieldName} should be {y.msg}."))))
: investigated.ToSuccess();
});
}
}
18 changes: 18 additions & 0 deletions src/ResultType.Validation/Rule/ValidationError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace ResultType.Validation.Rule
{
using Results;

public class ValidationError : Error
{
public ValidationError(string msg, string memberName = "", string filePath = "", int line = 0) : base(msg, memberName, filePath, line)
{
}
}

public class BuildError : Error
{
public BuildError(string msg, string memberName = "", string filePath = "", int line = 0) : base(msg, memberName, filePath, line)
{
}
}
}
24 changes: 18 additions & 6 deletions src/ResultType/Extensions/FailureExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@ namespace ResultType.Extensions

public static class FailureExtensions
{
public static IResult<TType> ToFailure<TType>(this Exception obj, string msg, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
=> ResultFactory.CreateFailure<TType>(new Error(msg, memberName, sourceFilePath, sourceLineNumber));
public static IResult<TType> ToFailureWithMsg<TType>(this Exception obj, string msg, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
=> ResultFactory.CreateFailure<TType>(new ErrorWithException(msg, obj, memberName, sourceFilePath, sourceLineNumber));

public static IResult<TType> ToFailure<TType>(this Exception obj, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
=> ResultFactory.CreateFailure<TType>(new ErrorWithException(obj, memberName, sourceFilePath, sourceLineNumber));

public static IResult<TType> ToFailure<TType>(this string msg, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
=> ResultFactory.CreateFailure<TType>(msg, memberName, sourceFilePath, sourceLineNumber);


public static Task<IResult<TType>> ToFailureAsync<TType>(this Exception obj, string msg, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
=> ResultFactory.CreateFailureAsync<TType>(new Error(msg, memberName, sourceFilePath, sourceLineNumber));

public static Task<IResult<TType>> ToFailureAsync<TType>(this Exception obj, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
=> ResultFactory.CreateFailureAsync<TType>(new ErrorWithException(obj, memberName, sourceFilePath, sourceLineNumber));

public static Task<IResult<TType>> ToFailureWithMsgAsync<TType>(this Exception obj, string msg, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
=> ResultFactory.CreateFailureAsync<TType>(new ErrorWithException(msg, obj, memberName, sourceFilePath, sourceLineNumber));

public static Task<IResult<TType>> ToFailureAsync<TType>(this string msg, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
=> ResultFactory.CreateFailureAsync<TType>(new Error(msg, memberName, sourceFilePath, sourceLineNumber));

Expand All @@ -41,5 +46,12 @@ public static IResult<TType> ToFailureWhen<TType>(this TType obj, Predicate<TTyp
[CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
=> obj.ToSuccessWhen(x => !predicate(x), err, memberName, sourceFilePath, sourceLineNumber);

public static bool IsFailure<TType>(this IResult<TType> result) =>
result switch
{
Failure<TType> _ => true,
_ => false
};
}
}
8 changes: 8 additions & 0 deletions src/ResultType/Extensions/SuccessExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace ResultType.Extensions
{
using System;
using System.Net.NetworkInformation;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Factories;
Expand All @@ -27,5 +28,12 @@ public static IResult<TType> ToSuccessWhen<TType>(this TType obj, Predicate<TTyp
public static IResult<TType> ToSuccessWhen<TType>(this TType obj, Predicate<TType> predicate, IError err, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
=>
predicate(obj) ? ResultFactory.CreateSuccess(obj) : ResultFactory.CreateFailure<TType>(err);

public static bool IsSuccess<TType>(this IResult<TType> result) =>
result switch
{
Success<TType> _ => true,
_ => false
};
}
}
2 changes: 1 addition & 1 deletion src/ResultType/ResultType.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>2.0.0</Version>
<Version>2.0.3</Version>
<Authors>Michał Niegrzybowski</Authors>
<Company>Michał Niegrzybowski - MNie</Company>
<PackageId>MNie.ResultType</PackageId>
Expand Down
13 changes: 13 additions & 0 deletions src/ResultType/Results/Error.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace ResultType.Results
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
Expand Down Expand Up @@ -28,6 +29,18 @@ public Error(string msg, [CallerMemberName] string memberName = "", [CallerFileP
}
}

public class ErrorWithException : Error
{
public Exception Exception { get; }
public ErrorWithException(Exception ex, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
: base(ex.Message, memberName, filePath, line) =>
Exception = ex;

public ErrorWithException(string msg, Exception ex, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
: base(msg, memberName, filePath, line) =>
Exception = ex;
}

public class AggregateError : Error
{
public IReadOnlyCollection<IError> Errors { get; }
Expand Down
36 changes: 33 additions & 3 deletions test/ResultType.Tests/Extensions/ResultExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,28 @@ public async Task ToFailureAsync_WhenArgumentIsExceptionWithoutMsg_ReturnFailure

[Fact]
public async Task ToFailureAsync_ReturnSuccessResult() =>
((await (new Exception("error").ToFailureAsync<Unit>("dd"))) as Failure<Unit>)
((await (new Exception("error").ToFailureAsync<Unit>())) as Failure<Unit>)
.Error
.Message
.ShouldBe("dd");
.ShouldBe("error");

[Fact]
public void ToFailure_WhenArgumentIsException_ReturnFailureResult() =>
((new Exception("error").ToFailure<Unit>("dd")) as Failure<Unit>)
((new Exception("error").ToFailure<Unit>()) as Failure<Unit>)
.Error
.Message
.ShouldBe("error");

[Fact]
public async Task ToFailureWithMsgAsync_ReturnSuccessResult() =>
((await (new Exception("error").ToFailureWithMsgAsync<Unit>("dd"))) as Failure<Unit>)
.Error
.Message
.ShouldBe("dd");

[Fact]
public void ToFailureWithMsg_WhenArgumentIsException_ReturnFailureResult() =>
((new Exception("error").ToFailureWithMsg<Unit>("dd")) as Failure<Unit>)
.Error
.Message
.ShouldBe("dd");
Expand Down Expand Up @@ -228,5 +242,21 @@ public void ToFailureWhen_WhenConditionIsFalseWePropagateIError_ReturnSuccess()
.ToFailureWhen(string.IsNullOrWhiteSpace, new TestError("heheszki", "", "", 0))
is Success<string>)
.ShouldBeTrue();

[Fact]
public void IsSuccess_WhenResultIsSuccess_ReturnTrue() =>
ResultFactory.CreateSuccess("on nie wiedzial").IsSuccess().ShouldBeTrue();

[Fact]
public void IsSuccess_WhenResultIsFailure_ReturnFalse() =>
ResultFactory.CreateFailure("on nie wiedzial - exception").IsSuccess().ShouldBeFalse();

[Fact]
public void IsFailure_WhenResultIsSuccess_ReturnFalse() =>
ResultFactory.CreateSuccess("on nie wiedzial").IsFailure().ShouldBeFalse();

[Fact]
public void IsFailure_WhenResultIsFailure_ReturnTrue() =>
ResultFactory.CreateFailure("on nie wiedzial - exception").IsFailure().ShouldBeTrue();
}
}
Loading

0 comments on commit eda3f44

Please sign in to comment.