Functions.java

package com.github.dakusui.pcond.forms;

import com.github.dakusui.pcond.experimentals.currying.CurriedFunction;
import com.github.dakusui.pcond.experimentals.currying.CurryingUtils;
import com.github.dakusui.pcond.experimentals.currying.multi.MultiFunction;
import com.github.dakusui.pcond.experimentals.currying.multi.MultiFunctionUtils;
import com.github.dakusui.pcond.core.printable.PrintableFunctionFactory;
import com.github.dakusui.pcond.core.refl.MethodQuery;
import com.github.dakusui.pcond.core.refl.Parameter;
import com.github.dakusui.pcond.validator.Validator;

import java.util.Collection;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static com.github.dakusui.pcond.core.refl.ReflUtils.invokeMethod;
import static com.github.dakusui.pcond.forms.Predicates.allOf;
import static com.github.dakusui.pcond.forms.Predicates.isInstanceOf;
import static com.github.dakusui.pcond.internals.InternalUtils.formatObject;
import static java.lang.String.format;
import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;

/**
 * An entry point for acquiring function objects.
 * Functions retrieved by methods in this class are all "printable".
 */
public class Functions {
  private Functions() {
  
  }
  
  /**
   * Returns a printable function that returns a given object itself.
   *
   * @param <E> The type of the object.
   * @return The function.
   */
  public static <E> Function<E, E> identity() {
    return PrintableFunctionFactory.Simple.IDENTITY.instance();
  }
  
  /**
   * Returns a function that gives a string representation of a object given to it.
   * Internally, the returned function calls `toString` method on a given object.
   *
   * @param <E> The type of the object
   * @return The function.
   */
  public static <E> Function<E, String> stringify() {
    return PrintableFunctionFactory.Simple.STRINGIFY.instance();
  }
  
  /**
   * Returns a function that gives a length of a string passed as an argument.
   *
   * @return The function.
   */
  public static Function<? super String, Integer> length() {
    return PrintableFunctionFactory.Simple.LENGTH.instance();
  }
  
  
  @SuppressWarnings({ "unchecked", "RedundantClassCall" })
  public static <E> Function<List<E>, E> elementAt(int i) {
    return Function.class.cast(PrintableFunctionFactory.Parameterized.ELEMENT_AT.create(singletonList(i)));
  }
  
  /**
   * Returns a function that that returns a size of a given list.
   *
   * @return The function.
   */
  public static <E> Function<Collection<E>, Integer> size() {
    return PrintableFunctionFactory.Simple.SIZE.instance();
  }
  
  /**
   * Returns a function that returns a stream for a given collection.
   *
   * @param <E> Type of elements in the given collection.
   * @return The function.
   */
  public static <E> Function<Collection<? extends E>, Stream<E>> stream() {
    return PrintableFunctionFactory.Simple.STREAM.instance();
  }
  
  /**
   * Returns a function that returns a stream for a given collection.
   *
   * @param elementClass A parameter to let compiler know the type of the element in a collection.
   * @param <E>          A type of elements in a collection.
   * @return The function.
   */
  public static <E> Function<Collection<? extends E>, Stream<E>> stream(@SuppressWarnings("unused") Class<E> elementClass) {
    return stream();
  }
  
  /**
   * Returns a function that returns a stream for a given object.
   * This method corresponds to {@link Stream#of(Object)} method.
   *
   * @param <E> Type of object.
   * @return The function.
   */
  public static <E> Function<E, Stream<E>> streamOf() {
    return PrintableFunctionFactory.Simple.STREAM_OF.instance();
  }
  
  /**
   * Returns a function that casts an object into a given class.
   *
   * @param type The type to which the given object is cast
   * @param <E>  The type to which the object is case.
   * @return The function.
   */
  public static <E> Function<? super Object, E> cast(Class<E> type) {
    return PrintableFunctionFactory.Parameterized.CAST.create(singletonList(type));
  }
  
  /**
   * Returns a function that casts an object into a given class.
   * ```java
   *     assertThat(
   *         asList(lastName, fullName),
   *         allOf(
   *             transform(elementAt(0).andThen(cast(String.class))).check(allOf(isNotNull(), not(isEmptyString()))),
   *             transform(elementAt(1).andThen(castTo((List<String>)value()))).check(Predicates.contains(lastName))));
   *  ```
   *
   * @param value A type place-holder.
   *              Always use a value returned from {@link Functions#value()} method.
   * @param <E>   The type to which the object is case.
   * @return The function.
   */
  public static <E> Function<? super Object, E> castTo(@SuppressWarnings("unused") E value) {
    return PrintableFunctionFactory.Simple.CAST_TO.instance();
  }

  /**
   * Returns a function that creates and returns a list that contains all the elements in the given list.
   *
   * @param <I> The type of the input collection.
   * @param <E> Type of the elements in the collection
   * @return The function.
   */
  public static <I extends Collection<E>, E> Function<I, List<E>> collectionToList() {
    return PrintableFunctionFactory.Simple.COLLECTION_TO_LIST.instance();
  }
  
  /**
   * Returns a function that converts a given array into a list.
   *
   * @param <E> Type of elements in a given array.
   * @return The function.
   */
  public static <E> Function<E[], List<E>> arrayToList() {
    return PrintableFunctionFactory.Simple.ARRAY_TO_LIST.instance();
  }
  
  /**
   * Returns a function the counts lines in a given string.
   *
   * @return The function.
   */
  public static Function<String, Integer> countLines() {
    return PrintableFunctionFactory.Simple.COUNT_LINES.instance();
  }
  
  /**
   * //@formatter:off
   * The returned function tries to find a {@code substring} after a given string.
   * If found, it returns the result of the following statement.
   *
   * [source,java]
   * ----
   * s.substring(s.indexOf(substring) + substring.length())
   * ----
   *
   * If not found, a {@link StringIndexOutOfBoundsException} will be thrown.
   * //@formatter:on
   *
   * @param substring A substring to find in a given string.
   * @return The string after the {@code substring}.
   */
  public static Function<String, String> findString(String substring) {
    requireNonNull(substring);
    return PrintableFunctionFactory.function(
        () -> format("findString[%s]", substring),
        s -> {
          int index = s.indexOf(substring);
          if (index >= 0)
            return s.substring(s.indexOf(substring) + substring.length());
          throw new NoSuchElementException(format("'%s' was not found in '%s'", substring, s));
        });
  }
  
  /**
   * https://en.wikipedia.org/wiki/Currying[Curries] a static method specified by the given arguments.
   *
   * @param aClass         A class to which the method to be curried belongs to.
   * @param methodName     A name of the method to be curried.
   * @param parameterTypes Parameters types of the method.
   * @return A printable and curried function of the target method.
   */
  @SuppressWarnings("JavadocLinkAsPlainText")
  public static CurriedFunction<Object, Object> curry(Class<?> aClass, String methodName, Class<?>... parameterTypes) {
    return curry(multifunction(aClass, methodName, parameterTypes));
  }
  
  /**
   * Curries a given multi-function.
   *
   * @param function A multi-function to be curried
   * @return A curried function
   * @see Functions#curry(Class, String, Class[])
   */
  public static CurriedFunction<Object, Object> curry(MultiFunction<Object> function) {
    return CurryingUtils.curry(function);
  }
  
  public static <R> MultiFunction<R> multifunction(Class<?> aClass, String methodName, Class<?>... parameterTypes) {
    return MultiFunctionUtils.multifunction(IntStream.range(0, parameterTypes.length).toArray(), aClass, methodName, parameterTypes);
  }
  
  /**
   * Returns a {@link Function} created from a method specified by a {@code methodQuery}.
   * If the {@code methodQuery} matches none or more than one methods, a {@code RuntimeException} will be thrown.
   *
   * To pass an input value given to an entry point method, such as `TestAssertions.assertThat`, to the method, use a place-holder value returned by {@link Functions#parameter()}.
   *
   * @param methodQuery A query object that specifies a method to be invoked by the returned function.
   * @param <T>         the type of the input to the returned function
   * @return Created function.
   * @see Functions#classMethod(Class, String, Object[])
   * @see Functions#instanceMethod(Object, String, Object[])
   * @see Functions#parameter()
   */
  public static <T, R> Function<T, R> call(MethodQuery methodQuery) {
    return Printables.function(methodQuery.describe(), t -> invokeMethod(methodQuery.bindActualArguments((o) -> o instanceof Parameter, o -> t)));
  }
  
  
  /**
   * // @formatter:off
   * Creates a {@link MethodQuery} object from given arguments to search for {@code static} methods.
   * Note that {@code arguments} are actual argument values, not the formal parameter types.
   * The pcond library searches for the "best" matching method for you.
   * In case no matching method is found or more than one methods are found, a {@link RuntimeException}
   * will be thrown.
   *
   * In order to specify a parameter which should be passed to the returned function at applying,
   * you can use an object returned by {@link Functions#parameter} method.
   * This is useful to construct a function from an existing method.
   *
   * To pass an input value given to an entry point method, such as `TestAssertions.assertThat`, to the method, use a place-holder value returned by {@link Functions#parameter()}.
   *
   * That is, in order to create a function which computes sin using query a method {@link Math#sin(double)},
   * you can do following
   *
   * [source, java]
   * ----
   * public class Example
   *   public void buildSinFunction() {
   *     MethodQuery mq = classMethod(Math.class, "sin", parameter());
   *     Function<Double, Double> sin = call(mq);
   *     System.out.println(sin(Math.PI/2));
   *   }
   * }
   * ----
   * This prints {@code 1.0}.
   *
   * In case your arguments do not contain any {@link Parameter} object, the input
   * argument passed to the built function will be simply ignored.
   *
   * // @formatter:on
   *
   * @param targetClass A class
   * @param methodName  A method name
   * @param arguments   Arguments
   * @return A method query for static methods specified by arguments.
   * @see com.github.dakusui.pcond.core.refl.ReflUtils#findMethod(Class, String, Object[])
   * @see Functions#parameter()
   */
  public static MethodQuery classMethod(Class<?> targetClass, String methodName, Object... arguments) {
    return MethodQuery.classMethod(targetClass, methodName, arguments);
  }
  
  /**
   * // @formatter:off
   * Creates a {@link MethodQuery} object from given arguments to search for {@code static} methods.
   * Excepting that this method returns a query for instance methods, it is quite
   * similar to {@link Functions#classMethod(Class, String, Object[])}.
   *
   * This method is useful to build a function from an instance method.
   * That is, you can create a function which returns the length of a given string
   * from a method {@link String#length()} with a following code snippet.
   *
   * [source, java]
   * ----
   * public void buildLengthFunction() {
   *   Function<String, Integer> length = call(instanceMethod(parameter(), "length"));
   * }
   * ----
   *
   * In case the {@code targetObject} is not an instance of {@link Parameter} and {@code arguments}
   * contain no {@code Parameter} object, the function will simply ignore the input passed to it.
   *
   * To pass an input value given to an entry point method, such as `TestAssertions.assertThat`, to the method, use a place-holder value returned by {@link Functions#parameter()}.
   *
   * // @formatter:on
   *
   * @param targetObject An object on which methods matching returned query should be invoked.
   * @param methodName   A name of method.
   * @param arguments    Arguments passed to the method.
   * @return A method query for instance methods specified by arguments.
   * @see Functions#classMethod(Class, String, Object[])
   * @see Functions#parameter()
   */
  public static MethodQuery instanceMethod(Object targetObject, String methodName, Object... arguments) {
    return MethodQuery.instanceMethod(targetObject, methodName, arguments);
  }
  
  /**
   * // @formatter:off
   * A short hand method to call
   *
   * [source, java]
   * ---
   * call(instanceMethod(object, methodName, args))
   * ---
   * // @formatter:on
   *
   * To pass an input value given to an entry point method, such as `TestAssertions.assertThat`, to the method, use a place-holder value returned by {@link Functions#parameter()}.
   *
   * @param targetObject An object on which methods matching returned query should be invoked.
   * @param methodName   A name of method.
   * @param arguments    Arguments passed to the method.
   * @param <T>          The type of the input to the returned function.
   * @param <R>          The type of the output from the returned function.
   * @return The function that calls a method matching a query built from the given arguments.
   * @see Functions#call(MethodQuery)
   * @see Functions#instanceMethod(Object, String, Object[])
   * @see Functions#parameter()
   */
  private static <T, R> Function<T, R> callInstanceMethod(Object targetObject, String methodName, Object... arguments) {
    return call(instanceMethod(targetObject, methodName, arguments));
  }
  
  /**
   * Returns a function that calls a method which matches the given {@code methodName}
   * and {@code args} on the object given as input to it.
   *
   * Note that method look up is done when the predicate is applied.
   * This means this method does not throw any exception by itself and in case
   * you give wrong {@code methodName} or {@code arguments}, an exception will be
   * thrown when the returned function is applied.
   *
   * // @formatter:off
   * [source, java]
   * ----
   * public class Example {
   *   public void method() {
   *     assertThat(value, transform(call("toString")).check(isNotNull()));
   *   }
   * }
   * ----
   *
   * To pass an input value given to an entry point method, such as `TestAssertions.assertThat`, to the method, use a place-holder value returned by {@link Functions#parameter()}.
   *
   * // @formatter:on
   *
   * @param methodName The method name
   * @param arguments  Arguments passed to the method.
   * @param <T>        The type of input to the returned function
   * @param <R>        The type of output from the returned function
   * @return A function that invokes the method matching the {@code methodName} and {@code args}
   * @see Functions#parameter()
   */
  public static <T, R> Function<T, R> call(String methodName, Object... arguments) {
    return callInstanceMethod(parameter(), methodName, arguments);
  }
  
  /**
   * Returns a function that converts an input value to an exception object, which is thrown by `func`, when it is applied.
   * If it does not throw an exception, or even if thrown, it is not an instance of {@code exceptionClass}, an assertion executed inside this method will fail and an exception
   * to indicate it will be thrown.
   * The exception will be typically an {@link AssertionError}.
   *
   * @param exceptionClass An exception class to be thrown.
   * @param func           A function to be exercised
   * @param <T>            A type of exception value to be thrown by {@code func}.
   * @param <E>            An input value type of {@code func}.
   * @return A function that maps an input value to an exception.
   */
  @SuppressWarnings("unchecked")
  public static <T, E extends Throwable> Function<T, E> expectingException(Class<E> exceptionClass, Function<? super T, ?> func) {
    return Printables.function(
        () -> String.format("expectingException(%s,%s)", exceptionClass.getSimpleName(), func),
        in -> {
          Object out;
          try {
            out = func.apply(in);
          } catch (Throwable e) {
            Validator.instance().assertThat(e, isInstanceOf(exceptionClass));
            return (E) e;
          }
          Validator.instance().assertThat(
              String.format("%s(%s)->%s", func, formatObject(in, 12), formatObject(out, 12)),
              allOf(exceptionThrown(), exceptionClassWas(exceptionClass)));
          throw new AssertionError("A line that shouldn't be reached. File a ticket.");
        });
  }
  
  /**
   * Returns a {@link Parameter} object, which is used in combination with {@link Functions#instanceMethod(Object, String, Object[])},
   * {@link Functions#classMethod(Class, String, Object[])}, or their shorthand methods.
   * The object returned by this method is replaced with the actual input value passed to a function built
   * through {@link Functions#call(MethodQuery)} or {@link Predicates#callp(MethodQuery)}
   * when it is applied.
   *
   * @return a {@code Parameter} object
   * @see Functions#classMethod(Class, String, Object[])
   * @see Functions#instanceMethod(Object, String, Object[])
   * @see Functions#call(MethodQuery)
   * @see Functions#call(String, Object[])
   * @see Predicates#callp(MethodQuery)
   * @see Predicates#callp(String, Object[])
   */
  public static Parameter parameter() {
    return Parameter.INSTANCE;
  }
  
  private static Predicate<Object> exceptionThrown() {
    return Printables.predicate("exceptionThrown", v -> false);
  }
  
  private static Predicate<Object> exceptionClassWas(Class<? extends Throwable> exceptionClass) {
    return Printables.predicate(() -> "exceptionClass:" + requireNonNull(exceptionClass).getSimpleName(), v -> false);
  }

  /**
   * A method to return a value for a "casting placeholder value".
   *
   * @param <E> Type to cast to.
   * @return Casting placeholder value
   */
  public static <E> E value() {
    return null;
  }
}