Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
benmccallum committed Nov 21, 2020
2 parents b708f16 + 7e10dd9 commit fc11c47
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 52 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ Some of the default implementations are defined publicly on the default options
Errors will be written out into the GraphQL execution result in the `Errors` property. By default,
extra information about the validation will be written out in the Extensions property.

This is handled by the `ValidationErrorFilter`.

If you need to selectively ignore some `ValidationException` instances,
you can provide your own predicate function to `ValidationErrorFilter` via the constructor.

For example:

> (note, this is the serialized `IExecutionResult`, not a GraphQL server response)
Expand Down Expand Up @@ -140,6 +145,13 @@ public class UserInputValidator : AbstractValidator<UserInput>, IRequiresOwnScop
}
```

### Using MediatR for firing validation?

If you're using [MediatR](https://github.com/jbogard/MediatR) for firing validation, no worries!

If your MediatR pipeline behaviour throws a `FluentValidation.ValidationException` you can still use FairyBread's
`DefaultValidationErrorFilter` as mentioned earlier to rewrite it on the way out into a friendlier error for the client.

### Where to next?

For more examples, please see the tests.
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<PackageId>$(MSBuildProjectName)</PackageId>
<PackageIconUrl>https://github.com/benmccallum/fairybread/raw/master/logo-400x400.png</PackageIconUrl>

<Version>1.0.0-preview.9</Version>
<Version>1.0.0</Version>

<HotChocolateVersion>10.5.2</HotChocolateVersion>
</PropertyGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/FairyBread.Tests/CustomizationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ private IQueryExecutor InitQueryExecutor(Action<IServiceCollection> preBuildProv
{
builder
.UseDefaultPipeline()
.AddErrorFilter<DefaultValidationErrorFilter>();
.AddErrorFilter<ValidationErrorFilter>();
});
}

Expand Down
80 changes: 80 additions & 0 deletions src/FairyBread.Tests/DefaultValidationErrorFilterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using FluentValidation;
using HotChocolate;
using Moq;
using Xunit;

namespace FairyBread.Tests
{
public class DefaultValidationErrorFilterTests
{
[Fact]
public void Should_Handle_ValidationException()
{
// Arrange
var error = new AnError().WithException(new ValidationException("some msg"));

var defaultValidationErrorFilter = new ValidationErrorFilter();

// Act / Assert
Assert.Throws<NotImplementedException>(() => defaultValidationErrorFilter.OnError(error));
}

[Fact]
public void Should_Ignore_OtherException()
{
// Arrange
var error = new AnError().WithException(new Exception("some msg"));

var defaultValidationErrorFilter = new ValidationErrorFilter();

// Act / Assert
defaultValidationErrorFilter.OnError(error);
}

[Fact]
public void Should_Ignore_CertainValidationException()
{
// Arrange
var error = new AnError().WithException(new Exception("some msg"));

var defaultValidationErrorFilter = new ValidationErrorFilter(ex => false);

// Act / Assert
defaultValidationErrorFilter.OnError(error);
}

class AnError : IError
{
public string Message => throw new NotImplementedException();

public string Code => throw new NotImplementedException();

public IReadOnlyList<object> Path => throw new NotImplementedException();

public IReadOnlyList<Location> Locations => throw new NotImplementedException();

private Exception? _exception;
public Exception? Exception => _exception;

public IReadOnlyDictionary<string, object> Extensions => throw new NotImplementedException();

public IError AddExtension(string key, object value) => throw new NotImplementedException();
public IError RemoveException() => throw new NotImplementedException();
public IError RemoveExtension(string key) => throw new NotImplementedException();
public IError WithCode(string code) => throw new NotImplementedException();
public IError WithException(Exception exception)
{
_exception = exception;
return this;
}

public IError WithExtensions(IReadOnlyDictionary<string, object> extensions) => throw new NotImplementedException();
public IError WithLocations(IReadOnlyList<Location> locations) => throw new NotImplementedException();
public IError WithMessage(string message) => throw new NotImplementedException();
public IError WithPath(Path path) => throw new NotImplementedException();
public IError WithPath(IReadOnlyList<object> path) => throw new NotImplementedException();
}
}
}
4 changes: 2 additions & 2 deletions src/FairyBread.Tests/InputValidationMiddlewareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ private IQueryExecutor GetQueryExecutor(Action<IFairyBreadOptions>? configureOpt
{
builder
.UseDefaultPipeline()
.AddErrorFilter<DefaultValidationErrorFilter>();
.AddErrorFilter<ValidationErrorFilter>();
});
}

Expand Down Expand Up @@ -133,7 +133,7 @@ public async Task Mutation_Works(CaseData caseData)
{
builder
.UseDefaultPipeline()
.AddErrorFilter<DefaultValidationErrorFilter>();
.AddErrorFilter<ValidationErrorFilter>();
});

// Act
Expand Down
2 changes: 1 addition & 1 deletion src/FairyBread.Tests/RequiresOwnScopeValidatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ private IQueryExecutor InitQueryExecutor(Action<IServiceCollection> preBuildProv
{
builder
.UseDefaultPipeline()
.AddErrorFilter<DefaultValidationErrorFilter>();
.AddErrorFilter<ValidationErrorFilter>();
});
}

Expand Down
4 changes: 2 additions & 2 deletions src/FairyBread.Tests/ValidateDescriptorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ private IQueryExecutor InitQueryExecutor()
services.AddFairyBread(options =>
{
options.AssembliesToScanForValidators = new[] { typeof(FooInputDtoValidator).Assembly };
options.ShouldValidate = DefaultFairyBreadOptions.ShouldValidateBasedOnValidateDescriptorImplementation;
options.ShouldValidate = DefaultFairyBreadOptions.DefaultImplementations.ShouldValidateBasedOnValidateDescriptor;
});

var serviceProvider = services.BuildServiceProvider();
Expand All @@ -42,7 +42,7 @@ private IQueryExecutor InitQueryExecutor()
{
builder
.UseDefaultPipeline()
.AddErrorFilter<DefaultValidationErrorFilter>();
.AddErrorFilter<ValidationErrorFilter>();
});
}

Expand Down
133 changes: 92 additions & 41 deletions src/FairyBread/DefaultFairyBreadOptions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
Expand All @@ -15,61 +14,113 @@ public class DefaultFairyBreadOptions : IFairyBreadOptions

public virtual bool ThrowIfNoValidatorsFound { get; set; } = true;

public virtual Func<IMiddlewareContext, Argument, bool> ShouldValidate { get; set; } = ShouldValidateImplementation;
/// <inheritdoc/>
public virtual Func<IMiddlewareContext, Argument, bool> ShouldValidate { get; set; }
= DefaultImplementations.ShouldValidate;

public static bool ShouldValidateImplementation(IMiddlewareContext context, Argument argument)
/// <summary>
/// Default implementations of some things that can be re-composed as needed.
/// </summary>
public static class DefaultImplementations
{
// If a mutation operation and this is an input object
if (context.Operation.Operation == OperationType.Mutation &&
argument.Type.InnerType() is InputObjectType)
/// <summary>
/// By default, FairyBread will validate any argument that:
/// <list type="bullet">
/// <item>
/// <description>
/// is an InputObjectType on a mutation operation,
/// </description>
/// </item>
/// <item>
/// <description>
/// is manually opted-in at the field level with:
/// <list type="bullet">
/// <item><description>
/// [Validate] on the resolver method argument in pure code first
/// </description></item>
/// <item><description>
/// .UseValidation() on the argument definition in code first
/// </description></item>
/// </list>
/// </description>
/// </item>
/// <item>
/// <description>
/// is manually opted-in at the input type level with:
/// <list type="bullet">
/// <item><description>
/// [Validate] on the CLR backing type in pure code first
/// </description></item>
/// <item><description>
/// .UseValidation() on the InputObjectType descriptor in code first
/// </description></item>
/// </list>
/// </description>
/// </item>
/// <item>
/// <description>
///
/// </description>
/// </item>
/// </list>
/// </summary>
/// <remarks>
/// More at: https://github.com/benmccallum/fairybread#when-validation-will-fire
/// </remarks>
public static bool ShouldValidate(IMiddlewareContext context, Argument argument)
{
return true;
// If a mutation operation and this is an input object
if (context.Operation.Operation == OperationType.Mutation &&
argument.Type.InnerType() is InputObjectType)
{
return true;
}

if (ShouldValidateBasedOnValidateDescriptor(context, argument))
{
return true;
}

return false;
}

if (ShouldValidateBasedOnValidateDescriptorImplementation(context, argument))
public static bool ShouldValidateBasedOnValidateDescriptor(IMiddlewareContext context, Argument argument)
{
return true;
}
// If argument itself was annotated
if (IsValidateDescriptorApplied(argument.ContextData))
{
return true;
}

return false;
}
// If argument's input type was annotated
if (argument.Type.InnerType() is InputObjectType inputType &&
IsValidateDescriptorApplied(inputType))
{
return true;
}

public static bool ShouldValidateBasedOnValidateDescriptorImplementation(IMiddlewareContext context, Argument argument)
{
// If argument itself was annotated
if (IsValidateDescriptorApplied(argument.ContextData))
{
return true;
// If argument's clr type was annotated
if (ClrTypesMarkedWithValidate.Cache.GetOrAdd(
argument.ClrType,
clrType => clrType.GetCustomAttribute<ValidateAttribute>(inherit: true) != null))
{
return true;
}

return false;
}

// If argument's input type was annotated
if (argument.Type.InnerType() is InputObjectType inputType &&
IsValidateDescriptorApplied(inputType))
private static bool IsValidateDescriptorApplied(IHasContextData thing)
{
return true;
return IsValidateDescriptorApplied(thing.ContextData);
}

// If argument's clr type was annotated
if (ClrTypesMarkedWithValidate.Cache.GetOrAdd(
argument.ClrType,
clrType => clrType.GetCustomAttribute<ValidateAttribute>(inherit: true) != null))
private static bool IsValidateDescriptorApplied(IReadOnlyDictionary<string, object?> contextData)
{
return true;
return contextData.TryGetValue(ValidateAttribute.ValidateContextDataKey, out var isValidateDescriptorAppliedRaw) &&
isValidateDescriptorAppliedRaw is bool isValidateDescriptorApplied &&
isValidateDescriptorApplied;
}

return false;
}

private static bool IsValidateDescriptorApplied(IHasContextData thing)
{
return IsValidateDescriptorApplied(thing.ContextData);
}

private static bool IsValidateDescriptorApplied(IReadOnlyDictionary<string, object?> contextData)
{
return contextData.TryGetValue(ValidateAttribute.ValidateContextDataKey, out var isValidateDescriptorAppliedRaw) &&
isValidateDescriptorAppliedRaw is bool isValidateDescriptorApplied &&
isValidateDescriptorApplied;
}
}
}
8 changes: 7 additions & 1 deletion src/FairyBread/IFairyBreadOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using HotChocolate.Resolvers;
using FluentValidation;
using HotChocolate.Resolvers;
using HotChocolate.Types;
using System;
using System.Collections.Generic;
Expand All @@ -12,6 +13,11 @@ public interface IFairyBreadOptions

bool ThrowIfNoValidatorsFound { get; set; }

/// <summary>
/// Function used to determine if an argument should be validated by
/// FairyBread's <see cref="InputValidationMiddleware"/>.
/// The default implementation is <see cref="DefaultImplementations.ShouldValidate(IMiddlewareContext, Argument)"/>
/// </summary>
Func<IMiddlewareContext, Argument, bool> ShouldValidate { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
using System.Linq;
using System;
using System.Linq;
using FluentValidation;
using FluentValidation.Results;
using HotChocolate;

namespace FairyBread
{
public class DefaultValidationErrorFilter : IErrorFilter
public class ValidationErrorFilter : IErrorFilter
{
/// <summary>
/// Function used to determine if an <see cref="IError"/> with a <see cref="ValidationException"/>
/// should be handled and rewritten.
/// </summary>
private Func<ValidationException, bool> _shouldHandleValidationExceptionPredicate { get; set; } = ex => true;

public ValidationErrorFilter()
{

}

public ValidationErrorFilter(Func<ValidationException, bool> shouldHandleValidationExceptionPredicate)
{
_shouldHandleValidationExceptionPredicate = shouldHandleValidationExceptionPredicate;
}

public IError OnError(IError error)
{
if (error.Exception != null &&
error.Exception is ValidationException validationException)
error.Exception is ValidationException validationException &&
_shouldHandleValidationExceptionPredicate(validationException))
{
return OnValidationException(error, validationException);
}
Expand Down

0 comments on commit fc11c47

Please sign in to comment.