ValueParser.java

/*
 * Copyright (c) 2016, Stein Eldar Johnsen
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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
 *
 *   http://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 net.morimekta.terminal.args;

import net.morimekta.terminal.args.impl.ArgumentImpl;
import net.morimekta.terminal.args.impl.OptionImpl;
import net.morimekta.terminal.args.impl.PropertyImpl;
import net.morimekta.terminal.args.parser.BigDecimalParser;
import net.morimekta.terminal.args.parser.BigIntegerParser;
import net.morimekta.terminal.args.parser.ByteParser;
import net.morimekta.terminal.args.parser.DirParser;
import net.morimekta.terminal.args.parser.DoubleParser;
import net.morimekta.terminal.args.parser.DurationParser;
import net.morimekta.terminal.args.parser.EnumParser;
import net.morimekta.terminal.args.parser.FileParser;
import net.morimekta.terminal.args.parser.FloatParser;
import net.morimekta.terminal.args.parser.IntegerParser;
import net.morimekta.terminal.args.parser.LongParser;
import net.morimekta.terminal.args.parser.OutputDirParser;
import net.morimekta.terminal.args.parser.OutputFileParser;
import net.morimekta.terminal.args.parser.PathParser;
import net.morimekta.terminal.args.parser.ShortParser;
import net.morimekta.terminal.args.parser.UnsignedIntegerParser;
import net.morimekta.terminal.args.parser.UnsignedLongParser;

import java.io.File;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Optional;
import java.util.function.Consumer;

/**
 * Value parser interface. It converts from a string value (usually the
 * CLI argument value) and converts it into a specific value type. Essentially
 * this interface is meant to bridge two {@link Consumer}s, the target consumer
 * (usually the value setter or adder), and the string consumer that the
 * {@link OptionImpl} or {@link ArgumentImpl} needs.
 * <p>
 * E.g., the following code will create an option '--timestamp' that parses the
 * argument value as a long (i64) and calls <code>bean.setTimestamp(long)</code>
 * with that value:
 * <code>
 * new Option("--timestamp", null, "The timestamp", i64(bean::setTimestamp));
 * </code>
 */
@FunctionalInterface
public interface ValueParser<T> {
    /**
     * A typed putter interface used to put key value pairs into maps, properties
     * etc.
     *
     * @param <T> The value type when putting.
     */
    @FunctionalInterface
    interface TypedPutter<T> {
        /**
         * Put a typed value
         *
         * @param key   The property key.
         * @param value The property value.
         */
        void put(String key, T value);
    }

    /**
     * Parse the value into a typed instance.
     *
     * @param value The string value.
     * @return The typed instance.
     * @throws ArgException If the parsing failed.
     */
    T parse(String value);

    /**
     * Make a string consumer to typed value consumer out of the converter.
     *
     * @param consumer The consumer to wrap.
     * @return The string consumer.
     */
    default Consumer<String> andApply(Consumer<T> consumer) {
        return s -> consumer.accept(parse(s));
    }

    /**
     * Make a property putter that calls a typed putter with the parsed value.
     *
     * @param putter The typed putter.
     * @return The property putter.
     */
    default PropertyImpl.Putter andPut(TypedPutter<T> putter) {
        return (k, v) -> putter.put(k, parse(v));
    }

    /**
     * Make a consumer that puts a specific value with the typed putter.
     *
     * @param putter the typed putter.
     * @param key    The property key.
     * @return The string consumer.
     */
    default Consumer<String> andPutAs(TypedPutter<T> putter, String key) {
        return s -> putter.put(key, parse(s));
    }

    // *********************************
    // ***   STATIC HELPER METHODS   ***
    // *********************************

    /**
     * Get the default parser for the given type.
     *
     * @param type The type to get parser for.
     * @return The value parser.
     */
    @SuppressWarnings("rawtypes,unchecked")
    static Optional<ValueParser<?>> defaultParser(Class<?> type) {
        if (type == String.class) {
            return Optional.of(str -> str);
        }
        if (type == Byte.class || type == Byte.TYPE) {
            return Optional.of(i8());
        }
        if (type == Short.class || type == Short.TYPE) {
            return Optional.of(i16());
        }
        if (type == Integer.class || type == Integer.TYPE) {
            return Optional.of(i32());
        }
        if (type == Long.class || type == Long.TYPE) {
            return Optional.of(i64());
        }
        if (type == BigInteger.class) {
            return Optional.of(bigInt());
        }
        if (type == Float.class || type == Float.TYPE) {
            return Optional.of(flt());
        }
        if (type == Double.class || type == Double.TYPE) {
            return Optional.of(dbl());
        }
        if (type == BigDecimal.class) {
            return Optional.of(bigDecimal());
        }
        if (type == Duration.class) {
            return Optional.of(duration());
        }
        if (type.isEnum()) {
            return Optional.of(oneOf((Class) type));
        }
        if (type == Path.class) {
            return Optional.of(path());
        }
        if (type == File.class) {
            return Optional.of(val -> path().parse(val).toFile());
        }
        return Optional.empty();
    }

    /**
     * Convenience method to put a specific value into a putter.
     *
     * @param putter The putter.
     * @param key    The key to put.
     * @return The string consumer.
     */
    static Consumer<String> putAs(PropertyImpl.Putter putter, String key) {
        return s -> putter.put(key, s);
    }

    /**
     * Make a 32-bit integer parser.
     *
     * @return The parser.
     */
    static ValueParser<Byte> i8() {
        return new ByteParser();
    }

    /**
     * Make a 32-bit integer parsing consumer.
     *
     * @param target The target consumer.
     * @return The consumer wrapper.
     */
    static Consumer<String> i8(Consumer<Byte> target) {
        return i8().andApply(target);
    }

    /**
     * Make a 32-bit integer parser.
     *
     * @return The parser.
     */
    static ValueParser<Short> i16() {
        return new ShortParser();
    }

    /**
     * Make a 32-bit integer parsing consumer.
     *
     * @param target The target consumer.
     * @return The consumer wrapper.
     */
    static Consumer<String> i16(Consumer<Short> target) {
        return i16().andApply(target);
    }

    /**
     * Make a 32-bit integer parser.
     *
     * @return The parser.
     */
    static ValueParser<Integer> i32() {
        return new IntegerParser();
    }

    /**
     * Make a 32-bit integer parsing consumer.
     *
     * @param target The target consumer.
     * @return The consumer wrapper.
     */
    static Consumer<String> i32(Consumer<Integer> target) {
        return i32().andApply(target);
    }

    /**
     * Make a 32-bit integer parser.
     *
     * @return The parser.
     */
    static ValueParser<Integer> ui32() {
        return new UnsignedIntegerParser();
    }

    /**
     * Make a 32-bit integer parsing consumer.
     *
     * @param target The target consumer.
     * @return The consumer wrapper.
     */
    static Consumer<String> ui32(Consumer<Integer> target) {
        return ui32().andApply(target);
    }

    /**
     * Make a 64-bit integer parser.
     *
     * @return The parser.
     */
    static ValueParser<Long> i64() {
        return new LongParser();
    }

    /**
     * Make a 64-bit integer parsing consumer.
     *
     * @param target The target consumer.
     * @return The consumer wrapper.
     */
    static Consumer<String> i64(Consumer<Long> target) {
        return i64().andApply(target);
    }

    /**
     * Make a 64-bit integer parser.
     *
     * @return The parser.
     */
    static ValueParser<Long> ui64() {
        return new UnsignedLongParser();
    }

    /**
     * Make a 64-bit integer parsing consumer.
     *
     * @param target The target consumer.
     * @return The consumer wrapper.
     */
    static Consumer<String> ui64(Consumer<Long> target) {
        return ui64().andApply(target);
    }

    /**
     * Make a big integer parser.
     *
     * @return The parser.
     */
    static ValueParser<BigInteger> bigInt() {
        return new BigIntegerParser();
    }

    /**
     * Make a big integer parsing consumer.
     *
     * @param target The target consumer.
     * @return The consumer wrapper.
     */
    static Consumer<String> bigInt(Consumer<BigInteger> target) {
        return bigInt().andApply(target);
    }

    /**
     * Make a double parser.
     *
     * @return The parser.
     */
    static ValueParser<Float> flt() {
        return new FloatParser();
    }

    /**
     * Make a 64-bit integer parsing consumer.
     *
     * @param target The target consumer.
     * @return The consumer wrapper.
     */
    static Consumer<String> flt(Consumer<Float> target) {
        return flt().andApply(target);
    }

    /**
     * Make a double parser.
     *
     * @return The parser.
     */
    static ValueParser<Double> dbl() {
        return new DoubleParser();
    }

    /**
     * Make a 64-bit integer parsing consumer.
     *
     * @param target The target consumer.
     * @return The consumer wrapper.
     */
    static Consumer<String> dbl(Consumer<Double> target) {
        return dbl().andApply(target);
    }

    /**
     * Make a double parser.
     *
     * @return The parser.
     */
    static ValueParser<BigDecimal> bigDecimal() {
        return new BigDecimalParser();
    }

    /**
     * Make a 64-bit integer parsing consumer.
     *
     * @param target The target consumer.
     * @return The consumer wrapper.
     */
    static Consumer<String> bigDecimal(Consumer<BigDecimal> target) {
        return bigDecimal().andApply(target);
    }

    /**
     * Make an enum value parsing consumer.
     *
     * @param klass The enum class.
     * @param <E>   The enum type.
     * @return The parser.
     */
    static <E extends Enum<E>> ValueParser<E> oneOf(Class<E> klass) {
        return new EnumParser<>(klass);
    }

    /**
     * Make a file parsing consumer that refers to an existing file.
     *
     * @param klass  The enum class.
     * @param target The target consumer.
     * @param <E>    The enum type.
     * @return The consumer wrapper.
     */
    static <E extends Enum<E>> Consumer<String> oneOf(Class<E> klass, Consumer<E> target) {
        return oneOf(klass).andApply(target);
    }

    /**
     * Make a file parser that refers to an existing file.
     *
     * @return The parser.
     */
    static ValueParser<Path> file() {
        return new FileParser();
    }

    /**
     * Make a file parsing consumer that refers to an existing file.
     *
     * @param target The target consumer.
     * @return The consumer wrapper.
     */
    static Consumer<String> file(Consumer<Path> target) {
        return file().andApply(target);
    }

    /**
     * Make a file parser that refers to an existing directory.
     *
     * @return The consumer wrapper.
     */
    static ValueParser<Path> dir() {
        return new DirParser();
    }

    /**
     * Make a file parsing consumer that refers to an existing directory.
     *
     * @param target The target consumer.
     * @return The parser.
     */
    static Consumer<String> dir(Consumer<Path> target) {
        return dir().andApply(target);
    }

    /**
     * Make a file parser that refers either to a non-existing entry or an
     * existing file, but not a directory or special device.
     *
     * @return The parser.
     */
    static ValueParser<Path> outputFile() {
        return new OutputFileParser();
    }

    /**
     * Make a file parsing consumer that refers either to a non-existing entry or an
     * existing file, but not a directory or special device.
     *
     * @param target The target consumer.
     * @return The consumer wrapper.
     */
    static Consumer<String> outputFile(Consumer<Path> target) {
        return outputFile().andApply(target);
    }

    /**
     * Make a parser that refers either to a non-existing entry or an
     * existing directory, but not a file or special device.
     *
     * @return The parser.
     */
    static ValueParser<Path> outputDir() {
        return new OutputDirParser();
    }

    /**
     * Make a parsing consumer that refers either to a non-existing entry or an
     * existing directory, but not a file or special device.
     *
     * @param target The target consumer.
     * @return The consumer wrapper.
     */
    static Consumer<String> outputDir(Consumer<Path> target) {
        return outputDir().andApply(target);
    }

    /**
     * Make a parser that parses a path.
     *
     * @return The parser.
     */
    static ValueParser<Path> path() {
        return new PathParser();
    }

    /**
     * Make a parsing consumer that parses a path.
     *
     * @param target The target consumer.
     * @return The consumer wrapper.
     */
    static Consumer<String> path(Consumer<Path> target) {
        return path().andApply(target);
    }

    /**
     * Make a parser that parses a duration.
     *
     * @return The parser.
     */
    static ValueParser<Duration> duration() {
        return new DurationParser();
    }

    /**
     * Make a parsing consumer that parses a duration.
     *
     * @param target The target consumer.
     * @return The consumer wrapper.
     */
    static Consumer<String> duration(Consumer<Duration> target) {
        return duration().andApply(target);
    }
}