diff options
-rw-r--r-- | documentation/datamodel/datamodel-forms.asciidoc | 4 | ||||
-rw-r--r-- | server/src/main/java/com/vaadin/data/Binder.java | 133 | ||||
-rw-r--r-- | server/src/main/java/com/vaadin/data/Result.java | 170 | ||||
-rw-r--r-- | server/src/main/java/com/vaadin/data/SimpleResult.java | 96 | ||||
-rw-r--r-- | server/src/main/java/com/vaadin/data/ValidationError.java | 68 | ||||
-rw-r--r-- | server/src/main/java/com/vaadin/data/Validator.java | 136 | ||||
-rw-r--r-- | server/src/test/java/com/vaadin/data/BinderTest.java | 68 | ||||
-rw-r--r-- | server/src/test/java/com/vaadin/data/ResultTest.java | 105 | ||||
-rw-r--r-- | server/src/test/java/com/vaadin/data/ValidatorTest.java | 93 |
9 files changed, 868 insertions, 5 deletions
diff --git a/documentation/datamodel/datamodel-forms.asciidoc b/documentation/datamodel/datamodel-forms.asciidoc index cbb50d1743..06705a7fc6 100644 --- a/documentation/datamodel/datamodel-forms.asciidoc +++ b/documentation/datamodel/datamodel-forms.asciidoc @@ -322,7 +322,7 @@ Even if the user has not edited a field, all validation error will be shown if w binder.load(new Person()); // This will make all current validation errors visible -Set<BinderResult> validationErrors = binder.validate(); +List<ValidationError<?>> validationErrors = binder.validate(); if (!validationErrors.isEmpty()) { Notification.show("Validation error count: " @@ -594,7 +594,7 @@ Button saveButton = new Button("Save", event -> { // Create non-shared copy to use for validation Person copy = new Person(person); - Set<BinderResult> errors = binder.validateWithBean(copy); + List<ValidationError<?>> errors = binder.validateWithBean(copy); if (errors.isEmpty()) { // Write new values to the actual bean diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java index 3ae5a82e3e..a34db33e51 100644 --- a/server/src/main/java/com/vaadin/data/Binder.java +++ b/server/src/main/java/com/vaadin/data/Binder.java @@ -16,14 +16,20 @@ package com.vaadin.data; import java.io.Serializable; +import java.util.ArrayList; import java.util.LinkedHashSet; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; import com.vaadin.event.Registration; +import com.vaadin.server.UserError; +import com.vaadin.ui.AbstractComponent; /** * Connects one or more {@code Field} components to properties of a backing data @@ -105,6 +111,42 @@ public class Binder<T> implements Serializable { * if {@code bind} has already been called on this binding */ public void bind(Function<T, V> getter, BiConsumer<T, V> setter); + + /** + * Adds a validator to this binding. Validators are applied, in + * registration order, when the field value is saved to the backing + * property. If any validator returns a failure, the property value is + * not updated. + * + * @param validator + * the validator to add, not null + * @return this binding, for chaining + * @throws IllegalStateException + * if {@code bind} has already been called + */ + public Binding<T, V> withValidator(Validator<? super V> validator); + + /** + * A convenience method to add a validator to this binding using the + * {@link Validator#from(Predicate, String)} factory method. + * <p> + * Validators are applied, in registration order, when the field value + * is saved to the backing property. If any validator returns a failure, + * the property value is not updated. + * + * @see #withValidator(Validator) + * @see Validator#from(Predicate, String) + * + * @param predicate + * the predicate performing validation, not null + * @param message + * the error message to report in case validation failure + * @return this binding, for chaining + * @throws IllegalStateException + * if {@code bind} has already been called + */ + public Binding<T, V> withValidator(Predicate<? super V> predicate, + String message); } /** @@ -121,6 +163,8 @@ public class Binder<T> implements Serializable { private Function<T, V> getter; private BiConsumer<T, V> setter; + private List<Validator<? super V>> validators = new ArrayList<>(); + /** * Creates a new binding associated with the given field. * @@ -144,12 +188,35 @@ public class Binder<T> implements Serializable { } } + @Override + public Binding<T, V> withValidator(Validator<? super V> validator) { + checkUnbound(); + Objects.requireNonNull(validator, "validator cannot be null"); + validators.add(validator); + return this; + } + + @Override + public Binding<T, V> withValidator(Predicate<? super V> predicate, + String message) { + return withValidator(Validator.from(predicate, message)); + } + private void bind(T bean) { setFieldValue(bean); onValueChange = field .addValueChangeListener(e -> storeFieldValue(bean)); } + private List<ValidationError<V>> validate() { + return validators.stream() + .map(validator -> validator.apply(field.getValue())) + .filter(Result::isError) + .map(result -> new ValidationError<>(field, + result.getMessage().orElse(null))) + .collect(Collectors.toList()); + } + private void unbind() { onValueChange.remove(); } @@ -186,6 +253,7 @@ public class Binder<T> implements Serializable { "cannot modify binding: already bound to a property"); } } + } private T bean; @@ -287,13 +355,34 @@ public class Binder<T> implements Serializable { } /** + * Validates the values of all bound fields and returns the result of the + * validation as a set of validation errors. + * <p> + * Validation is successful if the resulting set is empty. + * + * @return the validation result. + */ + public List<ValidationError<?>> validate() { + List<ValidationError<?>> resultErrors = new ArrayList<>(); + for (BindingImpl<?> binding : bindings) { + clearError(binding.field); + List<? extends ValidationError<?>> errors = binding.validate(); + resultErrors.addAll(errors); + if (!errors.isEmpty()) { + handleError(binding.field, errors.get(0).getMessage()); + } + } + return resultErrors; + } + + /** * Unbinds the currently bound bean if any. If there is no bound bean, does * nothing. */ public void unbind() { if (bean != null) { bean = null; - bindings.forEach(b -> b.unbind()); + bindings.forEach(BindingImpl::unbind); } } @@ -308,7 +397,10 @@ public class Binder<T> implements Serializable { */ public void load(T bean) { Objects.requireNonNull(bean, "bean cannot be null"); - bindings.forEach(binding -> binding.setFieldValue(bean)); + bindings.forEach( + + binding -> binding.setFieldValue(bean)); + } /** @@ -323,7 +415,10 @@ public class Binder<T> implements Serializable { */ public void save(T bean) { Objects.requireNonNull(bean, "bean cannot be null"); - bindings.forEach(binding -> binding.storeFieldValue(bean)); + bindings.forEach( + + binding -> binding.storeFieldValue(bean)); + } /** @@ -341,4 +436,36 @@ public class Binder<T> implements Serializable { return b; } + /** + * Clears the error condition of the given field, if any. The default + * implementation clears the + * {@link AbstractComponent#setComponentError(ErrorMessage) component error} + * of the field if it is a Component, otherwise does nothing. + * + * @param field + * the field with an invalid value + */ + protected void clearError(HasValue<?> field) { + if (field instanceof AbstractComponent) { + ((AbstractComponent) field).setComponentError(null); + } + } + + /** + * Handles a validation error emitted when trying to save the value of the + * given field. The default implementation sets the + * {@link AbstractComponent#setComponentError(ErrorMessage) component error} + * of the field if it is a Component, otherwise does nothing. + * + * @param field + * the field with the invalid value + * @param error + * the error message to set + */ + protected void handleError(HasValue<?> field, String error) { + if (field instanceof AbstractComponent) { + ((AbstractComponent) field).setComponentError(new UserError(error)); + } + } + } diff --git a/server/src/main/java/com/vaadin/data/Result.java b/server/src/main/java/com/vaadin/data/Result.java new file mode 100644 index 0000000000..0d6ffad94e --- /dev/null +++ b/server/src/main/java/com/vaadin/data/Result.java @@ -0,0 +1,170 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.data; + +import java.io.Serializable; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Represents the result of an operation that might fail, such as input + * validation or type conversion. A result may contain either a value, + * signifying a successful operation, or an error message in case of a failure. + * <p> + * Result instances are created using the factory methods {@link #ok(R)} and + * {@link #error(String)}, denoting success and failure respectively. + * <p> + * Unless otherwise specified, {@code Result} method arguments cannot be null. + * + * @param <R> + * the result value type + */ +public interface Result<R> extends Serializable { + + /** + * Returns a successful result wrapping the given value. + * + * @param <R> + * the result value type + * @param value + * the result value, can be null + * @return a successful result + */ + public static <R> Result<R> ok(R value) { + return new SimpleResult<>(value, null); + } + + /** + * Returns a failure result wrapping the given error message. + * + * @param <R> + * the result value type + * @param message + * the error message + * @return a failure result + */ + public static <R> Result<R> error(String message) { + Objects.requireNonNull(message, "message cannot be null"); + return new SimpleResult<R>(null, message); + } + + /** + * Returns a Result representing the result of invoking the given supplier. + * If the supplier returns a value, returns a {@code Result.ok} of the + * value; if an exception is thrown, returns the message in a + * {@code Result.error}. + * + * @param <R> + * the result value type + * @param supplier + * the supplier to run + * @param onError + * the function to provide the error message + * @return the result of invoking the supplier + */ + public static <R> Result<R> of(Supplier<R> supplier, + Function<Exception, String> onError) { + Objects.requireNonNull(supplier, "supplier cannot be null"); + Objects.requireNonNull(onError, "onError cannot be null"); + + try { + return ok(supplier.get()); + } catch (Exception e) { + return error(onError.apply(e)); + } + } + + /** + * If this Result has a value, returns a Result of applying the given + * function to the value. Otherwise, returns a Result bearing the same error + * as this one. Note that any exceptions thrown by the mapping function are + * not wrapped but allowed to propagate. + * + * @param <S> + * the type of the mapped value + * @param mapper + * the mapping function + * @return the mapped result + */ + public default <S> Result<S> map(Function<R, S> mapper) { + return flatMap(value -> ok(mapper.apply(value))); + } + + /** + * If this Result has a value, applies the given Result-returning function + * to the value. Otherwise, returns a Result bearing the same error as this + * one. Note that any exceptions thrown by the mapping function are not + * wrapped but allowed to propagate. + * + * @param <S> + * the type of the mapped value + * @param mapper + * the mapping function + * @return the mapped result + */ + public <S> Result<S> flatMap(Function<R, Result<S>> mapper); + + /** + * Invokes either the first callback or the second one, depending on whether + * this Result denotes a success or a failure, respectively. + * + * @param ifOk + * the function to call if success + * @param ifError + * the function to call if failure + */ + public void handle(Consumer<R> ifOk, Consumer<String> ifError); + + /** + * Applies the {@code consumer} if result is not an error. + * + * @param consumer + * consumer to apply in case it's not an error + */ + public default void ifOk(Consumer<R> consumer) { + handle(consumer, error -> { + }); + } + + /** + * Applies the {@code consumer} if result is an error. + * + * @param consumer + * consumer to apply in case it's an error + */ + public default void ifError(Consumer<String> consumer) { + handle(value -> { + }, consumer); + } + + /** + * Returns {@code true} if result is an error. + * + * @return whether the result is an error + */ + public boolean isError(); + + /** + * Returns an Optional of the result message, or an empty Optional if none. + * + * @return the optional message + */ + public Optional<String> getMessage(); +} diff --git a/server/src/main/java/com/vaadin/data/SimpleResult.java b/server/src/main/java/com/vaadin/data/SimpleResult.java new file mode 100644 index 0000000000..75e9cbef12 --- /dev/null +++ b/server/src/main/java/com/vaadin/data/SimpleResult.java @@ -0,0 +1,96 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * An internal implementation of {@code Result}. + * + * @param <R> + * the result value type + */ +class SimpleResult<R> implements Result<R> { + + private final R value; + private final String message; + + /** + * Creates a new {@link Result} instance using {@code value} for a non error + * {@link Result} and {@code message} for an error {@link Result}. + * <p> + * If {@code message} is null then {@code value} is ignored and result is an + * error. + * + * @param value + * the value of the result, may be {@code null} + * @param message + * the error message of the result, may be {@code null} + */ + SimpleResult(R value, String message) { + // value != null => message == null + assert value == null + || message == null : "Message must be null if value is provided"; + this.value = value; + this.message = message; + } + + @Override + @SuppressWarnings("unchecked") + public <S> Result<S> flatMap(Function<R, Result<S>> mapper) { + Objects.requireNonNull(mapper, "mapper cannot be null"); + + if (isError()) { + // Safe cast; valueless + return (Result<S>) this; + } else { + return mapper.apply(value); + } + } + + @Override + public void handle(Consumer<R> ifOk, Consumer<String> ifError) { + Objects.requireNonNull(ifOk, "ifOk cannot be null"); + Objects.requireNonNull(ifError, "ifError cannot be null"); + if (isError()) { + ifError.accept(message); + } else { + ifOk.accept(value); + } + } + + @Override + public Optional<String> getMessage() { + return Optional.ofNullable(message); + } + + @Override + public boolean isError() { + return message != null; + } + + @Override + public String toString() { + if (isError()) { + return "error(" + message + ")"; + } else { + return "ok(" + value + ")"; + } + } +} diff --git a/server/src/main/java/com/vaadin/data/ValidationError.java b/server/src/main/java/com/vaadin/data/ValidationError.java new file mode 100644 index 0000000000..1aa8d2b9a8 --- /dev/null +++ b/server/src/main/java/com/vaadin/data/ValidationError.java @@ -0,0 +1,68 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data; + +import java.io.Serializable; +import java.util.Objects; + +/** + * Represents a validation error. An error contains a reference to a field whose + * value is invalid and a message describing a validation failure. + * + * @author Vaadin Ltd + * @since 8.0 + * + * @param <V> + * the field value type + */ +public class ValidationError<V> implements Serializable { + + private HasValue<V> field; + private String message; + + /** + * Creates a new instance of ValidationError with provided validated field + * and error message. + * + * @param field + * the validated field + * @param message + * the validation error message, not {@code null} + */ + public ValidationError(HasValue<V> field, String message) { + Objects.requireNonNull(message, "message cannot be null"); + this.field = field; + this.message = message; + } + + /** + * Returns a reference to the validated field. + * + * @return the validated field + */ + public HasValue<V> getField() { + return field; + } + + /** + * Returns a validation error message. + * + * @return the validation error message + */ + public String getMessage() { + return message; + } +} diff --git a/server/src/main/java/com/vaadin/data/Validator.java b/server/src/main/java/com/vaadin/data/Validator.java new file mode 100644 index 0000000000..28dabca7e7 --- /dev/null +++ b/server/src/main/java/com/vaadin/data/Validator.java @@ -0,0 +1,136 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.data; + +import java.io.Serializable; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * A functional interface for validating user input or other potentially invalid + * data. When a validator instance is applied to a value of the corresponding + * type, it returns a <i>result</i> signifying that the value either passed or + * failed the validation. + * <p> + * For instance, the following validator checks if a number is positive: + * + * <pre> + * Validator<Integer> v = num -> { + * if (num >= 0) + * return Result.ok(num); + * else + * return Result.error("number must be positive"); + * }; + * </pre> + * + * @author Vaadin Ltd. + * + * @param <T> + * the type of the value to validate + * + * @see Result + */ +@FunctionalInterface +public interface Validator<T> extends Function<T, Result<T>>, Serializable { + + /** + * Returns a validator that chains this validator with the given function. + * Specifically, the function may be another validator. The resulting + * validator first applies this validator, and if the value passes, then the + * given validator. + * <p> + * For instance, the following chained validator checks if a number is + * between 0 and 10, inclusive: + * + * <pre> + * Validator<Integer> v = Validator.from(num -> num >= 0, "number must be >= 0") + * .chain(Validator.from(num -> num <= 10, "number must be <= 10")); + * </pre> + * + * @param next + * the validator to apply next, not null + * @return a chained validator + * + * @see #from(Predicate, String) + */ + public default Validator<T> chain(Function<T, Result<T>> next) { + Objects.requireNonNull(next, "next cannot be null"); + return val -> apply(val).flatMap(next); + } + + /** + * Validates the given value. Returns a {@code Result} instance representing + * the outcome of the validation. + * + * @param value + * the input value to validate + * @return the validation result + */ + @Override + public Result<T> apply(T value); + + /** + * Returns a validator that passes any value. + * + * @param <T> + * the value type + * @return an always-passing validator + */ + public static <T> Validator<T> alwaysPass() { + return v -> Result.ok(v); + } + + /** + * Builds a validator out of a conditional function and an error message. If + * the function returns true, the validator returns {@code Result.ok()}; if + * it returns false or throws an exception, {@code Result.error()} is + * returned with the given message. + * <p> + * For instance, the following validator checks if a number is between 0 and + * 10, inclusive: + * + * <pre> + * Validator<Integer> v = Validator.from(num -> num >= 0 && num <= 10, + * "number must be between 0 and 10"); + * </pre> + * + * @param <T> + * the value type + * @param guard + * the function used to validate, not null + * @param errorMessage + * the message returned if validation fails, not null + * @return the new validator using the function + */ + public static <T> Validator<T> from(Predicate<T> guard, + String errorMessage) { + Objects.requireNonNull(guard, "guard cannot be null"); + Objects.requireNonNull(errorMessage, "errorMessage cannot be null"); + return value -> { + try { + if (guard.test(value)) { + return Result.ok(value); + } else { + return Result.error(errorMessage); + } + } catch (Exception e) { + return Result.error(errorMessage); + } + }; + } +} diff --git a/server/src/test/java/com/vaadin/data/BinderTest.java b/server/src/test/java/com/vaadin/data/BinderTest.java index 2e284ca292..c3193603ef 100644 --- a/server/src/test/java/com/vaadin/data/BinderTest.java +++ b/server/src/test/java/com/vaadin/data/BinderTest.java @@ -4,10 +4,18 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import com.vaadin.data.Binder.Binding; +import com.vaadin.server.AbstractErrorMessage; +import com.vaadin.server.ErrorMessage; +import com.vaadin.server.UserError; import com.vaadin.tests.data.bean.Person; import com.vaadin.ui.AbstractField; @@ -189,6 +197,66 @@ public class BinderTest { Assert.assertEquals("", nameField.getValue()); } + @Test + public void validate_notBound_noErrors() { + Binder<Person> binder = new Binder<>(); + + List<ValidationError<?>> errors = binder.validate(); + + Assert.assertTrue(errors.isEmpty()); + } + + @Test + public void bound_validatorsAreOK_noErrors() { + Binder<Person> binder = new Binder<>(); + Binding<Person, String> binding = binder.forField(nameField); + binding.withValidator(Validator.alwaysPass()).bind(Person::getFirstName, + Person::setFirstName); + + nameField.setComponentError(new UserError("")); + List<ValidationError<?>> errors = binder.validate(); + + Assert.assertTrue(errors.isEmpty()); + Assert.assertNull(nameField.getComponentError()); + } + + @SuppressWarnings("serial") + @Test + public void bound_validatorsFail_errors() { + Binder<Person> binder = new Binder<>(); + Binding<Person, String> binding = binder.forField(nameField); + binding.withValidator(Validator.alwaysPass()); + String msg1 = "foo"; + String msg2 = "bar"; + binding.withValidator(new Validator<String>() { + @Override + public Result<String> apply(String value) { + return new SimpleResult<>(null, msg1); + } + }); + binding.withValidator(value -> false, msg2); + binding.bind(Person::getFirstName, Person::setFirstName); + + List<ValidationError<?>> errors = binder.validate(); + + Assert.assertEquals(2, errors.size()); + + Set<String> errorMessages = errors.stream() + .map(ValidationError::getMessage).collect(Collectors.toSet()); + Assert.assertTrue(errorMessages.contains(msg1)); + Assert.assertTrue(errorMessages.contains(msg2)); + + Set<?> fields = errors.stream().map(ValidationError::getField) + .collect(Collectors.toSet()); + Assert.assertEquals(1, fields.size()); + Assert.assertTrue(fields.contains(nameField)); + + ErrorMessage componentError = nameField.getComponentError(); + Assert.assertNotNull(componentError); + Assert.assertEquals("foo", + ((AbstractErrorMessage) componentError).getMessage()); + } + private void bindName() { binder.bind(nameField, Person::getFirstName, Person::setFirstName); binder.bind(p); diff --git a/server/src/test/java/com/vaadin/data/ResultTest.java b/server/src/test/java/com/vaadin/data/ResultTest.java new file mode 100644 index 0000000000..c01daa0123 --- /dev/null +++ b/server/src/test/java/com/vaadin/data/ResultTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data; + +import java.util.function.Function; + +import org.junit.Assert; +import org.junit.Test; + +/** + * @author Vaadin Ltd + * + */ +public class ResultTest { + + @Test + public void testOk() { + String value = "foo"; + Result<String> ok = Result.ok(value); + Assert.assertFalse(ok.isError()); + Assert.assertFalse(ok.getMessage().isPresent()); + ok.ifOk(v -> Assert.assertEquals(value, v)); + ok.ifError(msg -> Assert.fail()); + } + + @Test + public void testError() { + String message = "foo"; + Result<String> error = Result.error(message); + Assert.assertTrue(error.isError()); + Assert.assertTrue(error.getMessage().isPresent()); + error.ifOk(v -> Assert.fail()); + error.ifError(msg -> Assert.assertEquals(message, msg)); + Assert.assertEquals(message, error.getMessage().get()); + } + + @Test + public void of_noException() { + Result<String> result = Result.of(() -> "", exception -> null); + Assert.assertTrue(result instanceof SimpleResult); + Assert.assertFalse(((SimpleResult<?>) result).isError()); + } + + @Test + public void of_exception() { + String message = "foo"; + Result<String> result = Result.<String> of(() -> { + throw new RuntimeException(); + }, exception -> message); + Assert.assertTrue(result instanceof SimpleResult); + Assert.assertTrue(((SimpleResult<?>) result).isError()); + Assert.assertEquals(message, result.getMessage().get()); + } + + @SuppressWarnings("serial") + @Test + public void map_norError_mapperIsApplied() { + Result<String> result = new SimpleResult<String>("foo", null) { + + @Override + public <S> Result<S> flatMap(Function<String, Result<S>> mapper) { + return mapper.apply("foo"); + } + }; + Result<String> mapResult = result.map(value -> { + Assert.assertEquals("foo", value); + return "bar"; + }); + Assert.assertTrue(mapResult instanceof SimpleResult); + Assert.assertFalse(((SimpleResult<?>) mapResult).isError()); + mapResult.ifOk(v -> Assert.assertEquals("bar", v)); + } + + @SuppressWarnings("serial") + @Test + public void map_error_mapperIsApplied() { + Result<String> result = new SimpleResult<String>("foo", null) { + + @Override + public <S> Result<S> flatMap(Function<String, Result<S>> mapper) { + return new SimpleResult<S>(null, "bar"); + } + }; + Result<String> mapResult = result.map(value -> { + Assert.assertEquals("foo", value); + return "somevalue"; + }); + Assert.assertTrue(mapResult instanceof SimpleResult); + Assert.assertTrue(((SimpleResult<?>) mapResult).isError()); + mapResult.ifError(msg -> Assert.assertEquals("bar", msg)); + } +} diff --git a/server/src/test/java/com/vaadin/data/ValidatorTest.java b/server/src/test/java/com/vaadin/data/ValidatorTest.java new file mode 100644 index 0000000000..1a432c063c --- /dev/null +++ b/server/src/test/java/com/vaadin/data/ValidatorTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data; + +import java.util.Objects; + +import org.junit.Assert; +import org.junit.Test; + +/** + * @author Vaadin Ltd + * + */ +public class ValidatorTest { + + @Test + public void alwaysPass() { + Validator<String> alwaysPass = Validator.alwaysPass(); + Result<String> result = alwaysPass.apply("foo"); + Assert.assertTrue(result instanceof SimpleResult); + SimpleResult<String> implRes = (SimpleResult<String>) result; + Assert.assertFalse(implRes.getMessage().isPresent()); + } + + @Test + public void chain_alwaysPassAndError() { + Validator<String> alwaysPass = Validator.alwaysPass(); + Validator<String> chain = alwaysPass + .chain(value -> Result.error("foo")); + Result<String> result = chain.apply("bar"); + Assert.assertTrue(result.isError()); + Assert.assertEquals("foo", result.getMessage().get()); + } + + @SuppressWarnings("serial") + @Test + public void chain_mixture() { + Validator<String> first = new Validator<String>() { + + @Override + public Result<String> apply(String value) { + if (value == null) { + return Result.error("Cannot be null"); + } + return Result.ok(value); + } + }; + Validator<String> second = new Validator<String>() { + + @Override + public Result<String> apply(String value) { + if (value != null && value.isEmpty()) { + return Result.error("Cannot be empty"); + } + return Result.ok(value); + } + }; + + Validator<String> chain = first.chain(second); + Result<String> result = chain.apply("bar"); + Assert.assertFalse(result.isError()); + + result = chain.apply(null); + Assert.assertTrue(result.isError()); + + result = chain.apply(""); + Assert.assertTrue(result.isError()); + } + + @Test + public void from() { + Validator<String> validator = Validator.from(Objects::nonNull, + "Cannot be null"); + Result<String> result = validator.apply(null); + Assert.assertTrue(result.isError()); + + result = validator.apply(""); + Assert.assertFalse(result.isError()); + } +} |