public interface Adapter<T> extends JmesPath<T>, Comparator<T> {
  /**
   * Parse a JSON string to a value.
   *
   * May throw exceptions when the given string is not valid JSON. The exact
   * exceptions thrown will depend on the concrete implementation.
   */
  T parseString(String str);

  /**
   * Converts the argument to a {@link List}.
   *
   * When the argument represents an array the list will contain the elements of
   * the array, when the argument represents an object the result is a list of
   * the object's values and for all other types the result is an empty list.
   */
  List<T> toList(T value);

  /**
   * Converts the argument to a {@link String}. When the argument represents a string
   * its string value is returned, otherwise a string with the value encoded
   * as JSON is returned.
   */
  String toString(T value);

  /**
   * Converts the argument to a {@link Number}, or null if the argument does not
   * represent a number.
   */
  Number toNumber(T value);

  /**
   * Returns true when the argument is truthy.
   *
   * All values are truthy, except the following, as per the JMESPath
   * specification: <code>false</code>, <code>null</code>, empty lists, empty
   * objects, empty strings.
   */
  boolean isTruthy(T value);

  /**
   * Returns the JSON type of the argument.
   *
   * As per the JMESPath specification the types are: <code>number</code>,
   * <code>string</code>, <code>boolean</code>, <code>array</code>,
   * <code>object</code>, <code>null</code>.
   *
   * @see JmesPathType
   */
  JmesPathType typeOf(T value);

  /**
   * Returns the value of a property of an object.
   *
   * The first argument must be an object and the second argument may be
   * the name of a property on that object. When the property does not exist
   * a null value (<em>not</em> Java <code>null</code>) is returned.
   */
  @Deprecated
  T getProperty(T value, String name);

  /**
   * Returns the value of a property of an object.
   *
   * The first argument must represent an object and the second argument may be
   * the name (which must be a string value) of a property of that object.
   * When the property does not exist or represents null a null value (but not
   * Java null) is returned.
   */
  T getProperty(T value, T name);

  /**
   * Returns all the property names of the given object, or an empty collection
   * when the given value does not represent an object.
   *
   * The property names are always string values.
   */
  Collection<T> getPropertyNames(T value);

  /**
   * Returns a null value (<em>not</em> Java <code>null</code>).
   */
  T createNull();

  /**
   * Returns an array value with the specified elements.
   */
  T createArray(Collection<T> elements);

  /**
   * Returns a string value containing the specified string.
   */
  T createString(String str);

  /**
   * Returns a boolean value containing the specified boolean.
   */
  T createBoolean(boolean b);

  /**
   * Returns an object value with the specified properties.
   *
   * The map keys must all be string values.
   *
   * Creating nested objects is not supported.
   */
  T createObject(Map<T, T> obj);

  /**
   * Returns a number value containing the specified floating point number.
   */
  T createNumber(double n);

  /**
   * Returns a number value containing the specified integer.
   */
  T createNumber(long n);

  /**
   * Throws an exception or returns a fallback value when a type check fails
   * during a function call evaluation.
   */
  T handleArgumentTypeError(Function function, String expectedType, String actualType);

  /**
   * Returns a function registry that can be used by the expression compiler
   * to look up functions.
   */
  FunctionRegistry functionRegistry();

  /**
   * Returns a node factory that can be used by the expression compiler to build
   * the interpreter AST.
   */
  NodeFactory<T> nodeFactory();
} public interface Expression<T> {
  /**
   * Evaluate this expression against a JSON-like structure and return the result.
   */
  T search(T input);
} public class RuntimeConfiguration {
  private final FunctionRegistry functionRegistry;
  private final boolean silentTypeErrors;

  private RuntimeConfiguration(Builder builder) {
    this.functionRegistry = builder.functionRegistry;
    this.silentTypeErrors = builder.silentTypeErrors;
  }

  public FunctionRegistry functionRegistry() {
    return functionRegistry;
  }

  public boolean silentTypeErrors() {
    return silentTypeErrors;
  }

  public static Builder builder() {
    return new Builder();
  }

  public static RuntimeConfiguration defaultConfiguration() {
    return builder().build();
  }

  public static class Builder {
    protected FunctionRegistry functionRegistry;
    protected boolean silentTypeErrors;

    public Builder() {
      this.functionRegistry = FunctionRegistry.defaultRegistry();
    }

    public RuntimeConfiguration build() {
      return new RuntimeConfiguration(this);
    }

    public Builder withFunctionRegistry(FunctionRegistry functionRegistry) {
      this.functionRegistry = functionRegistry;
      return this;
    }

    public Builder withSilentTypeErrors(boolean silentTypeErrors) {
      this.silentTypeErrors = silentTypeErrors;
      return this;
    }
  }
} public class JcfRuntime extends BaseRuntime<Object> {

  private static final StringEscapeHelper jsonEscapeHelper = new StringEscapeHelper(
    true,
    'b', '\b',
    't', '\t',
    'n', '\n',
    'f', '\f',
    'r', '\r',
    '\\', '\\',
    '\"', '\"'
  );

  public JcfRuntime() {
  }

  public JcfRuntime(RuntimeConfiguration configuration) {
    super(configuration);
  }

  @Override
  public Object parseString(String string) {
    return JsonParser.fromString(string, this);
  }

  @Override
  @SuppressWarnings("unchecked")
  public List<Object> toList(Object value) {
    switch (typeOf(value)) {
      case ARRAY:
        if (value instanceof List) {
          return (List<Object>) value;
        } else {
          return new ArrayList<>((Collection<Object>) value);
        }
      case OBJECT:
        Map<Object, Object> object = (Map<Object, Object>) value;
        return new ArrayList<>(object.values());
      default:
        return Collections.emptyList();
    }
  }

  @Override
  public String toString(Object str) {
    if (typeOf(str) == STRING) {
      return (String) str;
    } else {
      return unparse(str);
    }
  }

  @Override
  public Number toNumber(Object n) {
    if (typeOf(n) == NUMBER) {
      return (Number) n;
    } else {
      return null;
    }
  }

  @Override
  public JmesPathType typeOf(Object value) {
    if (value == null) {
      return NULL;
    } else if (value instanceof Boolean) {
      return BOOLEAN;
    } else if (value instanceof Number) {
      return NUMBER;
    } else if (value instanceof Map) {
      return OBJECT;
    } else if (value instanceof Collection) {
      return ARRAY;
    } else if (value instanceof String) {
      return STRING;
    } else {
      throw new IllegalStateException(String.format("Unknown node type encountered: %s", value.getClass().getName()));
    }
  }

  @Override
  @SuppressWarnings("unchecked")
  public boolean isTruthy(Object value) {
    switch (typeOf(value)) {
      case NULL:
        return false;
      case NUMBER:
        return true;
      case BOOLEAN:
        return (Boolean) value;
      case ARRAY:
        return !((Collection<Object>) value).isEmpty();
      case OBJECT:
        return !((Map<Object,Object>) value).isEmpty();
      case STRING:
        return !((String) value).isEmpty();
      default:
        throw new IllegalStateException(String.format("Unknown node type encountered: %s", value.getClass().getName()));
    }
  }

  @Override
  @SuppressWarnings("unchecked")
  public Object getProperty(Object value, Object name) {
    if (typeOf(value) == OBJECT) {
      return ((Map<Object, Object>) value).get(name);
    } else {
      return null;
    }
  }

  @Override
  @SuppressWarnings("unchecked")
  public Collection<Object> getPropertyNames(Object value) {
    if (typeOf(value) == OBJECT) {
      return ((Map<Object, Object>) value).keySet();
    } else {
      return Collections.emptyList();
    }
  }

  @Override
  public Object createNull() {
    return null;
  }

  @Override
  public Object createArray(Collection<Object> elements) {
    if (elements instanceof List) {
      return elements;
    } else {
      return new ArrayList<>(elements);
    }
  }

  @Override
  public Object createString(String str) {
    return str;
  }

  @Override
  public Object createBoolean(boolean b) {
    return b;
  }

  @Override
  public Object createObject(Map<Object, Object> obj) {
    return obj;
  }

  @Override
  public Object createNumber(double n) {
    return n;
  }

  @Override
  public Object createNumber(long n) {
    return n;
  }

  /**
   * Helper method to render a value as JSON.
   *
   * Assumes that <code>null</code>, <code>number</code> and <code>boolean</code>
   * render themselves correctly with <code>toString</code>, and that
   * <code>string</code> renders itself as an unquoted string.
   */
  private String unparse(Object object) {
    switch (typeOf(object)) {
      case NUMBER:
      case BOOLEAN:
        return object.toString();
      case NULL:
        return "null";
      case STRING:
        return '"' + jsonEscapeHelper.escape(toString(object)) + '"';
      case OBJECT:
        return unparseObject(object);
      case ARRAY:
        return unparseArray(object);
      default:
        throw new IllegalStateException();
    }
  }

  private String unparseObject(Object object) {
    StringBuilder str = new StringBuilder("{");
    Collection<Object> propertyNames = getPropertyNames(object);
    for (Object key: propertyNames) {
      Object value = getProperty(object, key);
      str.append('"').append(jsonEscapeHelper.escape(toString(key))).append("\":");
      str.append(unparse(value));
      str.append(',');
    }
    if (!propertyNames.isEmpty()) {
      str.setLength(str.length() - 1);
    }
    return str.append('}').toString();
  }

  private String unparseArray(Object array) {
    StringBuilder str = new StringBuilder("[");
    List<Object> elements = toList(array);
    for (Object element : elements) {
      str.append(unparse(element)).append(',');
    }
    if (!elements.isEmpty()) {
      str.setLength(str.length() - 1);

    }
    return str.append(']').toString();
  }
} public class CreateObjectNode<T> extends Node<T> {
  private final List<Entry<T>> entries;

  public static class Entry<U> {
    private final String key;
    private final Expression<U> value;

    public Entry(String key, Expression<U> value) {
      this.key = key;
      this.value = value;
    }

    protected String key() {
      return key;
    }

    protected Expression<U> value() {
      return value;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof Entry)) {
        return false;
      }
      Entry<?> other = (Entry<?>) o;
      return key().equals(other.key()) && value().equals(other.value());
    }

    @Override
    public int hashCode() {
      int h = 1;
      h = h * 31 + key.hashCode();
      h = h * 31 + value.hashCode();
      return h;
    }
  }

  public CreateObjectNode(Adapter<T> runtime, List<Entry<T>> entries) {
    super(runtime);
    this.entries = entries;
  }

  @Override
  public T search(T input) {
    if (runtime.typeOf(input) == JmesPathType.NULL) {
      return input;
    } else {
      Map<T, T> object = new LinkedHashMap<>();
      for (Entry<T> entry : entries) {
        object.put(runtime.createString(entry.key()), entry.value().search(input));
      }
      return runtime.createObject(object);
    }
  }

  @Override
  protected String internalToString() {
    if (entries.isEmpty()) {
      return "{}";
    }
    StringBuilder str = new StringBuilder("{");
    for (Entry<T> entry : entries) {
      str.append(entry.key()).append('=').append(entry.value()).append(", ");
    }
    str.setLength(str.length() - 2);
    return str.append('}').toString();
  }

  @Override
  protected boolean internalEquals(Object o) {
    CreateObjectNode<?> other = (CreateObjectNode<?>) o;
    return entries.equals(other.entries);
  }

  @Override
  protected int internalHashCode() {
    int h = 1;
    for (Entry<T> entry : entries) {
      h = h * 31 + entry.hashCode();
    }
    return h;
  }
} public abstract class Node<T> implements Expression<T> {
  protected final Adapter<T> runtime;

  public Node(Adapter<T> runtime) {
    this.runtime = runtime;
  }

  @Override
  public abstract T search(T input);

  @Override
  public String toString() {
    String extraArgs = internalToString();
    StringBuilder str = new StringBuilder();
    String name = getClass().getName();
    str.append(name.substring(name.lastIndexOf('.') + 1));
    str.setLength(str.length() - 4);
    str.append('(');
    if (extraArgs != null) {
      str.append(extraArgs);
    }
    str.append(')');
    return str.toString();
  }

  protected String internalToString() {
    return null;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (!getClass().isInstance(o)) {
      return false;
    }
    return internalEquals(o);
  }

  protected abstract boolean internalEquals(Object o);

  @Override
  public int hashCode() {
    return internalHashCode();
  }

  protected abstract int internalHashCode();
} public enum Operator {
  EQUALS,
  NOT_EQUALS,
  GREATER_THAN,
  GREATER_THAN_OR_EQUALS,
  LESS_THAN,
  LESS_THAN_OR_EQUALS;

  public static Operator fromString(String str) {
    if ("==".equals(str)) {
      return EQUALS;
    } else if ("!=".equals(str)) {
      return NOT_EQUALS;
    } else if (">".equals(str)) {
      return GREATER_THAN;
    } else if (">=".equals(str)) {
      return GREATER_THAN_OR_EQUALS;
    } else if ("<".equals(str)) {
      return LESS_THAN;
    } else if ("<=".equals(str)) {
      return LESS_THAN_OR_EQUALS;
    } else {
      throw new IllegalArgumentException(String.format("No such operator %s", str));
    }
  }
} public class FunctionRegistry {
  private static final FunctionRegistry defaultRegistry = new FunctionRegistry(
    new AbsFunction(),
    new AvgFunction(),
    new ContainsFunction(),
    new CeilFunction(),
    new EndsWithFunction(),
    new FloorFunction(),
    new JoinFunction(),
    new KeysFunction(),
    new LengthFunction(),
    new MapFunction(),
    new MaxFunction(),
    new MaxByFunction(),
    new MergeFunction(),
    new MinFunction(),
    new MinByFunction(),
    new NotNullFunction(),
    new ReverseFunction(),
    new SortFunction(),
    new SortByFunction(),
    new StartsWithFunction(),
    new SumFunction(),
    new ToArrayFunction(),
    new ToStringFunction(),
    new ToNumberFunction(),
    new TypeFunction(),
    new ValuesFunction()
  );

  private final Map<String, Function> functions;

  /**
   * Returns a registry with all the the functions specified in the JMESPath
   * specification.
   */
  public static FunctionRegistry defaultRegistry() {
    return defaultRegistry;
  }

  /**
   * Creates a new function registry containing the specified functions.
   * When there are multiple functions with the same name the last one is used.
   */
  public FunctionRegistry(Function... functions) {
    this(null, functions);
  }

  private FunctionRegistry(Map<String, Function> functions0, Function... functions1) {
    this.functions = functions0 == null ? new HashMap<String, Function>() : new HashMap<String, Function>(functions0);
    for (Function function : functions1) {
      this.functions.put(function.name(), function);
    }
  }

  /**
   * Returns the function by the specified name or null if no such function exists.
   */
  public Function getFunction(String functionName) {
    return functions.get(functionName);
  }

  /**
   * Creates a new function registry that contains all the functions of this
   * registry and the specified functions. When a new function has the same name
   * as one of the functions in this registry, the new function will be used.
   */
  public FunctionRegistry extend(Function... functions) {
    return new FunctionRegistry(this.functions, functions);
  }
} public abstract class BaseFunction implements Function {
  private final ArgumentConstraint argumentConstraints;
  private final String name;

  private static final Pattern CAMEL_CASE_COMPONENT_RE = Pattern.compile("[A-Z][^A-Z]+");

  /**
   * Constructor used by subclasses whose name ends with "Function" and that
   * accept a single, or a variable number of arguments.
   *
   * @throws FunctionConfigurationException when the function name cannot be produced from the class name
   */
  public BaseFunction(ArgumentConstraint argumentConstraints) {
    this(null, argumentConstraints);
  }

  /**
   * Constructor used by subclasses whose name ends with "Function" and that
   * accept a fixed number of argument.
   *
   * @throws FunctionConfigurationException when the function name cannot be produced from the class name
   */
  public BaseFunction(ArgumentConstraint... argumentConstraints) {
    this(null, ArgumentConstraints.listOf(argumentConstraints));
  }

  /**
   * Constructor used by subclasses that provide a custom name (not based on
   * the class name) and that accept a fixed number of arguments.
   */
  public BaseFunction(String name, ArgumentConstraint... argumentConstraints) {
    this(name, ArgumentConstraints.listOf(argumentConstraints));
  }

  /**
   * Constructor used by subclasses that provide a custom name (not based on
   * the class name) and that accept a single, or a variable number of arguments.
   */
  public BaseFunction(String name, ArgumentConstraint argumentConstraints) {
    this.name = name == null ? classNameToFunctionName() : name;
    this.argumentConstraints = argumentConstraints;
  }

  private String classNameToFunctionName() {
    String n = getClass().getName();
    if (n.indexOf('$') > -1) {
      n = n.substring(n.lastIndexOf('$') + 1);
    } else {
      n = n.substring(n.lastIndexOf('.') + 1);
    }
    if (!n.endsWith("Function")) {
      throw new FunctionConfigurationException(String.format("The function defined by %s must either pass a name to the Function constructor or the class name must end with \"Function\"", getClass().getName()));
    }
    Matcher m = CAMEL_CASE_COMPONENT_RE.matcher(n);
    int offset = 0;
    StringBuilder snakeCaseName = new StringBuilder();
    while (m.find(offset)) {
      String piece = n.substring(m.start(), m.end()).toLowerCase();
      if (piece.equals("function")) {
        break;
      }
      snakeCaseName.append(piece);
      snakeCaseName.append('_');
      offset = m.end();
    }
    snakeCaseName.deleteCharAt(snakeCaseName.length() - 1);
    return snakeCaseName.toString();
  }

  @Override
  public String name() {
    return name;
  }

  @Override
  public ArgumentConstraint argumentConstraints() {
    return argumentConstraints;
  }

  /**
   * Call this function with a list of arguments.
   *
   * The arguments can be either values or expressions, and will be checked
   * by the function's argument constraints before the function runs.
   */
  @Override
  public <T> T call(Adapter<T> runtime, List<FunctionArgument<T>> arguments) {
    Iterator<FunctionArgument<T>> argumentIterator = arguments.iterator();
    Iterator<ArgumentError> maybeError = argumentConstraints.check(runtime, argumentIterator, true);
    if (!maybeError.hasNext()) {
      return callFunction(runtime, arguments);
    } else {
      ArgumentError error = maybeError.next();
      if (error instanceof ArgumentError.ArgumentTypeError) {
        ArgumentError.ArgumentTypeError e = (ArgumentError.ArgumentTypeError) error;
        return runtime.handleArgumentTypeError(this, e.expectedType(), e.actualType());
      } else if (error instanceof ArgumentError.ArityError) {
        throw new IllegalStateException(ArityException.createMessage(this, arguments.size(), true));
      } else {
        throw new IllegalStateException(String.format("Unexpected error while type checking arguments to \"%s\": %s", name(), error.getClass().getName()));
      }
    }
  }

  /**
   * Called from {@link #call} after the argument constraints have been checked
   * against the arguments.
   * <p>
   * May perform additional type checking and throw {@link ArgumentTypeException}.
   * For example when using expressions there is currently no way to check the
   * types produced by those expressions without running the function. Functions
   * that accept expressions are responsible for checking the types of the values
   * produced by those expressions.
   */
  protected abstract <T> T callFunction(Adapter<T> runtime, List<FunctionArgument<T>> arguments);
} public final class ArgumentConstraints {
  private static final String EXPRESSION_TYPE = "expression";

  /**
   * Describes a heterogeneous 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.
   * <p>
   * 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 HeterogeneousListOf(constraints);
  }

  /**
   * Describes a homogeneous 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.
   * <p>
   * May only be used as a top level constraint.
   */
  public static ArgumentConstraint listOf(int min, int max, ArgumentConstraint constraint) {
    return new HomogeneousListOf(min, max, constraint);
  }

  /**
   * Describes a homogeneous list of arguments without upper limit.
   * An {@link ArityException} will be thrown when there are fewer arguments
   * than the specified minimum arity.
   * <p>
   * May only be used as a top level constraint.
   */
  public static ArgumentConstraint listOf(int min, ArgumentConstraint constraint) {
    return new VariadicListOf(min, 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() {}

  private static abstract class BaseArgumentConstraint implements ArgumentConstraint {
    private final int minArity;
    private final int maxArity;
    private final String expectedTypeDescription;

    public BaseArgumentConstraint(int minArity, int maxArity, String expectedTypeDescription) {
      this.minArity = minArity;
      this.maxArity = maxArity;
      this.expectedTypeDescription = expectedTypeDescription;
    }

    protected <T> Iterator<ArgumentError> checkNoRemainingArguments(Iterator<FunctionArgument<T>> arguments, boolean expectNoRemainingArguments) {
      if (expectNoRemainingArguments && arguments.hasNext()) {
        return singletonIterator(ArgumentError.createArityError());
      } else {
        return emptyIterator();
      }
    }

    @Override
    public int minArity() {
      return minArity;
    }

    @Override
    public int maxArity() {
      return maxArity;
    }

    @Override
    public boolean arityViolated(int n) { return (n < minArity || maxArity < n); }

    @Override
    public String expectedType() {
      return expectedTypeDescription;
    }

    protected <U> Iterator<U> singletonIterator(U obj) {
      return Collections.singleton(obj).iterator();
    }

    protected <U> Iterator<U> emptyIterator() {
      return Collections.emptyIterator();
    }
  }

  private static class HomogeneousListOf extends BaseArgumentConstraint {
    private final ArgumentConstraint subConstraint;

    public HomogeneousListOf(int minArity, int maxArity, ArgumentConstraint subConstraint) {
      super(minArity, maxArity, subConstraint.expectedType());
      this.subConstraint = subConstraint;
    }

    @Override
    public <T> Iterator<ArgumentError> check(Adapter<T> runtime, Iterator<FunctionArgument<T>> arguments, boolean expectNoRemainingArguments) {
      int i = 0;
      for (; i < minArity(); i++) {
        if (!arguments.hasNext()) {
          return singletonIterator(ArgumentError.createArityError());
        } else {
          Iterator<ArgumentError> error = subConstraint.check(runtime, arguments, false);
          if (error.hasNext()) {
            return error;
          }
        }
      }
      for (; i < maxArity(); i++) {
        if (arguments.hasNext()) {
          Iterator<ArgumentError> error = subConstraint.check(runtime, arguments, false);
          if (error.hasNext()) {
            return error;
          }
        } else {
          break;
        }
      }
      return checkNoRemainingArguments(arguments, expectNoRemainingArguments);
    }
  }

  private static class HeterogeneousListOf extends BaseArgumentConstraint {
    private final ArgumentConstraint[] subConstraints;

    public HeterogeneousListOf(ArgumentConstraint[] subConstraints) {
      super(calculateMinArity(subConstraints), calculateMaxArity(subConstraints), null);
      this.subConstraints = subConstraints;
    }

    private static int calculateMinArity(ArgumentConstraint[] subConstraints) {
      int min = 0;
      for (ArgumentConstraint constraint : subConstraints) {
        min += constraint.minArity();
      }
      return min;
    }

    private static int calculateMaxArity(ArgumentConstraint[] subConstraints) {
      int max = 0;
      for (ArgumentConstraint constraint : subConstraints) {
        max += constraint.maxArity();
      }
      return max;
    }

    @Override
    public <T> Iterator<ArgumentError> check(Adapter<T> runtime, Iterator<FunctionArgument<T>> arguments, boolean expectNoRemainingArguments) {
      for (int i = 0; i < subConstraints.length; i++) {
        if (arguments.hasNext()) {
          Iterator<ArgumentError> error = subConstraints[i].check(runtime, arguments, false);
          if (error.hasNext()) {
            return error;
          }
        } else {
          return singletonIterator(ArgumentError.createArityError());
        }
      }
      return checkNoRemainingArguments(arguments, expectNoRemainingArguments);
    }
  }

  private static class VariadicListOf extends BaseArgumentConstraint {
    private final ArgumentConstraint subConstraint;

    public VariadicListOf(int minArity, ArgumentConstraint subConstraint) {
      super(minArity, -1, subConstraint.expectedType());
      this.subConstraint = subConstraint;
    }

    @Override
    public <T> Iterator<ArgumentError> check(Adapter<T> runtime, Iterator<FunctionArgument<T>> arguments, boolean expectNoRemainingArguments) {
      int i = 0;
      for ( ;arguments.hasNext(); ++i) {
          Iterator<ArgumentError> error = subConstraint.check(runtime, arguments, false);
          if (error.hasNext()) {
            return error;
          }
      }
      if (i < minArity()) {
        return singletonIterator(ArgumentError.createArityError());
      } else {
        return emptyIterator();
      }
    }

    @Override
    public boolean arityViolated(int n) { return n < minArity(); }
  }


  private static abstract class TypeCheck extends BaseArgumentConstraint {
    public TypeCheck(String expectedType) {
      super(1, 1, expectedType);
    }

    @Override
    public <T> Iterator<ArgumentError> check(Adapter<T> runtime, Iterator<FunctionArgument<T>> arguments, boolean expectNoRemainingArguments) {
      if (arguments.hasNext()) {
        Iterator<ArgumentError> error = checkType(runtime, arguments.next());
        if (error.hasNext()) {
          return error;
        } else {
          return checkNoRemainingArguments(arguments, expectNoRemainingArguments);
        }
      } else {
        return singletonIterator(ArgumentError.createArityError());
      }
    }

    protected abstract <T> Iterator<ArgumentError> checkType(Adapter<T> runtime, FunctionArgument<T> argument);
  }

  private static class AnyValue extends TypeCheck {
    public AnyValue() {
      super("any value");
    }

    @Override
    protected <T> Iterator<ArgumentError> checkType(Adapter<T> runtime, FunctionArgument<T> argument) {
      if (argument.isExpression()) {
        return singletonIterator((ArgumentError) ArgumentError.createArgumentTypeError("any value", EXPRESSION_TYPE));
      } else {
        return emptyIterator();
      }
    }
  }

  private static class TypeOf extends TypeCheck {
    private final JmesPathType expectedType;

    public TypeOf(JmesPathType expectedType) {
      super(expectedType.toString());
      this.expectedType = expectedType;
    }

    @Override
    protected <T> Iterator<ArgumentError> checkType(Adapter<T> runtime, FunctionArgument<T> argument) {
      if (argument.isExpression()) {
        return singletonIterator((ArgumentError) ArgumentError.createArgumentTypeError(expectedType.toString(), EXPRESSION_TYPE));
      } else {
        JmesPathType actualType = runtime.typeOf(argument.value());
        if (actualType != expectedType) {
          return singletonIterator((ArgumentError) ArgumentError.createArgumentTypeError(expectedType.toString(), actualType.toString()));
        }
      }
      return emptyIterator();
    }
  }

  private static class TypeOfEither extends TypeCheck {
    private final JmesPathType[] expectedTypes;

    public TypeOfEither(JmesPathType[] expectedTypes) {
      super(createExpectedTypeString(expectedTypes));
      this.expectedTypes = expectedTypes;
    }

    private static 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 <T> Iterator<ArgumentError> checkType(Adapter<T> runtime, FunctionArgument<T> argument) {
      if (argument.isExpression()) {
        return singletonIterator((ArgumentError) ArgumentError.createArgumentTypeError(expectedType(), EXPRESSION_TYPE));
      } else {
        JmesPathType actualType = runtime.typeOf(argument.value());
        for (int i = 0; i < expectedTypes.length; i++) {
          if (expectedTypes[i] == actualType) {
            return emptyIterator();
          }
        }
        return singletonIterator((ArgumentError) ArgumentError.createArgumentTypeError(expectedType(), actualType.toString()));
      }
    }
  }

  private static class Expression extends TypeCheck {
    public Expression() {
      super(EXPRESSION_TYPE);
    }

    @Override
    protected <T> Iterator<ArgumentError> checkType(Adapter<T> runtime, FunctionArgument<T> argument) {
      if (!argument.isExpression()) {
        return singletonIterator((ArgumentError) ArgumentError.createArgumentTypeError(EXPRESSION_TYPE, runtime.typeOf(argument.value()).toString()));
      } else {
        return emptyIterator();
      }
    }
  }

  private static class ArrayOf extends BaseArgumentConstraint {
    private ArgumentConstraint subConstraint;

    public ArrayOf(ArgumentConstraint subConstraint) {
      super(1, 1, String.format("array of %s", subConstraint.expectedType()));
      this.subConstraint = subConstraint;
    }

    @Override
    public <T> Iterator<ArgumentError> check(Adapter<T> runtime, Iterator<FunctionArgument<T>> arguments, boolean expectNoRemainingArguments) {
      if (arguments.hasNext()) {
        FunctionArgument<T> argument = arguments.next();
        if (argument.isExpression()) {
          return singletonIterator((ArgumentError) ArgumentError.createArgumentTypeError(expectedType(), EXPRESSION_TYPE));
        } else {
          T value = argument.value();
          JmesPathType type = runtime.typeOf(value);
          if (type == JmesPathType.ARRAY) {
            Iterator<ArgumentError> error = checkElements(runtime, value);
            if (error.hasNext()) {
              return error;
            }
          } else {
            return singletonIterator((ArgumentError) ArgumentError.createArgumentTypeError(expectedType(), type.toString()));
          }
        }
        return checkNoRemainingArguments(arguments, expectNoRemainingArguments);
      } else {
        return singletonIterator(ArgumentError.createArityError());
      }
    }

    private <T> Iterator<ArgumentError> checkElements(Adapter<T> runtime, T value) {
      List<T> elements = runtime.toList(value);
      if (!elements.isEmpty()) {
        List<FunctionArgument<T>> wrappedElements = new ArrayList<>(elements.size());
        Set<JmesPathType> types = new LinkedHashSet<>();
        for (T element : elements) {
          wrappedElements.add(FunctionArgument.of(element));
          types.add(runtime.typeOf(element));
        }
        if (types.size() > 1) {
          return singletonIterator(createMixedTypesError(types));
        }
        Iterator<FunctionArgument<T>> wrappedElementsIterator = wrappedElements.iterator();
        while (wrappedElementsIterator.hasNext()) {
          Iterator<ArgumentError> error = subConstraint.check(runtime, wrappedElementsIterator, false);
          if (error.hasNext()) {
            ArgumentError e = error.next();
            if (e instanceof ArgumentError.ArgumentTypeError) {
              ArgumentError.ArgumentTypeError ee = (ArgumentError.ArgumentTypeError) e;
              return singletonIterator((ArgumentError) ArgumentError.createArgumentTypeError(expectedType(), String.format("array containing %s", ee.actualType())));
            } else {
              return singletonIterator(e);
            }
          }
        }
      }
      return emptyIterator();
    }

    private ArgumentError createMixedTypesError(Set<JmesPathType> 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 ");
        }
      }
      return ArgumentError.createArgumentTypeError(expectedType(), actualTypes.toString());
    }
  }
} public abstract class FunctionArgument<T> {
  private static class V<U> extends FunctionArgument<U> {
    private final U value;

    public V(U value) {
      this.value = value;
    }

    @Override
    public U value() { return value; }

    @Override
    public boolean isValue() { return true; }
  }

  private static class E<U> extends FunctionArgument<U> {
    private final Expression<U> expression;

    public E(Expression<U> expression) {
      this.expression = expression;
    }

    @Override
    public Expression<U> expression() { return expression; }

    @Override
    public boolean isExpression() { return true; }
  }

  /**
   * Creates a new function argument that contains a value.
   */
  public static <U> FunctionArgument<U> of(U value) {
    return new V<>(value);
  }

  /**
   * Creates a new function argument that contains an expression.
   */
  public static <U> FunctionArgument<U> of(Expression<U> expression) {
    return new E<>(expression);
  }

  private FunctionArgument() { }

  /**
   * Returns the expression contained in this argument, or null if this argument
   * is not an expression argument.
   */
  public Expression<T> expression() { return null; }

  /**
   * Returns the value contained in this argument, or null if this argument is
   * not a value argument.
   */
  public T value() { return null; }

  /** Returns true when this argument contains an expression */
  public boolean isExpression() { return false; }

  /** Returns true when this argument contains a value */
  public boolean isValue() { return false; }
}
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import io.burt.jmespath.Adapter;
import io.burt.jmespath.Expression;
import io.burt.jmespath.RuntimeConfiguration;
import io.burt.jmespath.jcf.JcfRuntime;
import io.burt.jmespath.node.CreateObjectNode;
import io.burt.jmespath.node.Node;
import io.burt.jmespath.node.Operator;
import io.burt.jmespath.function.FunctionRegistry;
import io.burt.jmespath.function.BaseFunction;
import io.burt.jmespath.function.ArgumentConstraints;
import io.burt.jmespath.function.FunctionArgument;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.containsString;
    Expression<Object> expected = Sequence(
      Property("Records"),
      Projection(
        Sequence(
          Property("requestParameters"),
          Property("keyName")
        )
      )
    );
    Expression<Object> actual = compile("Records[*].requestParameters.keyName");
    assertThat(actual, is(expected));
  }

  @Test
  public void projectionAndFlatten() {
    Expression<Object> expected = Sequence(
      Sequence(
        Property("Records"),
        Sequence(
          Index(0),
          Sequence(
            Property("responseElements"),
            Sequence(
              FlattenObject(),
              Projection(
                Property("items")
              )
            )
          )
        )
      ),
      Sequence(
        FlattenArray(),
        Projection(
          Property("instanceId")
        )
      )
    );
    Expression<Object> actual = compile("Records[0].responseElements.*.items[].instanceId");
    assertThat(actual, is(expected));
  }

  @Test
  public void operationsAfterPipeAfterProjection() {
    Expression<Object> expected = Sequence(
      Sequence(
        Property("Records"),
        Selection(
          String("")
        )
      ),
      Negate(
        Current()
      )
    );
    Expression<Object> actual = compile("Records[?''] | !@");
    assertThat(actual, is(expected));
  }

  @Test
  public void chainedParenthesis() {
    Expression<Object> expected = Sequence(
      Sequence(
        Property("foo"),
        FlattenArray()
      ),
      Property("bar")
    );
    Expression<Object> actual = compile("(foo[]).bar");
    assertThat(actual, is(expected));
  }

  @Test
  public void parseExceptionsCanBeIterated() {
    try {
      compile("foo`bar ^ hello");
      fail("Expected ParseException to have been thrown");
    } catch (ParseException pe) {
      int errorCount = 0;
      for (ParseError e : pe) {
        errorCount++;
      }
      assertThat(errorCount, is(2));
    }
  }

  @Test
  public void callingAFixedArityFunctionWithTooFewArgumentsThrowsParseException() {
    try {
      compile("max()");
      fail("Expected ParseException to have been thrown");
    } catch (ParseException pe) {
      assertThat(pe.getMessage(), containsString("invalid arity calling \"max\" (expected 1 but was 0)"));
    }
  }

  @Test
  public void callingAFixedArityFunctionWithTooManyArgumentsThrowsParseException() {
    try {
      compile("max(@, @)");
      fail("Expected ParseException to have been thrown");
    } catch (ParseException pe) {
      assertThat(pe.getMessage(), containsString("invalid arity calling \"max\" (expected 1 but was 2)"));
    }
  }

  @Test
  public void callingAVariableArityFunctionWithTooFewArgumentsThrowsParseException() {
    try {
      compile("not_null()");
      fail("Expected ParseException to have been thrown");
    } catch (ParseException pe) {
      assertThat(pe.getMessage(), containsString("invalid arity calling \"not_null\" (expected at least 1 but was 0)"));
    }
  }

  @Test
  public void callingAVariableArityFunctionWithTooManyArgumentsThrowsParseException() {
    runtime = new JcfRuntime(RuntimeConfiguration.builder().withFunctionRegistry(FunctionRegistry.defaultRegistry().extend(
      new BaseFunction("foobar", ArgumentConstraints.listOf(1, 3, ArgumentConstraints.anyValue())) {
        protected <T> T callFunction(Adapter<T> runtime, List<FunctionArgument<T>> arguments) { return runtime.createNull(); }