Skip to content

Add Group support #58

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

Merged
merged 5 commits into from
Jun 19, 2023
Merged
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.avaje.validation.generator.models.valid;

import java.util.Map;
import java.util.Set;

import io.avaje.lang.Nullable;
import io.avaje.validation.adapter.AnnotationValidator;
Expand All @@ -11,7 +12,7 @@
@AnnotationValidator(Nullable.class)
public final class CustomAnnotationAdapter implements ValidationAdapter<Object> {

public CustomAnnotationAdapter(ValidationContext ctx, Map<String, Object> attributes) {}
public CustomAnnotationAdapter(ValidationContext ctx, Set<Class<?>> groups, Map<String, Object> attributes) {}

@Override
public boolean validate(Object value, ValidationRequest req, String propertyName) {
Expand Down
25 changes: 19 additions & 6 deletions validator/src/main/java/io/avaje/validation/Validator.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.avaje.validation;

import io.avaje.lang.Nullable;
import io.avaje.validation.adapter.*;
import io.avaje.validation.core.DefaultBootstrap;
import io.avaje.validation.spi.Bootstrap;
Expand All @@ -12,12 +13,13 @@
import java.util.Map;
import java.util.ResourceBundle;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.function.Supplier;

public interface Validator {

/** Validate the object using the default locale. */
void validate(Object any) throws ConstraintViolationException;
void validate(Object value, @Nullable Class<?>... groups) throws ConstraintViolationException;

/**
* Validate the object with a given locale.
Expand All @@ -26,7 +28,8 @@ public interface Validator {
*
* <p>This is expected to be used when the Validator is configured to support multiple locales.
*/
void validate(Object any, Locale locale) throws ConstraintViolationException;
void validate(Object any, @Nullable Locale locale, @Nullable Class<?>... groups)
throws ConstraintViolationException;

static Builder builder() {
final var bootstrapService = ServiceLoader.load(Bootstrap.class).iterator();
Expand All @@ -52,9 +55,7 @@ interface Builder {
*/
Builder addResourceBundles(String... bundleName);

/**
* Add ResourceBundle for error message interpolation
*/
/** Add ResourceBundle for error message interpolation */
Builder addResourceBundles(ResourceBundle... bundle);

/** Set Default Locale for this validator, if not set, will use Locale.getDefault() */
Expand All @@ -63,10 +64,21 @@ interface Builder {
/** Adds additional Locales for this validator */
Builder addLocales(Locale... locales);

/**
* Contract for obtaining the Clock used as the reference for now when validating the @Future
* and @Past constraints.
*/
Builder clockProvider(Supplier<Clock> clockSupplier);

/** Define the acceptable margin of error when comparing date/time in temporal constraints. */
Builder temporalTolerance(Duration temporalTolerance);

/**
* En- or disables the fail fast mode. When fail fast is enabled the validation will stop on the
* first constraint violation detected.
*/
Builder failFast(boolean failFast);

/** Add a AdapterBuilder which provides a ValidationAdapter to use for the given type. */
Builder add(Type type, AdapterBuilder builder);

Expand Down Expand Up @@ -101,7 +113,8 @@ interface AdapterBuilder {
interface AnnotationAdapterBuilder {

/** Create a ValidationAdapter given the Validator instance. */
ValidationAdapter<?> build(ValidationContext ctx, Map<String, Object> attributes);
ValidationAdapter<?> build(
ValidationContext ctx, Set<Class<?>> groups, Map<String, Object> attributes);
}

/** Components register ValidationAdapters Validator.Builder */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
*
* private final Message message;
*
* public CustomAnnotationAdapter(ValidationContext ctx, Map<String, Object> attributes) {
* public CustomAnnotationAdapter(ValidationContext ctx, Set<Class<?>> groups, Map<String, Object> attributes) {
* //create a message object for error interpolation
* message = ctx.message("{message.property}");
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package io.avaje.validation.adapter;

import static java.util.stream.Collectors.toUnmodifiableSet;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

@FunctionalInterface
public interface ValidationAdapter<T> {
Expand Down Expand Up @@ -92,4 +96,21 @@ default ValidationAdapter<T> andThen(ValidationAdapter<? super T> after) {
return true;
};
}

/**
* Returns true if the validation request groups is empty or matches any of the adapter's
* configured groups
*/
default boolean checkGroups(Set<Class<?>> adapterGroups, ValidationRequest request) {
final var requestGroups = request.groups();

if (requestGroups.isEmpty()) return true;

for (final var group : requestGroups) {
if (adapterGroups.contains(group)) {
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.Set;

/**
* Context available when validation adapters are being created.
Expand Down Expand Up @@ -67,6 +68,6 @@ interface AnnotationFactory {
* <p>Returning null means that the adapter could be created by another factory.
*/
ValidationAdapter<?> create(
Class<? extends Annotation> annotationType, ValidationContext ctx, Map<String, Object> attributes);
Class<? extends Annotation> annotationType, ValidationContext ctx, Set<Class<?>> groups, Map<String, Object> attributes);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package io.avaje.validation.adapter;

import java.util.List;

/**
* A validation request.
*/
public interface ValidationRequest {

/** The groups tied to this ValidationRequest */
List<Class<?>> groups();

/**
* Add a constraint violation for the given property.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package io.avaje.validation.core;

import static java.util.stream.Collectors.toUnmodifiableSet;

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

Expand Down Expand Up @@ -73,12 +76,18 @@ <T> ValidationAdapter<T> build(Type type, Object cacheKey) {
<T> ValidationAdapter<T> buildAnnotation(Class<? extends Annotation> cls, Map<String, Object> attributes) {
// Ask each factory to create the validation adapter.
for (final ValidationContext.AnnotationFactory factory : annotationFactories) {
final var result = (ValidationAdapter<T>) factory.create(cls, context, attributes);
final var result = (ValidationAdapter<T>) factory.create(cls, context, extractGroups(attributes), attributes);
if (result != null) {
return result;
}
}
// unknown annotations have noop
return NOOP;
}

static Set<Class<?>> extractGroups(Map<String, Object> attributes) {
@SuppressWarnings("unchecked")
final var annotationsGroups = (List<Class<?>>) attributes.getOrDefault("groups", List.of());
return annotationsGroups.stream().collect(toUnmodifiableSet());
}
}
21 changes: 16 additions & 5 deletions validator/src/main/java/io/avaje/validation/core/DRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@ final class DRequest implements ValidationRequest {
private final Set<ConstraintViolation> violations = new LinkedHashSet<>();

private final DValidator validator;
@Nullable
private final Locale locale;
private final boolean failfast;
private final List<Class<?>> groups;
@Nullable private final Locale locale;

DRequest(DValidator validator, @Nullable Locale locale) {
DRequest(DValidator validator, boolean failfast, @Nullable Locale locale, List<Class<?>> groups) {
this.validator = validator;
this.failfast = failfast;
this.locale = locale;
this.groups = groups;
}

private String currentPath() {
StringJoiner joiner = new StringJoiner(".");
final StringJoiner joiner = new StringJoiner(".");
final var descendingIterator = pathStack.descendingIterator();
while (descendingIterator.hasNext()) {
joiner.add(descendingIterator.next());
Expand All @@ -34,8 +37,11 @@ private String currentPath() {

@Override
public void addViolation(ValidationContext.Message msg, String propertyName) {
String message = validator.interpolate(msg, locale);
final String message = validator.interpolate(msg, locale);
violations.add(new ConstraintViolation(currentPath(), propertyName, message));
if (failfast) {
throwWithViolations();
}
}

@Override
Expand All @@ -54,4 +60,9 @@ public void throwWithViolations() {
throw new ConstraintViolationException(violations);
}
}

@Override
public List<Class<?>> groups() {
return groups;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.avaje.validation.core;

import java.util.List;
import java.util.Locale;

import io.avaje.validation.adapter.ValidationAdapter;
Expand All @@ -15,8 +16,8 @@ final class DValidationType<T> implements ValidationType<T> {
}

@Override
public void validate(T object, Locale locale) {
final var req = validator.request(locale);
public void validate(T object, Locale locale, List<Class<?>> groups) {
final var req = validator.request(locale, groups);
adapter.validate(object, req);
req.throwWithViolations();
}
Expand Down
40 changes: 28 additions & 12 deletions validator/src/main/java/io/avaje/validation/core/DValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
Expand All @@ -35,6 +36,7 @@ final class DValidator implements Validator, ValidationContext {
private final LocaleResolver localeResolver;
private final DTemplateLookup templateLookup;
private final Map<String, String> messageCache = new ConcurrentHashMap<>();
private final boolean failfast;

DValidator(
List<AdapterFactory> factories,
Expand All @@ -44,7 +46,8 @@ final class DValidator implements Validator, ValidationContext {
MessageInterpolator interpolator,
LocaleResolver localeResolver,
Supplier<Clock> clockSupplier,
Duration temporalTolerance) {
Duration temporalTolerance,
boolean failfast) {
this.localeResolver = localeResolver;
final var defaultResourceBundle =
new DResourceBundleManager(bundleNames, bundles, localeResolver);
Expand All @@ -53,22 +56,23 @@ final class DValidator implements Validator, ValidationContext {
this.builder =
new CoreAdapterBuilder(
this, factories, annotationFactories, clockSupplier, temporalTolerance);
this.failfast = failfast;
}

MessageInterpolator interpolator() {
return this.interpolator;
}

@Override
public void validate(Object any) {
validate(any, null);
public void validate(Object any, @Nullable Class<?>... groups) {
validate(any, null, groups);
}

@Override
@SuppressWarnings("unchecked")
public void validate(Object any, @Nullable Locale locale) {
public void validate(Object any, @Nullable Locale locale, @Nullable Class<?>... groups) {
final var type = (ValidationType<Object>) type(any.getClass());
type.validate(any, locale);
type.validate(any, locale, List.of(groups));
}

private <T> ValidationType<T> type(Class<T> cls) {
Expand Down Expand Up @@ -119,8 +123,8 @@ public <T> ValidationAdapter<T> adapter(Type type) {
return builder.build(type, cacheKey);
}

ValidationRequest request(@Nullable Locale locale) {
return new DRequest(this, locale);
ValidationRequest request(@Nullable Locale locale, List<Class<?>> groups) {
return new DRequest(this, failfast, locale, groups);
}

String interpolate(Message msg, Locale requestLocale) {
Expand All @@ -147,6 +151,7 @@ static final class DBuilder implements Validator.Builder {
private Locale defaultLocal = Locale.getDefault();
private Supplier<Clock> clockSupplier = Clock::systemDefaultZone;
private Duration temporalTolerance = Duration.ZERO;
private boolean failfast;

@Override
public Builder add(Type type, AdapterBuilder builder) {
Expand Down Expand Up @@ -222,6 +227,12 @@ public Builder temporalTolerance(Duration temporalTolerance) {
return this;
}

@Override
public Builder failFast(boolean failfast) {
this.failfast = failfast;
return this;
}

private void registerComponents() {
// first register all user defined ValidatorComponent
for (final ValidatorComponent next : ServiceLoader.load(ValidatorComponent.class)) {
Expand Down Expand Up @@ -249,13 +260,16 @@ public DValidator build() {
interpolator,
localeResolver,
clockSupplier,
temporalTolerance);
temporalTolerance,
failfast);
}

private static <T> AnnotationFactory newAnnotationAdapterFactory(Type type, ValidationAdapter<T> adapter) {
private static <T> AnnotationFactory newAnnotationAdapterFactory(
Type type, ValidationAdapter<T> adapter) {
requireNonNull(type);
requireNonNull(adapter);
return (targetType, context, attributes) -> simpleMatch(type, targetType) ? adapter : null;
return (targetType, context, groups, attributes) ->
simpleMatch(type, targetType) ? adapter : null;
}

private static <T> AdapterFactory newAdapterFactory(Type type, ValidationAdapter<T> adapter) {
Expand All @@ -270,10 +284,12 @@ private static AdapterFactory newAdapterFactory(Type type, AdapterBuilder builde
return (targetType, ctx) -> simpleMatch(type, targetType) ? builder.build(ctx) : null;
}

private static AnnotationFactory newAdapterFactory(Class<? extends Annotation> type, AnnotationAdapterBuilder builder) {
private static AnnotationFactory newAdapterFactory(
Class<? extends Annotation> type, AnnotationAdapterBuilder builder) {
requireNonNull(type);
requireNonNull(builder);
return (targetType, ctx, attributes) -> simpleMatch(type, targetType) ? builder.build(ctx, attributes) : null;
return (targetType, ctx, groups, attributes) ->
simpleMatch(type, targetType) ? builder.build(ctx, groups, attributes) : null;
}
}

Expand Down
Loading