diff options
author | Jon Marius Venstad <venstad@gmail.com> | 2022-03-30 13:16:29 +0200 |
---|---|---|
committer | Jon Marius Venstad <venstad@gmail.com> | 2022-03-30 13:16:29 +0200 |
commit | 2bc3dac20f6ec32f319bc850b6c1a8f28ecdfeed (patch) | |
tree | 8028a04fce5d9d6534bee9e72d68b0210bceb4f1 /vespajlib | |
parent | 9ab03c6161d2dd4469cfa5bdf3f81e3ca4dd71c5 (diff) |
draft
Diffstat (limited to 'vespajlib')
5 files changed, 271 insertions, 0 deletions
diff --git a/vespajlib/src/main/java/ai/vespa/validation/Hostname.java b/vespajlib/src/main/java/ai/vespa/validation/Hostname.java new file mode 100644 index 00000000000..87ef837492c --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/validation/Hostname.java @@ -0,0 +1,31 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.validation; + +import java.util.regex.Pattern; + +import static ai.vespa.validation.Validation.requireMatch; +import static ai.vespa.validation.Validation.validate; + +/** + * A valid hostname. + * + * @author jonmv + */ +public class Hostname extends StringWrapper<Hostname> { + + public static final Pattern hostnameLabel = Pattern.compile("([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])"); + public static final Pattern hostnamePattern = Pattern.compile("(?=.{1,255})(" + hostnameLabel + "\\.)*" + hostnameLabel); + + private Hostname(String value) { + super(value); + } + + public static Hostname of(String hostname) { + return new Hostname(validate(hostname, "hostname", requireMatch(hostnamePattern))); + } + + public static String requireLabel(String label) { + return validate(label, "hostname label", requireMatch(hostnameLabel)); + } + +} diff --git a/vespajlib/src/main/java/ai/vespa/validation/Name.java b/vespajlib/src/main/java/ai/vespa/validation/Name.java new file mode 100644 index 00000000000..1869ba734b3 --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/validation/Name.java @@ -0,0 +1,33 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.validation; + +import java.util.regex.Pattern; + +import static ai.vespa.validation.Validation.requireMatch; +import static ai.vespa.validation.Validation.validate; + +/** + * A name is a non-null, non-blank {@link String} which starts with a letter, and has up to + * 254 more characters which may be letters, numbers, dashes or underscores. + * + * Prefer domain-specific wrappers over this class, but prefer this over raw strings when possible. gT + * + * @author jonmv + */ +public class Name extends StringWrapper<Name> { + + public static final Pattern namePattern = Pattern.compile("[A-Za-z][A-Za-z0-9_-]{0,254}"); + + private Name(String value) { + super(value); + } + + public static Name of(String value) { + return of(value, "name"); + }; + + public static Name of(String value, String description) { + return new Name(validate(value, description, requireMatch(namePattern))); + }; + +} diff --git a/vespajlib/src/main/java/ai/vespa/validation/StringWrapper.java b/vespajlib/src/main/java/ai/vespa/validation/StringWrapper.java new file mode 100644 index 00000000000..258be824ef5 --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/validation/StringWrapper.java @@ -0,0 +1,45 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.validation; + +import static java.util.Objects.requireNonNull; + +/** + * Abstract wrapper for glorified strings, to ease adding new such wrappers. + * + * @param <T> child type + * + * @author jonmv + */ +public abstract class StringWrapper<T extends StringWrapper<T>> implements Comparable<T> { + + private final String value; + + protected StringWrapper(String value) { + this.value = requireNonNull(value); + } + + public final String value() { return value; } + + @Override + public int compareTo(T other) { + return value().compareTo(other.value()); + } + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return value.equals(((StringWrapper<?>) o).value); + } + + @Override + public final int hashCode() { + return value.hashCode(); + } + + @Override + public String toString() { + return value; + } + +} diff --git a/vespajlib/src/main/java/ai/vespa/validation/Validation.java b/vespajlib/src/main/java/ai/vespa/validation/Validation.java new file mode 100644 index 00000000000..f02096c478b --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/validation/Validation.java @@ -0,0 +1,88 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.validation; + +import com.yahoo.yolean.Exceptions; + +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +import static java.util.Objects.requireNonNull; + +/** + * Every {@link String} is a security risk! + * This class has utility methods for validating strings, which are often user input. + * + * @author jonmv + */ +public class Validation { + + private Validation() { } + + /** Parses then given string, and then validates that each of the requirements are true for the parsed value. */ + @SafeVarargs + @SuppressWarnings("varargs") + public static <T> T validate(String value, Function<String, T> parser, String description, Predicate<? super T>... requirements) { + T parsed; + try { + parsed = parser.apply(requireNonNull(value, description + " cannot be null")); + } + catch (RuntimeException e) { + throw new IllegalArgumentException("failed parsing " + description + + " '" + value + "': " + Exceptions.toMessageString(e)); + } + return validate(parsed, description, requirements); + } + + /** Validates that each of the requirements are true for the given argument. */ + @SafeVarargs + @SuppressWarnings("varargs") + public static <T> T validate(T value, String description, Predicate<? super T>... requirements) { + for (Predicate<? super T> requirement : requirements) + if ( ! requirement.test(value)) + throw new IllegalArgumentException(description + " " + requirement + ", but got: '" + value + "'"); + + return value; + } + + /** Requires arguments to match the given pattern. */ + public static Predicate<String> requireMatch(Pattern pattern) { + return require(s -> pattern.matcher(s).matches(), "must match '" + pattern + "'"); + } + + /** Requires arguments to be non-blank. */ + public static Predicate<String> requireNonBlank() { + return require(s -> ! s.isBlank(), "cannot be blank"); + } + + /** Requires arguments to be at least the lower bound. */ + public static <T extends Comparable<? super T>> Predicate<T> requireAtLeast(T lower) { + requireNonNull(lower, "lower bound cannot be null"); + return require(c -> lower.compareTo(c) <= 0, "must be at least '" + lower + "'"); + } + + /** Requires arguments to be at most the upper bound. */ + public static <T extends Comparable<? super T>> Predicate<T> requireAtMost(T upper) { + requireNonNull(upper, "upper bound cannot be null"); + return require(c -> upper.compareTo(c) >= 0, "must be at most '" + upper + "'"); + } + + /** Requires arguments to be at least the lower bound, and at most the upper bound. */ + public static <T extends Comparable<? super T>> Predicate<T> requireInRange(T lower, T upper) { + requireNonNull(lower, "lower bound cannot be null"); + requireNonNull(upper, "upper bound cannot be null"); + if (lower.compareTo(upper) > 0) throw new IllegalArgumentException("lower bound cannot be greater than upper bound, " + + "but got '" + lower + "' > '" + upper + "'"); + return require(c -> lower.compareTo(c) <= 0 && upper.compareTo(c) >= 0, + "must be at least '" + lower + "' and at most '" + upper + "'"); + } + + /** Wraps a predicate with a message describing it. */ + public static <T> Predicate<T> require(Predicate<T> predicate, String message) { + return new Predicate<T>() { + @Override public boolean test(T t) { return predicate.test(t); } + @Override public String toString() { return message; } + }; + } + +}
\ No newline at end of file diff --git a/vespajlib/src/test/java/ai/vespa/validation/ValidationTest.java b/vespajlib/src/test/java/ai/vespa/validation/ValidationTest.java new file mode 100644 index 00000000000..c3b0730a5ee --- /dev/null +++ b/vespajlib/src/test/java/ai/vespa/validation/ValidationTest.java @@ -0,0 +1,74 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.validation; + +import com.google.common.base.Preconditions; +import org.junit.jupiter.api.Test; + +import static ai.vespa.validation.Validation.require; +import static ai.vespa.validation.Validation.requireAtLeast; +import static ai.vespa.validation.Validation.requireAtMost; +import static ai.vespa.validation.Validation.requireInRange; +import static ai.vespa.validation.Validation.requireNonBlank; +import static ai.vespa.validation.Validation.validate; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * @author jonmv + */ +class ValidationTest { + + @Test + void testNumberComparisons() { + assertEquals(3.14, validate("3.14", Double::parseDouble, "pi", + requireInRange(3.14, 3.14), + requireInRange(0.0, Double.POSITIVE_INFINITY), + requireInRange(Double.NEGATIVE_INFINITY, 3.14), + requireAtLeast(3.14), + requireAtMost(3.14))); + + assertEquals("lower bound cannot be greater than upper bound, but got '1.0' > '0.1'", + assertThrows(IllegalArgumentException.class, + () -> validate(3.14, "pi", requireInRange(1.0, 0.1))) + .getMessage()); + + assertEquals("pi must be at least '0.0' and at most '0.0', but got: '3.14'", + assertThrows(IllegalArgumentException.class, + () -> validate(3.14, "pi", requireInRange(0.0, 0.0))) + .getMessage()); + + assertEquals("pi must be at least '4.0' and at most '4.0', but got: '3.14'", + assertThrows(IllegalArgumentException.class, + () -> validate(3.14, "pi", requireInRange(4.0, 4.0))) + .getMessage()); + + assertEquals("pi must be at least '4.0', but got: '3.14'", + assertThrows(IllegalArgumentException.class, + () -> validate(3.14, "pi", requireAtLeast(4.0))) + .getMessage()); + + assertEquals("pi must be at most '3.0', but got: '3.14'", + assertThrows(IllegalArgumentException.class, + () -> validate(3.14, "pi", requireAtMost(3.0))) + .getMessage()); + } + + @Test + void testStringComparisons() { + assertEquals("hei", validate("hei", "word", + requireNonBlank(), + require(__ -> true, "nothing"), + requireInRange("hai", "hoi"))); + + assertEquals("word cannot be blank, but got: ''", + assertThrows(IllegalArgumentException.class, + () -> validate("", "word", requireNonBlank())) + .getMessage()); + + assertEquals("lower bound cannot be greater than upper bound, but got 'hoi' > 'hai'", + assertThrows(IllegalArgumentException.class, + () -> validate("hei", "word", requireInRange("hoi", "hai"))) + .getMessage()); + } + +} |