Skip to content

Commit

Permalink
Hide validator behind validator interface
Browse files Browse the repository at this point in the history
  • Loading branch information
inputfalken committed Nov 27, 2018
1 parent 0335677 commit 99ee4ef
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 93 deletions.
49 changes: 49 additions & 0 deletions src/Lemonad.ErrorHandling/IValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using Lemonad.ErrorHandling.Internal;

namespace Lemonad.ErrorHandling {
/// <summary>
/// An <typeparamref name="TError"/> collection of <typeparamref name="TError"/> based on validations of <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">
/// The type of the validation candidate.
/// </typeparam>
/// <typeparam name="TError">
/// The type of the the error type.
/// </typeparam>
public interface IValidator<out T, TError> : IReadOnlyCollection<TError> {
/// <summary>
/// Validate <typeparamref name="T"/> with an <paramref name="predicate"/> function and set the failure type in <paramref name="errorSelector"/>.
/// </summary>
/// <param name="predicate">
/// A function to test <typeparamref name="T"/> for a condition.
/// </param>
/// <param name="errorSelector">
/// A function to set the error if the predicate function would return false.
/// </param>
/// <returns>
/// An <see cref="Validator{T,TError}"/>.
/// </returns>
IValidator<T, TError> Validate(Func<T, bool> predicate, Func<T, TError> errorSelector);

/// <summary>
/// Validate <typeparamref name="T"/> with an <paramref name="predicate"/> function and set the failure type with <paramref name="error"/>.
/// </summary>
/// <param name="predicate">
/// A function to test <typeparamref name="T"/> for a condition.
/// </param>
/// <param name="error">
/// The error value.
/// </param>
/// <returns>
/// An <see cref="Validator{T,TError}"/>.
/// </returns>
IValidator<T, TError> Validate(Func<T, bool> predicate, TError error);

/// <summary>
/// Gets the <see cref="IResult{T,TError}"/>.
/// </summary>
IResult<T, IReadOnlyCollection<TError>> Result { get; }
}
}
43 changes: 43 additions & 0 deletions src/Lemonad.ErrorHandling/Internal/Validator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Collections;
using System.Collections.Generic;

namespace Lemonad.ErrorHandling.Internal {
internal readonly struct Validator<T, TError> : IValidator<T, TError> {
private readonly T _candidate;
private readonly Queue<TError> _errors;

public IResult<T, IReadOnlyCollection<TError>> Result { get; }

internal Validator(T candidate) {
_candidate = candidate;
_errors = new Queue<TError>();
Result = ErrorHandling.Result.Value<T, IReadOnlyCollection<TError>>(_candidate);
}

private Validator(in Queue<TError> errors, in T candidate) {
_errors = new Queue<TError>(errors);
_candidate = candidate;
Result = _errors.Count > 0
? ErrorHandling.Result.Error<T, IReadOnlyCollection<TError>>(_errors)
: ErrorHandling.Result.Value<T, IReadOnlyCollection<TError>>(_candidate);
}

public IEnumerator<TError> GetEnumerator() => _errors.GetEnumerator();

public IValidator<T, TError> Validate(Func<T, bool> predicate, Func<T, TError> errorSelector) {
if (predicate == null) throw new ArgumentException(nameof(predicate));
if (errorSelector == null) throw new ArgumentException(nameof(errorSelector));
var result = _candidate.ToResult(predicate, errorSelector);
// This refers to the initial queue.
if (result.Either.HasError) _errors.Enqueue(result.Either.Error);
return new Validator<T, TError>(_errors, _candidate);
}

public IValidator<T, TError> Validate(Func<T, bool> predicate, TError error) => Validate(predicate, _ => error);

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

public int Count => _errors.Count;
}
}
97 changes: 15 additions & 82 deletions src/Lemonad.ErrorHandling/Validator.cs
Original file line number Diff line number Diff line change
@@ -1,87 +1,20 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System;
using Lemonad.ErrorHandling.Internal;

namespace Lemonad.ErrorHandling {
/// <summary>
/// An <typeparamref name="TError"/> collection of <typeparamref name="TError"/> based on validations of <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">
/// The type of the validation candidate.
/// </typeparam>
/// <typeparam name="TError">
/// The type of the the error type.
/// </typeparam>
public readonly struct Validator<T, TError> : IReadOnlyCollection<TError> {
private readonly T _candidate;
private readonly Queue<TError> _errors;

/// <summary>
/// Convert to a <see cref="Result{T,TError}"/>.
/// </summary>
public IResult<T, IReadOnlyCollection<TError>> Result {
get {
var candidate = _candidate;
return _errors.ToResultError<T, IReadOnlyCollection<TError>>(x => x.Count > 0, _ => candidate);
}
}

/// <summary>
/// Creates an instance of <see cref="Validator{T,TError}"/>.
/// </summary>
/// <param name="candidate">
/// The validation <paramref name="candidate"/>.
/// </param>
public Validator(T candidate) {
_candidate = candidate;
_errors = new Queue<TError>();
}

private Validator(in Queue<TError> errors, in T candidate) {
_errors = errors;
_candidate = candidate;
}

/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
public IEnumerator<TError> GetEnumerator() => _errors.GetEnumerator();

/// <summary>
/// Validate <typeparamref name="T"/> with an <paramref name="predicate"/> function and set the failure type in <paramref name="errorSelector"/>.
/// </summary>
/// <param name="predicate">
/// A function to test <typeparamref name="T"/> for a condition.
/// </param>
/// <param name="errorSelector">
/// A function to set the error if the predicate function would return false.
/// </param>
/// <returns>
/// An <see cref="Validator{T,TError}"/>.
/// </returns>
public Validator<T, TError> Validate(Func<T, bool> predicate, Func<TError> errorSelector) {
var result = _candidate.ToResult(predicate, _ => errorSelector());
if (result.Either.HasError) _errors.Enqueue(result.Either.Error);
return new Validator<T, TError>(_errors, _candidate);
}

/// <summary>
/// Validate <typeparamref name="T"/> with an <paramref name="predicate"/> function and set the failure type with <paramref name="error"/>.
/// </summary>
/// <param name="predicate">
/// A function to test <typeparamref name="T"/> for a condition.
/// </param>
/// <param name="error">
/// The error value.
/// </param>
/// <returns>
/// An <see cref="Validator{T,TError}"/>.
/// </returns>
public Validator<T, TError> Validate(Func<T, bool> predicate, TError error) => Validate(predicate, () => error);

/// <inheritdoc cref="IEnumerable.GetEnumerator"/>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

/// <inheritdoc cref="IReadOnlyCollection{T}.Count"/>
public int Count => _errors.Count;
public static class Validator {
public static IValidator<T, TError> Value<T, TError>(T value) => new Validator<T, TError>(value);

public static IValidator<T, TError> Value<T, TError>(
T value,
Func<T, bool> predicate,
Func<T, TError> errorSelector
) => new Validator<T, TError>(value).Validate(predicate, errorSelector);

public static IValidator<T, TError> Value<T, TError>(
T value,
Func<T, bool> predicate,
TError errorSelector
) => new Validator<T, TError>(value).Validate(predicate, errorSelector);
}
}
96 changes: 85 additions & 11 deletions test/Lemonad.ErrorHandling.Test/Validator.Tests/ValidateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,124 @@ public class ValidateTests {
[Fact]
public void Single_False_Validation() {
const string error = "Not dividable by 2";
var validator = new Validator<int, string>(1).Validate(i => i % 2 == 0, error).ToList();
var validator = ErrorHandling.Validator.Value<int, string>(1).Validate(i => i % 2 == 0, error).ToList();
Assert.Single(validator, error);
}

[Fact]
public void Single_True_Validation() {
const string error = "Not dividable by 2";
var validator = new Validator<int, string>(2).Validate(i => i % 2 == 0, error).ToList();
var validator = ErrorHandling.Validator.Value<int, string>(2).Validate(i => i % 2 == 0, error).ToList();
Assert.Empty(validator);
}

[Fact]
public void Double_True_Validation() {
const string error1 = "Not dividable by 2";
const string error2 = "Is equal to 2.";
var validator = new Validator<int, string>(2)
var validator = ErrorHandling.Validator.Value<int, string>(2)
.Validate(i => i % 2 == 0, error1)
.Validate(i => i == 2, () => error2)
.Validate(i => i == 2, x => error2)
.ToList();
Assert.Empty(validator);
}

[Fact]
public void Double_True_Validation_Verify_States() {
const string error1 = "Not dividable by 2";
const string error2 = "Is equal to 2.";
var state0 = ErrorHandling.Validator.Value<int, string>(2);
Assert.Empty(state0);
var state1 = state0.Validate(i => i % 2 == 0, error1);
Assert.Empty(state1);
var state2 = state1.Validate(i => i == 2, x => error2);
Assert.Empty(state2);
var validator = state2.ToList();
Assert.Empty(validator);
}

[Fact]
public void Double_False_Validation() {
const string error1 = "Is not equal to 0";
const string error2 = "Not dividable by 2";
var validator = new Validator<int, string>(1)
.Validate(i => i == 0, () => error1)
.Validate(i => i % 2 == 0, () => error2)
var validator = ErrorHandling.Validator.Value<int, string>(1)
.Validate(i => i == 0, x => error1)
.Validate(i => i % 2 == 0, x => error2)
.ToList();
Assert.Equal(2, validator.Count);
Assert.Equal(error1, validator[0]);
Assert.Equal(error2, validator[1]);
}

[Fact]
public void Single_True_Validation_And_Single_False_Validation() {
public void Double_False_Validation_Verify_States() {
const string error1 = "Is not equal to 0";
const string error2 = "Not dividable by 2";
var state0 = ErrorHandling.Validator.Value<int, string>(1);
Assert.Empty(state0);
var state1 = state0.Validate(i => i == 0, x => error1);
Assert.Single(state1);
var state2 = state1.Validate(i => i % 2 == 0, x => error2);
Assert.Equal(2, state2.Count);
var validator = state2.ToList();
Assert.Equal(2, validator.Count);
Assert.Equal(error1, validator[0]);
Assert.Equal(error2, validator[1]);
}

[Fact]
public void First_False_Validation_And_Second_True_Validation_Verify_States() {
const string error1 = "Is not equal to 0";
const string error2 = "Not dividable by 2";
var state0 = ErrorHandling.Validator.Value<int, string>(2);
var state1 = state0.Validate(i => i == 0, x => error1);
var state2 = state1.Validate(i => i % 2 == 0, x => error2);
// These errors occur since the collection in `state2` references the collection in `state0`.
// So all errors in `state2` are found in `state0` therefor.
Assert.Empty(state0);
Assert.Single(state1);
Assert.Single(state2);
}

[Fact]
public void First_True_Validation_And_Second_False_Validation_Verify_States() {
const string error1 = "Is not equal to 0";
const string error2 = "Not dividable by 2";
var validator = new Validator<int, string>(2)
.Validate(i => i == 0, () => error1)
.Validate(i => i % 2 == 0, () => error2)
var state0 = ErrorHandling.Validator.Value<int, string>(2);
var state1 = state0.Validate(i => i % 2 == 0, x => error2);
var state2 = state1.Validate(i => i == 0, x => error1);
Assert.Empty(state0);
Assert.Empty(state1);
Assert.Single(state2);
}

[Fact]
public void First_False_Validation_And_Second_True_Validation() {
const string error1 = "Is not equal to 0";
const string error2 = "Not dividable by 2";
var validator = ErrorHandling.Validator.Value<int, string>(2)
.Validate(i => i == 0, x => error1)
.Validate(i => i % 2 == 0, x => error2)
.ToList();
Assert.Single(validator);
}

[Fact]
public void First_True_Validation_And_Second_False_Validation() {
const string error1 = "Is not equal to 0";
const string error2 = "Not dividable by 2";
var validator = ErrorHandling.Validator.Value<int, string>(2)
.Validate(i => i % 2 == 0, x => error2)
.Validate(i => i == 0, x => error1)
.ToList();
Assert.Single(validator);
}

[Fact]
public void Result_Does_Not_Create_New_Instance_On_Get() {
var validator = ErrorHandling.Validator.Value<int, string>(2);
Assert.Equal(validator.Result, validator.Result);
Assert.StrictEqual(validator.Result, validator.Result);
}
}
}

0 comments on commit 99ee4ef

Please sign in to comment.