org.openrewrite.Validated Maven / Gradle / Ivy
Show all versions of rewrite-core Show documentation
/*
* Copyright 2020 the original author or authors.
*
* 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
*
* https://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 org.openrewrite;
import org.jspecify.annotations.Nullable;
import org.openrewrite.internal.StringUtils;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static java.util.stream.StreamSupport.stream;
/**
* A container object which may or may not contain a valid value.
* If a value is valid, {@link #isValid()} returns {@code true}.
* If the value is invalid, the object is considered invalid and
* {@link #isValid()} returns {@code false}.
*
* @param the type of value being validated
*/
public interface Validated extends Iterable> {
boolean isValid();
default boolean isInvalid() {
return !isValid();
}
default List> failures() {
List> list = new ArrayList<>();
for (Validated v : this) {
if (v.isInvalid()) {
list.add((Invalid) v);
}
}
return list;
}
@SuppressWarnings("unused")
static Secret validSecret(String property, String value) {
return new Secret(property, value);
}
static None none() {
return new None<>();
}
static Valid valid(String property, @Nullable T value) {
return new Valid<>(property, value);
}
/**
* Validate that the Predicate will evaluate to 'true' on the supplied value.
* When the Predicate evaluates to 'false' the error message will be of the form:
*
* "[property] was '[value]' but it [message]"
*
* @param property The property name to test
* @param message The failure message if the test doesn't pass
* @param value The value of the property
* @param test The test predicate
* @param The property value type.
* @return A validation result
*/
static Validated test(String property, String message, @Nullable T value, Predicate test) {
return test.test(value) ?
valid(property, value) :
invalid(property, value, message.replace("{}", value == null ? "null" : value.toString()));
}
/**
* Validate that the Predicate will evaluate to 'true' on the supplied value.
* Will return a {@link None} if the value is valid.
*
* This allows validation of a value that is not the intended return value.
*
* When the Predicate evaluates to 'false' the error message will be of the form:
*
* "[property] was '[value]' but it [message]"
*
* @param property The property name to test
* @param message The failure message if the test doesn't pass
* @param value The value of the property
* @param test The test predicate
* @param The property value type.
* @return A validation result
*/
static Validated testNone(String property, String message, @Nullable V value, Predicate test) {
return test.test(value) ?
new None<>() :
invalid(property, value, message.replace("{}", value == null ? "null" : value.toString()));
}
static Validated required(String property, @Nullable T value) {
return value != null ?
valid(property, value) :
missing(property, null, "is required");
}
static Validated notBlank(String property, @Nullable String value) {
return test(property, "must not be blank", value, s -> value != null && !StringUtils.isBlank(value));
}
static Missing missing(String property, @Nullable T value, String message) {
return new Missing<>(property, value, message);
}
static Invalid invalid(String property, @Nullable Object value, String message) {
return invalid(property, value, message, null);
}
static Invalid invalid(String property, @Nullable Object value, String message,
@Nullable Throwable exception) {
return new Invalid<>(property, value, message, exception);
}
@SuppressWarnings("unchecked")
default Validated and(Validated extends T> validated) {
if (validated instanceof None) {
return this;
}
return new Both<>(this, (Validated) validated);
}
@SuppressWarnings("unchecked")
default Validated or(Validated extends T> validated) {
if (validated instanceof None) {
return this;
}
return new Either<>(this, (Validated) validated);
}
@Nullable T getValue();
/**
* Indicates that no validation has occurred. None is considered "valid", effectively a no-op validation.
*/
class None implements Validated {
@Override
public boolean isValid() {
return true;
}
@Override
public Iterator> iterator() {
return Collections.emptyIterator();
}
@Override
public T getValue() {
throw new IllegalStateException("Value does not exist");
}
@Override
public Validated or(Validated extends T> validated) {
if (validated instanceof None) {
return this;
}
//noinspection unchecked
return (Validated) validated;
}
@Override
public Validated and(Validated extends T> validated) {
if (validated instanceof None) {
return this;
}
//noinspection unchecked
return (Validated) validated;
}
}
/**
* A specialization {@link Valid} that won't print the secret in plain text if the validation is serialized.
*/
class Secret extends Valid {
public Secret(String property, String value) {
super(property, value);
}
@Override
public String toString() {
return "Secret{" +
"property='" + property + '\'' +
'}';
}
}
/**
* A valid property value.
*/
class Valid implements Validated {
protected final String property;
@Nullable
private final T value;
public Valid(String property, @Nullable T value) {
this.property = property;
this.value = value;
}
@Override
public boolean isValid() {
return true;
}
@Override
public Iterator> iterator() {
return Stream.of((Validated) this).iterator();
}
public String getProperty() {
return property;
}
@Override
public @Nullable T getValue() {
return value;
}
@Override
public String toString() {
return "Valid{" +
"property='" + property + '\'' +
", value='" + value + '\'' +
'}';
}
}
class Invalid implements Validated {
private final String property;
@Nullable
private final Object value;
private final String message;
@Nullable
private final Throwable exception;
public Invalid(String property, @Nullable Object value, String message, @Nullable Throwable exception) {
this.property = property;
this.value = value;
this.message = message;
this.exception = exception;
}
@Override
public boolean isValid() {
return false;
}
@Override
public @Nullable T getValue() {
throw new ValidationException(this);
}
@Override
public Iterator> iterator() {
return Stream.of((Validated) this).iterator();
}
public String getMessage() {
return message;
}
public String getProperty() {
return property;
}
public @Nullable Object getInvalidValue() {
return value;
}
public @Nullable Throwable getException() {
return exception;
}
@Override
public String toString() {
return getClass().getSimpleName() + "{" +
"property='" + property + '\'' +
", value='" + value + '\'' +
", message='" + message + '\'' +
'}';
}
}
class Missing extends Invalid {
public Missing(String property, @Nullable T value, String message) {
super(property, value, message, null);
}
}
class Either implements Validated {
private final Validated left;
private final Validated right;
public Either(Validated left, Validated right) {
this.left = left;
this.right = right;
}
@Override
public boolean isValid() {
return left.isValid() || right.isValid();
}
public Optional> findAny() {
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator(), Spliterator.CONCURRENT), false)
.filter(Validated::isValid)
.findAny();
}
@Override
public T getValue() {
return findAny()
.map(Validated::getValue)
.orElseThrow(() -> new IllegalStateException("Value does not exist"));
}
@Override
public Iterator> iterator() {
//If only one side is valid, this short circuits the invalid path.
if (left.isValid() && right.isInvalid()) {
return stream(left.spliterator(), false).iterator();
} else if (left.isInvalid() && right.isValid()) {
return stream(right.spliterator(), false).iterator();
} else {
//If both are valid/invalid, concat all validations.
return Stream.concat(
stream(left.spliterator(), false),
stream(right.spliterator(), false)
).iterator();
}
}
}
class Both implements Validated {
protected final Validated left;
protected final Validated right;
public Both(Validated left, Validated right) {
this.left = left;
this.right = right;
}
@Override
public boolean isValid() {
return left.isValid() && right.isValid();
}
@Override
public T getValue() {
return right.getValue();
}
@Override
public Iterator> iterator() {
return Stream.concat(
stream(left.spliterator(), false),
stream(right.spliterator(), false)
).iterator();
}
}
}