view libcommon/irccd/util.hpp @ 394:c6fbb6e0e06d

Happy new year!
author David Demelier <markand@malikania.fr>
date Sun, 01 Jan 2017 15:29:41 +0100
parents e9adab218027
children 6aae16300e0c
line wrap: on
line source

/*
 * util.hpp -- some utilities
 *
 * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#ifndef IRCCD_UTIL_HPP
#define IRCCD_UTIL_HPP

/**
 * \file util.hpp
 * \brief Utilities.
 */

#include <ctime>
#include <initializer_list>
#include <limits>
#include <regex>
#include <sstream>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <vector>

#include <format.h>
#include <json.hpp>

#include "net.hpp"
#include "sysconfig.hpp"

namespace irccd {

/**
 * \brief Namespace for utilities.
 */
namespace util {

/**
 * \enum MessageType
 * \brief Describe which type of message has been received
 *
 * On channels and queries, you may have a special command or a standard message
 * depending on the beginning of the message.
 *
 * Example: `!reminder help' may invoke the command event if a plugin reminder
 * exists.
 */
enum class MessageType {
    Command,                        //!< special command
    Message                         //!< standard message
};

/**
 * \brief Combine the type of message and its content.
 */
using MessagePair = std::pair<std::string, MessageType>;

/**
 * \brief Used for format() function.
 */
class Substitution {
public:
    /**
     * \brief Disable or enable some features.
     */
    enum Flags {
        Date        = (1 << 0),     //!< date templates
        Keywords    = (1 << 1),     //!< keywords
        Env         = (1 << 2),     //!< environment variables
        Shell       = (1 << 3),     //!< command line command
        IrcAttrs    = (1 << 4)      //!< IRC escape codes
    };

    /**
     * Flags for selecting templates.
     */
    std::uint8_t flags{Date | Keywords | Env | IrcAttrs};

    /**
     * Fill that field if you want a date.
     */
    std::time_t time{std::time(nullptr)};

    /**
     * Fill that map if you want to replace keywords.
     */
    std::unordered_map<std::string, std::string> keywords;
};

/**
 * Format a string and update all templates.
 *
 * ## Syntax
 *
 * The syntax is <strong>?{}</strong> where <strong>?</strong> is replaced by
 * one of the token defined below. Braces are mandatory and cannot be ommited.
 *
 * To write a literal template construct, prepend the token twice.
 *
 * ## Availables templates
 *
 * The following templates are available:
 *
 * - <strong>\#{name}</strong>: name will be substituted from the keywords in
 *   params,
 * - <strong>\${name}</strong>: name will be substituted from the environment
 *   variable,
 * - <strong>\@{attributes}</strong>: the attributes will be substituted to IRC
 *   colors (see below),
 * - <strong>%</strong>, any format accepted by strftime(3).
 *
 * ## Attributes
 *
 * The attribute format is composed of three parts, foreground, background and
 * modifiers, each separated by a comma.
 *
 * **Note:** you cannot omit parameters, to specify the background, you must
 * specify the foreground.
 *
 * ## Examples
 *
 * ### Valid constructs
 *
 *   - <strong>\#{target}, welcome</strong>: if target is set to "irccd",
 *     becomes "irccd, welcome",
 *   - <strong>\@{red}\#{target}</strong>: if target is specified, it is written
 *     in red,
 *
 * ### Invalid or literals constructs
 *
 *   - <strong>\#\#{target}</strong>: will output "\#{target}",
 *   - <strong>\#\#</strong>: will output "\#\#",
 *   - <strong>\#target</strong>: will output "\#target",
 *   - <strong>\#{target</strong>: will throw std::invalid_argument.
 *
 * ### Colors & attributes
 *
 *   - <strong>\@{red,blue}</strong>: will write text red on blue background,
 *   - <strong>\@{default,yellow}</strong>: will write default color text on
 *     yellow background,
 *   - <strong>\@{white,black,bold,underline}</strong>: will write white text on
 *     black in both bold and underline.
 */
IRCCD_EXPORT std::string format(std::string text, const Substitution &params = {});

/**
 * Remove leading and trailing spaces.
 *
 * \param str the string
 * \return the removed white spaces
 */
IRCCD_EXPORT std::string strip(std::string str);

/**
 * Split a string by delimiters.
 *
 * \param list the string to split
 * \param delimiters a list of delimiters
 * \param max max number of split
 * \return a list of string splitted
 */
IRCCD_EXPORT std::vector<std::string> split(const std::string &list, const std::string &delimiters, int max = -1);

/**
 * Join values by a separator and return a string.
 *
 * \param first the first iterator
 * \param last the last iterator
 * \param delim the optional delimiter
 */
template <typename InputIt, typename DelimType = char>
std::string join(InputIt first, InputIt last, DelimType delim = ':')
{
    std::ostringstream oss;

    if (first != last) {
        oss << *first;

        while (++first != last)
            oss << delim << *first;
    }

    return oss.str();
}

/**
 * Convenient overload.
 *
 * \param list the initializer list
 * \param delim the delimiter
 * \return the string
 */
template <typename T, typename DelimType = char>
inline std::string join(std::initializer_list<T> list, DelimType delim = ':')
{
    return join(list.begin(), list.end(), delim);
}

/**
 * Clamp the value between low and high.
 *
 * \param value the value
 * \param low the minimum value
 * \param high the maximum value
 * \return the value between minimum and maximum
 */
template <typename T>
constexpr T clamp(T value, T low, T high) noexcept
{
    return (value < high) ? std::max(value, low) : std::min(value, high);
}

/**
 * Parse IRC message and determine if it's a command or a simple message.
 *
 * \param message the message line
 * \param commandChar the command char (e.g '!')
 * \param plugin the plugin name
 * \return the pair
 */
IRCCD_EXPORT MessagePair parseMessage(std::string message, const std::string &commandChar, const std::string &plugin);

/**
 * Server and identities must have strict names. This function can
 * be used to ensure that they are valid.
 *
 * \param name the identifier name
 * \return true if is valid
 */
inline bool isIdentifierValid(const std::string &name)
{
    return std::regex_match(name, std::regex("[A-Za-z0-9-_]+"));
}

/**
 * Check if the value is a boolean, 1, yes and true are accepted.
 *
 * \param value the value
 * \return true if is boolean
 * \note this function is case-insensitive
 */
IRCCD_EXPORT bool isBoolean(std::string value) noexcept;

/**
 * Check if the string is an integer.
 *
 * \param value the input
 * \param base the optional base
 * \return true if integer
 */
IRCCD_EXPORT bool isInt(const std::string &value, int base = 10) noexcept;

/**
 * Check if the string is real.
 *
 * \param value the value
 * \return true if real
 */
IRCCD_EXPORT bool isReal(const std::string &value) noexcept;

/**
 * Check if the string is a number.
 *
 * \param value the value
 * \return true if it is a number
 */
inline bool isNumber(const std::string &value) noexcept
{
    return isInt(value) || isReal(value);
}

/**
 * Tells if a number is bound between the limits.
 *
 * \param value the value to check
 * \param min the minimum
 * \param max the maximum
 * \return true if value is beyond the limits
 */
template <typename T>
constexpr bool isBound(T value, T min = std::numeric_limits<T>::min(), T max = std::numeric_limits<T>::max()) noexcept
{
    return value >= min && value <= max;
}

/**
 * Try to convert the string into number.
 *
 * This function will try to convert the string to number in the limits of T.
 *
 * If the string is not a number or if the converted value is out of range than
 * specified boundaries, an exception is thrown.
 *
 * By default, the function will use numeric limits from T.
 *
 * \param number the string to convert
 * \param min the minimum (defaults to T minimum)
 * \param max the maximum (defaults to T maximum)
 * \return the converted value
 * \throw std::invalid_argument if number is not a string
 * \throw std::out_of_range if the number is not between min and max
 */
template <typename T>
inline T toNumber(const std::string &number, T min = std::numeric_limits<T>::min(), T max = std::numeric_limits<T>::max())
{
    static_assert(std::is_integral<T>::value, "T must be integer type");

    std::conditional_t<std::is_unsigned<T>::value, unsigned long long, long long> value;

    if (std::is_unsigned<T>::value)
        value = std::stoull(number);
    else
        value = std::stoll(number);

    if (value < min || value > max)
        throw std::out_of_range("out of range");

    return static_cast<T>(value);
}

/**
 * Parse a network message from an input buffer and remove it from it.
 *
 * \param input the buffer, will be updated
 * \return the message or empty string if there is nothing
 */
IRCCD_EXPORT std::string nextNetwork(std::string &input);

/**
 * Use arguments to avoid compiler warnings about unused parameters.
 */
template <typename... Args>
inline void unused(Args&&...) noexcept
{
}

/**
 * Utilities for nlohmann json.
 */
namespace json {

/**
 * Require a property.
 *
 * \param json the json value
 * \param key the property name
 * \param type the requested property type
 * \return the value
 * \throw std::runtime_error if the property is missing
 */
inline nlohmann::json require(const nlohmann::json &json, const std::string &key, nlohmann::json::value_t type)
{
    auto it = json.find(key);
    auto dummy = nlohmann::json(type);

    if (it == json.end())
        throw std::runtime_error(fmt::format("missing '{}' property", key));
    if (it->type() != type)
        throw std::runtime_error(fmt::format("invalid '{}' property ({} expected, got {})", key, it->type_name(), dummy.type_name()));

    return *it;
}

/**
 * Convenient access for booleans.
 *
 * \param json the json object
 * \param key the property key
 * \return the boolean
 * \throw std::runtime_error if the property is missing or not a boolean
 */
inline bool requireBool(const nlohmann::json &json, const std::string &key)
{
    return require(json, key, nlohmann::json::value_t::boolean);
}

/**
 * Convenient access for ints.
 *
 * \param json the json object
 * \param key the property key
 * \return the int
 * \throw std::runtime_error if the property is missing or not ant int
 */
inline std::int64_t requireInt(const nlohmann::json &json, const std::string &key)
{
    return require(json, key, nlohmann::json::value_t::number_integer);
}

/**
 * Convenient access for unsigned ints.
 *
 * \param json the json object
 * \param key the property key
 * \return the unsigned int
 * \throw std::runtime_error if the property is missing or not ant int
 */
inline std::uint64_t requireUint(const nlohmann::json &json, const std::string &key)
{
    return require(json, key, nlohmann::json::value_t::number_unsigned);
}

/**
 * Convenient access for strings.
 *
 * \param json the json object
 * \param key the property key
 * \return the string
 * \throw std::runtime_error if the property is missing or not a string
 */
inline std::string requireString(const nlohmann::json &json, const std::string &key)
{
    return require(json, key, nlohmann::json::value_t::string);
}

/**
 * Convenient access for unique identifiers.
 *
 * \param json the json object
 * \param key the property key
 * \return the identifier
 * \throw std::runtime_error if the property is invalid
 */
inline std::string requireIdentifier(const nlohmann::json &json, const std::string &key)
{
    auto id = requireString(json, key);

    if (!isIdentifierValid(id))
        throw std::runtime_error("invalid '{}' identifier property");

    return id;
}

/**
 * Convert the json value to boolean.
 *
 * \param json the json value
 * \param def the default value if not boolean
 * \return a boolean
 */
inline bool toBool(const nlohmann::json &json, bool def = false) noexcept
{
    return json.is_boolean() ? json.get<bool>() : def;
}

/**
 * Convert the json value to int.
 *
 * \param json the json value
 * \param def the default value if not an int
 * \return an int
 */
inline std::int64_t toInt(const nlohmann::json &json, std::int64_t def = 0) noexcept
{
    return json.is_number_integer() ? json.get<std::int64_t>() : def;
}

/**
 * Convert the json value to unsigned.
 *
 * \param json the json value
 * \param def the default value if not a unsigned int
 * \return an unsigned int
 */
inline std::uint64_t toUint(const nlohmann::json &json, std::uint64_t def = 0) noexcept
{
    return json.is_number_unsigned() ? json.get<std::uint64_t>() : def;
}

/**
 * Convert the json value to string.
 *
 * \param json the json value
 * \param def the default value if not a string
 * \return a string
 */
inline std::string toString(const nlohmann::json &json, std::string def = "") noexcept
{
    return json.is_string() ? json.get<std::string>() : def;
}

/**
 * Get a property or return null one if not found or if json is not an object.
 *
 * \param json the json value
 * \param property the property key
 * \return the value or null one if not found
 */
inline nlohmann::json get(const nlohmann::json &json, const std::string &property) noexcept
{
    auto it = json.find(property);

    if (it == json.end())
        return nlohmann::json();

    return *it;
}

/**
 * Convenient access for boolean with default value.
 *
 * \param json the json value
 * \param key the property key
 * \param def the default value
 * \return the boolean
 */
inline bool getBool(const nlohmann::json &json, const std::string &key, bool def = false) noexcept
{
    return toBool(get(json, key), def);
}

/**
 * Convenient access for ints with default value.
 *
 * \param json the json value
 * \param key the property key
 * \param def the default value
 * \return the int
 */
inline std::int64_t getInt(const nlohmann::json &json, const std::string &key, std::int64_t def = 0) noexcept
{
    return toInt(get(json, key), def);
}

/**
 * Convenient access for unsigned ints with default value.
 *
 * \param json the json value
 * \param key the property key
 * \param def the default value
 * \return the unsigned int
 */
inline std::uint64_t getUint(const nlohmann::json &json, const std::string &key, std::uint64_t def = 0) noexcept
{
    return toUint(get(json, key), def);
}

/**
 * Get an integer in the given range.
 *
 * \param json the json value
 * \param key the property key
 * \param min the minimum value
 * \param max the maximum value
 * \return the value
 */
template <typename T>
inline T getIntRange(const nlohmann::json &json,
                     const std::string &key,
                     std::int64_t min = std::numeric_limits<T>::min(),
                     std::int64_t max = std::numeric_limits<T>::max()) noexcept
{
    return clamp(getInt(json, key), min, max);
}

/**
 * Get an unsigned integer in the given range.
 *
 * \param json the json value
 * \param key the property key
 * \param min the minimum value
 * \param max the maximum value
 * \return value
 */
template <typename T>
inline T getUintRange(const nlohmann::json &json,
                    const std::string &key,
                    std::uint64_t min = std::numeric_limits<T>::min(),
                    std::uint64_t max = std::numeric_limits<T>::max()) noexcept
{
    return clamp(getUint(json, key), min, max);
}

/**
 * Convenient access for strings with default value.
 *
 * \param json the json value
 * \param key the property key
 * \param def the default value
 * \return the string
 */
inline std::string getString(const nlohmann::json &json, const std::string &key, std::string def = "") noexcept
{
    return toString(get(json, key), def);
}

/**
 * Print the value as human readable.
 *
 * \param value the value
 * \return the string
 */
inline std::string pretty(const nlohmann::json &value)
{
    switch (value.type()) {
    case nlohmann::json::value_t::boolean:
        return value.get<bool>() ? "true" : "false";
    case nlohmann::json::value_t::string:
        return value.get<std::string>();
    default:
        return value.dump();
    }
}

/**
 * Pretty print a json value in the given object.
 *
 * \param object the object
 * \param prop the property
 * \return the pretty value or empty if key does not exist
 */
inline std::string pretty(const nlohmann::json &object, const std::string &prop)
{
    auto it = object.find(prop);

    if (it == object.end())
        return "";

    return pretty(*it);
}

} // !json

/**
 * \brief Miscellaneous utilities for Pollable objects
 */
namespace poller {

/**
 * \cond HIDDEN_SYMBOLS
 */

inline void prepare(fd_set &, fd_set &, net::Handle &) noexcept
{
}

/**
 * \endcond
 */

/**
 * Call prepare function for every Pollable objects.
 *
 * \param in the input set
 * \param out the output set
 * \param max the maximum handle
 * \param first the first Pollable object
 * \param rest the additional Pollable objects
 */
template <typename Pollable, typename... Rest>
inline void prepare(fd_set &in, fd_set &out, net::Handle &max, Pollable &first, Rest&... rest)
{
    first.prepare(in, out, max);
    prepare(in, out, max, rest...);
}

/**
 * \cond HIDDEN_SYMBOLS
 */

inline void sync(fd_set &, fd_set &) noexcept
{
}

/**
 * \endcond
 */

/**
 * Call sync function for every Pollable objects.
 *
 * \param in the input set
 * \param out the output set
 * \param first the first Pollable object
 * \param rest the additional Pollable objects
 */
template <typename Pollable, typename... Rest>
inline void sync(fd_set &in, fd_set &out, Pollable &first, Rest&... rest)
{
    first.sync(in, out);
    sync(in, out, rest...);
}

/**
 * Prepare and sync Pollable objects.
 *
 * \param timeout the timeout in milliseconds (< 0 means forever)
 * \param first the the first Pollable object
 * \param rest the additional Pollable objects
 */
template <typename Pollable, typename... Rest>
void poll(int timeout, Pollable &first, Rest&... rest)
{
    fd_set in, out;
    timeval tv = {0, timeout * 1000};

    FD_ZERO(&in);
    FD_ZERO(&out);

    net::Handle max = 0;

    prepare(in, out, max, first, rest...);

    // Timeout or error are discarded.
    if (::select(max + 1, &in, &out, nullptr, timeout < 0 ? nullptr : &tv) > 0)
        sync(in, out, first, rest...);
}

} // !poller

} // !util

} // !irccd

#endif // !IRCCD_UTIL_HPP