io.burt.jmespath.function.ArgumentConstraints Maven / Gradle / Ivy
Show all versions of jmespath-core Show documentation
package io.burt.jmespath.function;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;
import java.util.LinkedHashSet;
import io.burt.jmespath.Adapter;
import io.burt.jmespath.JmesPathType;
/**
* A DSL for describing argument constraints of functions.
*
* For example, this is how join
describes its arguments:
*
*
* public JoinFunction() {
* super(
* ArgumentConstraints.typeOf(JmesPathType.STRING),
* ArgumentConstraints.arrayOf(ArgumentConstraints.typeOf(JmesPathType.STRING))
* );
* }
*
* I.e. accept exactly two argument, where the first must be a string and the
* second must be an array of strings.
*
* The static methods of this class can be used to compose constraints for most
* situations, but you can also create your own constraints (that can be combined
* with other constraints) by implementing the {@link ArgumentConstraint}
* interface.
*/
public final class ArgumentConstraints {
private static final String EXPRESSION_TYPE = "expression";
/**
* Describes a heterogenous list of arguments. Each argument is checked against
* the corresponding constraint. An {@link ArityException} will be thrown when
* the number of arguments does not exactly match the number of constraints.
*
* May only be used as a top level constraint – and is already built in to
* {@link Function}, so direct usage of this method should not be needed.
*/
public static ArgumentConstraint listOf(ArgumentConstraint... constraints) {
return new HeterogenousListOf(constraints);
}
/**
* Descripes a homogenous list of arguments, of fixed or variable length.
* An {@link ArityException} will be thrown when there are fewer arguments
* than the specified minimum arity, or when there are more arguments than
* the specified maximum arity.
*
* May only be used as a top level constraint.
*/
public static ArgumentConstraint listOf(int min, int max, ArgumentConstraint constraint) {
return new HomogenousListOf(min, max, constraint);
}
/**
* Describes a single argument of any value. An {@link ArgumentTypeException}
* will be thrown when the argument is an expression.
*/
public static ArgumentConstraint anyValue() {
return new AnyValue();
}
/**
* Describes a single argument of a specified value type. An {@link ArgumentTypeException}
* will be thrown when the argument is of the wrong type (as determined by
* {@link Adapter#typeOf}) or is an expression.
*/
public static ArgumentConstraint typeOf(JmesPathType type) {
return new TypeOf(type);
}
/**
* Describes a single argument that is of one of the specified value types.
* An {@link ArgumentTypeException} will be thrown when the argument is not of
* one of the specified types (as determined by {@link Adapter#typeOf})
* or is an expression.
*/
public static ArgumentConstraint typeOf(JmesPathType... types) {
return new TypeOfEither(types);
}
/**
* Describes a single argument that is an array. Each element in the array
* will be checked against the specified constraint. An {@link ArgumentTypeException}
* is thrown when the argument does not represent an array value.
*/
public static ArgumentConstraint arrayOf(ArgumentConstraint constraint) {
return new ArrayOf(constraint);
}
/**
* Describes a single expression argument. An {@link ArgumentTypeException}
* will be thrown when the argument is not an expression.
*/
public static ArgumentConstraint expression() {
return new Expression();
}
private ArgumentConstraints() {}
@SuppressWarnings("serial")
static class InternalArgumentTypeException extends FunctionCallException {
private final String expectedType;
private final String actualType;
public InternalArgumentTypeException(String expectedType, String actualType) {
this(expectedType, actualType, null);
}
public InternalArgumentTypeException(String expectedType, String actualType, Throwable cause) {
super("", cause);
this.expectedType = expectedType;
this.actualType = actualType;
}
public String expectedType() { return expectedType; }
public String actualType() { return actualType; }
}
@SuppressWarnings("serial")
static class InternalArityException extends FunctionCallException {
public InternalArityException() {
super("");
}
}
private static class HomogenousListOf implements ArgumentConstraint {
private final ArgumentConstraint subConstraint;
private final int minArity;
private final int maxArity;
public HomogenousListOf(int minArity, int maxArity, ArgumentConstraint subConstraint) {
this.subConstraint = subConstraint;
this.minArity = minArity;
this.maxArity = maxArity;
}
@Override
public void check(Adapter runtime, Iterator> arguments) {
int i = 0;
for (; i < minArity; i++) {
if (!arguments.hasNext()) {
throw new InternalArityException();
} else {
subConstraint.check(runtime, arguments);
}
}
for (; i < maxArity; i++) {
if (arguments.hasNext()) {
subConstraint.check(runtime, arguments);
} else {
break;
}
}
}
@Override
public int minArity() {
return minArity;
}
@Override
public int maxArity() {
return maxArity;
}
@Override
public String expectedType() {
return subConstraint.expectedType();
}
}
private static class HeterogenousListOf implements ArgumentConstraint {
private final ArgumentConstraint[] subConstraints;
private final int minArity;
private final int maxArity;
public HeterogenousListOf(ArgumentConstraint[] subConstraints) {
this.subConstraints = subConstraints;
int min = 0;
int max = 0;
for (ArgumentConstraint constraint : subConstraints) {
min += constraint.minArity();
max += constraint.maxArity();
}
this.minArity = min;
this.maxArity = max;
}
@Override
public void check(Adapter runtime, Iterator> arguments) {
for (int i = 0; i < subConstraints.length; i++) {
if (arguments.hasNext()) {
subConstraints[i].check(runtime, arguments);
} else {
throw new InternalArityException();
}
}
}
@Override
public int minArity() {
return minArity;
}
@Override
public int maxArity() {
return maxArity;
}
@Override
public String expectedType() {
throw new IllegalStateException("A heterogenous list constraint does not have an expected type");
}
}
private static abstract class TypeCheck implements ArgumentConstraint {
@Override
public void check(Adapter runtime, Iterator> arguments) {
if (arguments.hasNext()) {
checkType(runtime, arguments.next());
} else {
throw new InternalArityException();
}
}
protected abstract void checkType(Adapter runtime, FunctionArgument argument);
@Override
public int minArity() {
return 1;
}
@Override
public int maxArity() {
return 1;
}
}
private static class AnyValue extends TypeCheck {
@Override
protected void checkType(Adapter runtime, FunctionArgument argument) {
if (argument.isExpression()) {
throw new InternalArgumentTypeException("any value", EXPRESSION_TYPE);
}
}
@Override
public String expectedType() {
return "any value";
}
}
private static class TypeOf extends TypeCheck {
private final JmesPathType expectedType;
public TypeOf(JmesPathType expectedType) {
this.expectedType = expectedType;
}
@Override
protected void checkType(Adapter runtime, FunctionArgument argument) {
if (argument.isExpression()) {
throw new InternalArgumentTypeException(expectedType.toString(), EXPRESSION_TYPE);
} else {
JmesPathType actualType = runtime.typeOf(argument.value());
if (actualType != expectedType) {
throw new InternalArgumentTypeException(expectedType.toString(), actualType.toString());
}
}
}
@Override
public String expectedType() {
return expectedType.toString();
}
}
private static class TypeOfEither extends TypeCheck {
private final JmesPathType[] expectedTypes;
private final String expectedTypeString;
public TypeOfEither(JmesPathType[] expectedTypes) {
this.expectedTypes = expectedTypes;
this.expectedTypeString = createExpectedTypeString(expectedTypes);
}
private String createExpectedTypeString(JmesPathType[] expectedTypes) {
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < expectedTypes.length; i++) {
buffer.append(expectedTypes[i]);
if (i < expectedTypes.length - 2) {
buffer.append(", ");
} else if (i < expectedTypes.length - 1) {
buffer.append(" or ");
}
}
return buffer.toString();
}
@Override
protected void checkType(Adapter runtime, FunctionArgument argument) {
if (argument.isExpression()) {
throw new InternalArgumentTypeException(expectedTypeString, EXPRESSION_TYPE);
} else {
JmesPathType actualType = runtime.typeOf(argument.value());
for (int i = 0; i < expectedTypes.length; i++) {
if (expectedTypes[i] == actualType) {
return;
}
}
throw new InternalArgumentTypeException(expectedTypeString, actualType.toString());
}
}
@Override
public String expectedType() {
return expectedTypeString;
}
}
private static class Expression extends TypeCheck {
@Override
protected void checkType(Adapter runtime, FunctionArgument argument) {
if (!argument.isExpression()) {
throw new InternalArgumentTypeException(EXPRESSION_TYPE, runtime.typeOf(argument.value()).toString());
}
}
@Override
public int minArity() {
return 1;
}
@Override
public int maxArity() {
return 1;
}
@Override
public String expectedType() {
return EXPRESSION_TYPE;
}
}
private static class ArrayOf implements ArgumentConstraint {
private ArgumentConstraint subConstraint;
public ArrayOf(ArgumentConstraint subConstraint) {
this.subConstraint = subConstraint;
}
@Override
public void check(Adapter runtime, Iterator> arguments) {
if (arguments.hasNext()) {
FunctionArgument argument = arguments.next();
if (argument.isExpression()) {
throw new InternalArgumentTypeException(expectedType(), EXPRESSION_TYPE);
} else {
T value = argument.value();
JmesPathType type = runtime.typeOf(value);
if (type == JmesPathType.ARRAY) {
checkElements(runtime, value);
} else {
throw new InternalArgumentTypeException(expectedType(), type.toString());
}
}
} else {
throw new InternalArityException();
}
}
private void checkElements(Adapter runtime, T value) {
List elements = runtime.toList(value);
if (!elements.isEmpty()) {
List> wrappedElements = new ArrayList<>(elements.size());
Set types = new LinkedHashSet<>();
for (T element : elements) {
wrappedElements.add(FunctionArgument.of(element));
types.add(runtime.typeOf(element));
}
if (types.size() > 1) {
handleMixedError(types);
}
Iterator> wrappedElementsIterator = wrappedElements.iterator();
while (wrappedElementsIterator.hasNext()) {
try {
subConstraint.check(runtime, wrappedElementsIterator);
} catch (InternalArgumentTypeException iate) {
throw new InternalArgumentTypeException(expectedType(), String.format("array containing %s", iate.actualType()), iate);
}
}
}
}
private void handleMixedError(Set types) {
StringBuilder actualTypes = new StringBuilder("array containing ");
Object[] typesArray = types.toArray();
for (int i = 0; i < typesArray.length; i++) {
actualTypes.append(typesArray[i].toString());
if (i < typesArray.length - 2) {
actualTypes.append(", ");
} else if (i < typesArray.length - 1) {
actualTypes.append(" and ");
}
}
throw new InternalArgumentTypeException(expectedType(), actualTypes.toString());
}
@Override
public int minArity() {
return 1;
}
@Override
public int maxArity() {
return 1;
}
@Override
public String expectedType() {
return String.format("array of %s", subConstraint.expectedType());
}
}
}