Created
November 3, 2016 11:36
-
-
Save io7m/8b8bf4c208e9a067a87786e8d8499097 to your computer and use it in GitHub Desktop.
Validation example using a mix of single-error Validations and list-of-error Validations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import javaslang.Function1; | |
import javaslang.collection.List; | |
import javaslang.control.Validation; | |
import java.util.Properties; | |
import static javaslang.control.Validation.invalid; | |
import static javaslang.control.Validation.valid; | |
/** | |
* An example to demonstrate what tends to happen when mixing {@link Validation} | |
* values that use both single error values and lists of error values. | |
*/ | |
public final class ValidMix | |
{ | |
private ValidMix() | |
{ | |
} | |
static class Error | |
{ | |
final String message; | |
Error(final String message) | |
{ | |
this.message = message; | |
} | |
} | |
/** | |
* Attempt to parse an integer. It makes sense for this to return a | |
* Validation with a single error value, as the function attempts to | |
* parse a scalar and there's really only one thing that can go wrong. | |
*/ | |
private static Validation<Error, ? extends Integer> parseInteger( | |
final String text) | |
{ | |
try { | |
return valid(Integer.valueOf(Integer.parseInt(text))); | |
} catch (final NumberFormatException e) { | |
return invalid(new Error(e.getMessage())); | |
} | |
} | |
/** | |
* Attempt to parse a string value from a key. It makes sense for this to | |
* return a Validation with a single error value for the same reasons as | |
* {@link #parseInteger(String)}. | |
*/ | |
private static Validation<Error, String> parseStringKey( | |
final Properties p, | |
final String key) | |
{ | |
if (p.containsKey(key)) { | |
return valid(p.getProperty(key)); | |
} | |
return invalid(new Error("No such key: " + key)); | |
} | |
/** | |
* Attempt to parse an integer value from a key. It makes sense for this to | |
* return a Validation with a single error value for the same reasons as | |
* {@link #parseInteger(String)}. | |
*/ | |
private static Validation<Error, Integer> parseIntegerKey( | |
final Properties p, | |
final String key) | |
{ | |
return parseStringKey(p, key).flatMap(ValidMix::parseInteger); | |
} | |
/** | |
* Attempt to parse a list of integer values from a key. It makes sense for | |
* this to return a Validation with a list of error values, as each individual | |
* integer value may fail to parse. | |
*/ | |
private static Validation<List<Error>, List<Integer>> parseListInteger( | |
final Properties p, | |
final String key) | |
{ | |
/* | |
* Note the use of `unflatten` to get from a single error to a list of | |
* errors... | |
*/ | |
return unflatten(parseStringKey(p, key)).flatMap( | |
text -> { | |
final List<Validation<Error, ? extends Integer>> integers = | |
List.of(text.split("\\s+")) | |
.map(Function1.of(String::trim) | |
.andThen(Function1.of(ValidMix::parseInteger))); | |
final List<Validation<Error, ? extends Integer>> invalids = | |
integers.filter(Validation::isInvalid); | |
if (!invalids.isEmpty()) { | |
return invalid(integers.map(Validation::getError)); | |
} | |
return valid(integers.map(Validation::get)); | |
}); | |
} | |
/** | |
* Attempt to parse a list of string values from a key. It makes sense for | |
* this to return a Validation with a list of error values, as each individual | |
* string value may fail to parse. | |
*/ | |
private static Validation<List<Error>, List<String>> parseListString( | |
final Properties p, | |
final String key) | |
{ | |
return unflatten(parseStringKey(p, key)).flatMap( | |
text -> { | |
final List<Validation<Error, String>> strings = | |
List.of(text.split("\\s+")) | |
.map(Function1.of(String::trim).andThen( | |
segment -> { | |
if (segment.isEmpty()) { | |
return invalid(new Error("Empty string")); | |
} | |
return valid(segment); | |
})); | |
final List<Validation<Error, String>> invalids = | |
strings.filter(Validation::isInvalid); | |
if (!invalids.isEmpty()) { | |
return invalid(strings.map(Validation::getError)); | |
} | |
return valid(strings.map(Validation::get)); | |
}); | |
} | |
static class Data | |
{ | |
Integer x; | |
Integer y; | |
List<Integer> zs; | |
String a; | |
String b; | |
List<String> cs; | |
public Data( | |
final Integer x, | |
final Integer y, | |
final List<Integer> zs, | |
final String a, | |
final String b, | |
final List<String> cs) | |
{ | |
this.x = x; | |
this.y = y; | |
this.zs = zs; | |
this.a = a; | |
this.b = b; | |
this.cs = cs; | |
} | |
} | |
/** | |
* Parse a configuration. Note that now we have a mix of Validation values | |
* that have single Error values and lists of Error values. The {@link | |
* Validation#ap(Validation)} function will yield {@code | |
* Validation<List<List<Error>>, Data>} and must be flattened down to {@code | |
* Validation<List<Error>>, Data>}. | |
*/ | |
static Validation<List<Error>, Data> parseData( | |
final Properties p) | |
{ | |
return flatten(Validation.combine( | |
unflatten(parseIntegerKey(p, "x")), | |
unflatten(parseIntegerKey(p, "y")), | |
parseListInteger(p, "zs"), | |
unflatten(parseStringKey(p, "a")), | |
unflatten(parseStringKey(p, "b")), | |
parseListString(p, "cs")).ap(Data::new)); | |
} | |
private static <E, T> Validation<List<E>, T> flatten( | |
final Validation<List<List<E>>, T> v) | |
{ | |
return v.leftMap(xs -> xs.fold(List.empty(), List::appendAll)); | |
} | |
private static <E, T> Validation<List<E>, T> unflatten( | |
final Validation<E, T> v) | |
{ | |
return v.leftMap(List::of); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment