"Helios was the personification of the Sun in Greek mythology. He is the son of the Titan Hyperion and the Titaness Theia (according to Hesiod), also known as Euryphaessa (in Homeric Hymn 31) and brother of the goddesses Selene, the moon, and Eos, the dawn" — https://en.wikipedia.org/wiki/Helios
1. Intro
Helios
is a micro validation library that focuses on:
-
API simplicity
-
Small footprint
-
Validator reusability
-
Embrace
functional programming
-
Documentation
Many Java validation libraries out there are based on annotations. Annotations are ok, but then validation depends on having your data model polluted with them. Moreover, sometimes you may want to have different validation restrictions on the same type of object in different parts of the application.
2. Usage
2.1. Dependency
If you’re using Gradle:
repositories {
maven {
url "http://dl.bintray.com/mariogarcia/maven"
}
}
compile 'com.github.mariogarcia:helios:0.1.0'
Replace 0.1.0 with the latest available version, which is
0.1.2
|
2.2. Simple property
When validating anything with Helios
you would be using the
helios.Helios#validate
method. The validate
method is overloaded
to be able to receive validators as a variable arguments or as a list,
we’ll see later on when to use one or another. The validate
method
parameters are:
-
A string identifying the property validated
-
The value to validate
-
A variable number of validators
The following example shows how to validate a simple property isolated.
package helios.samples.asciidoc;
import java.util.List;
import helios.Helios;
import helios.Validators;
import helios.ValidatorError;
public class SimpleProperty {
public List<ValidatorError> validateName(String name) {
return Helios.validate("name", (1)
name, (2)
Validators.required(), (3)
Validators.inRangeOfString(0,4));
}
}
1 | Helios.validate call. Also the first parameter is the identifier
of the validated property. |
2 | The value to validate. |
3 | Varargs of built-in validators. |
If you pass an invalid number to the validateName
method the
validation will return a list of helios.ValidatorError
. Every
ValidatorError
instance is immutable, so you can access to its
property
, key
and keyI18n
values directly, no getters needed.
In the example we’re using some built-int Helios
validators. Helios
has a little set of built-in validators you can
find in the class helios.ValidatorsUtil
class.
2.3. Grouping
One way of reusing validators is to group them in a list and use the
helios.Helios.validate
method that permits a list of validators:
package helios.samples.asciidoc;
import static helios.Helios.validate;
import static helios.Validators.matches;
import static helios.Validators.required;
import static helios.Validators.inRangeOfString;
import java.util.List;
import java.util.Arrays;
import helios.Validator;
import helios.ValidatorError;
public class Grouping {
private List<Validator<String>> VALIDATORS =
Arrays.asList(
required(),
inRangeOfString(0,4),
matches("j.*"));
public List<ValidatorError> validateName(String name) {
return validate("name", name, VALIDATORS);
}
public List<ValidatorError> validateOtherName(String other) {
return validate("other", other, VALIDATORS);
}
}
2.4. Nested
In a real application we’ll be probably be validating a full object,
not only single properties, this can be addressed in Helios
by nesting
calls to Helios#validate
inside lambda expressions.
public List<ValidatorError> validateProduct(final Product product) {
return validate("product", product,
(Product p) -> validate("desc", p.desc, getDescValidators()), (1)
(Product p) -> validate("items", p.items, getItemsValidators()), (2)
(Product p) -> validate("amount", p.amount, getAmountValidators())); (3)
}
1 | Validator to check "desc" property |
2 | Validator to check "items" property |
3 | Validator to check "amount" property |
These validators are just lambda expressions that comply with the
helios.Validator
interface. They receive a given value of the
required type and thanks to invoking validate
they return a list of
ValidatorError
which is the expected outcome for a Validator
.
The validateProduct method itself could be considered as a
validator of type Validator<Product> because it receives a Product
and returns a list of ValidatorError .
|
3. Validators
All validators are based on helios.Validator
interface. This
interface is a functional interface
and has one single method
validate
that receives a given value and may return a list of zero
or more errors.
Because Helios
try to keep its size to the minimum, there’re only a
set of basic built-in validators coming with the library. However
there’re plenty of other ways of creating your own validators.
3.1. Built-in
Although you can create you own validators, Helios
comes with a
basic set of validators for numbers, strings, and lists. Please
checkout the javadoc to see them
all.
3.2. Create your own
3.2.1. Validator
Of course you can implement helios.Validator
to create a new
validator type.
package helios.samples.asciidoc.validators;
import static helios.ValidatorError.error; (1)
import static helios.ValidatorError.errors;
import java.util.List;
import helios.Validator;
import helios.ValidatorError;
public class StartsWithJ implements Validator<String> { (2)
@Override
public List<ValidatorError> validate(String word) {
return word.startsWith("j") ?
errors() :
errors(error(word, "starts.with.j")); (3)
}
}
1 | Static methods will serve to produce ValidatorError instances easily |
2 | Implementing helios.Validator |
3 | Creating errors if its needed with a given error key and the value that produced the error |
3.2.2. Lambda
Because helios.Validator
is a functional interface you can create
new instances out of lambda expressions.
package helios.samples.asciidoc.validators;
import static helios.ValidatorError.error;
import static helios.ValidatorError.errors;
import static helios.Validators.required;
import helios.Validator;
import helios.Helios;
public class LambdaValidator {
public Validator<String> getCustomValidator() { (1)
return (String word) -> {
return word.startsWith("j") ?
errors() :
errors(error(word, "starts.with.j"));
};
}
public Validator<String> getRequiredStringValidator() { (2)
return (String word) -> Helios.validate("word", word, required());
}
}
1 | From a given boolean expression we could make use of the
ValidatorError errors and error methods to return a list of
ValidatorError instances |
2 | You can also use Helios#validate if you would like to reuse
pre-existent built-in validators or you may want to nest validators |
3.2.3. Method Reference
If you had a method that complies with the helios.Validator
contract, you could use it in any method where a Validator
is passed
as parameter.
package helios.samples.asciidoc.validators;
import static helios.Validators.required;
import static helios.Validators.inRangeOfString;
import static helios.ValidatorError.error;
import static helios.ValidatorError.errors;
import java.util.List;
import helios.Helios;
import helios.Validator;
import helios.ValidatorError;
public class MethodReference {
public List<ValidatorError> isSmallWord(final String word) {
return Helios.validate("word", word, this::stringValidator);
}
public List<ValidatorError> stringValidator(String word) {
return word != null && word.length() > 0 ?
errors() :
errors(error(word, "string.required"));
}
}
3.2.4. Compose
Apart from grouping validators in lists, you can also create a
validator out of any pre-existent validators with the method
helios.ValidatorsUtil.compose
. In fact this method is already used
in Helios
built-in validators:
public static Validator<String> inRangeOfString(int min, int max) {
return compose(minOfString(min), maxOfString(max));
}
3.2.5. As Predicate
At some point you may realize a validator or a set of validators could
be used as a java.util.function.Predicate
. A good example could be
to filter out a list of words:
package helios.samples.asciidoc.validators;
import static helios.Validators.matches;
import static helios.Validators.required;
import static helios.ValidatorsUtil.compose;
import java.util.List;
import java.util.Arrays;
import helios.Validator;
public class AsPredicate {
private final List<String> WORDS = Arrays.asList( (1)
"something",
"aloha",
"somewhere",
"some");
public Long countWordsStartingWithS() {
Validator<String> byCustom = compose(required(), matches("[Ss].*"));
return countWords(WORDS, byCustom); (2)
}
public Long countWords(final List<String> wordList, final Validator<String> filter) {
return wordList
.stream()
.filter(filter.toPredicate()) (3)
.count();
}
}
1 | A list of words we want to filter |
2 | Invoking a function receiving a Validator as parameter |
3 | Invoking Validator#toPredicate to convert a Validator to
Predicate and use it to filter out a stream of words |
4. ValidatorError
If any validator fails it will return a list of
helios.ValidatorError
instances. When a validator is created it
could have the following information:
-
property
: the validated property -
key
: the error identificator -
keyI18n
: it’s a combination of "$property.$key". It contains a unique identifier that can be used to translate the error.
You can create a new ValidatorError
either by calling its
constructor directly or using the method
ValidatorError#error
. Although this method was first created to be
used internally, it was pretty clear that it could be very useful to
developers when creating custom validators.
All instances of ValidatorError are efectively
immutable. All its fields are final and initialized in constructor. It
could be considered thread-safe friendly.
|
5. Development
5.1. Dependencies
Library | Description | Scope |
---|---|---|
org.slf4j:slf4j-api:1.7.21 |
serves as a simple facade or abstraction for various logging frameworks |
compile |
com.nagternal:spock-genesis:0.4.0 |
Spock Genesis provides a variety of classes that produce generators |
testCompile |
5.2. Github and Javadoc
-
Javadoc: can be found here.