llarp/config/definition.hpp

Namespaces

Name
llarp
[crypto.hpp]
llarp::config

Classes

Name
struct llarp::config::option_flag
struct llarp::config::Required_t
struct llarp::config::Hidden_t
struct llarp::config::MultiValue_t
struct llarp::config::RelayOnly_t
struct llarp::config::ClientOnly_t
struct llarp::config::Deprecated_t
struct llarp::config::Default
Wrapper to specify a default value to an OptionDefinition.
struct llarp::config::Comment
Adds one or more comment lines to the option definition.
struct llarp::config::Env
Fetches config from environmental variable.
struct llarp::OptionDefinitionBase
A base class for specifying config options and their constraints.
struct llarp::OptionDefinition
The primary type-aware implementation of OptionDefinitionBase, this templated class allows for implementations which can use fmt::format for conversion to string and std::istringstream for input from string.
struct llarp::ConfigDefinition
A ConfigDefinition holds an ordered set of OptionDefinitions defining the allowable values and their constraints (specified through calls to defineOption()).

Source code

#pragma once

#include <fmt/core.h>
#include <initializer_list>
#include <type_traits>
#include <llarp/util/str.hpp>
#include <llarp/util/fs.hpp>

#include <iostream>
#include <memory>
#include <set>
#include <sstream>
#include <stdexcept>
#include <unordered_map>
#include <vector>
#include <functional>
#include <optional>
#include <cassert>

namespace llarp
{
  namespace config
  {
    // Base class for the following option flag types
    struct option_flag
    {};

    struct Required_t : option_flag
    {};
    struct Hidden_t : option_flag
    {};
    struct MultiValue_t : option_flag
    {};
    struct RelayOnly_t : option_flag
    {};
    struct ClientOnly_t : option_flag
    {};
    struct Deprecated_t : option_flag
    {};

    inline constexpr Required_t Required{};
    inline constexpr Hidden_t Hidden{};
    inline constexpr MultiValue_t MultiValue{};
    inline constexpr RelayOnly_t RelayOnly{};
    inline constexpr ClientOnly_t ClientOnly{};
    inline constexpr Deprecated_t Deprecated{};

    template <typename T>
    struct Default
    {
      T val;
      constexpr explicit Default(T val) : val{std::move(val)}
      {}
    };

    struct Comment
    {
      std::vector<std::string> comments;
      explicit Comment(std::initializer_list<std::string> comments) : comments{std::move(comments)}
      {}
    };

    struct Env
    {
      std::function<std::optional<std::string_view>(std::string_view)> get_env;
      std::string varname;
      template <typename GetEnv>
      constexpr Env(std::string_view _varname, const GetEnv& _get_env) : varname{_varname}
      {
        get_env = [&_get_env](auto val) -> auto { return _get_env(val); };
      }

      std::optional<std::string_view>
      operator()() const
      {
        return get_env(varname);
      }
    };

    template <typename T>
    auto
    AssignmentAcceptor(T& ref)
    {
      return [&ref](T arg) { ref = std::move(arg); };
    }

    // C++20 backport:
    template <typename T>
    using remove_cvref_t = std::remove_cv_t<std::remove_reference_t<T>>;

    template <typename T>
    constexpr bool is_default = false;
    template <typename T>
    constexpr bool is_default<Default<T>> = true;
    template <typename U>
    constexpr bool is_default<U&> = is_default<remove_cvref_t<U>>;

    template <typename T>
    constexpr bool is_default_array = false;
    template <typename T, size_t N>
    constexpr bool is_default_array<std::array<Default<T>, N>> = true;
    template <typename U>
    constexpr bool is_default_array<U&> = is_default_array<remove_cvref_t<U>>;
    template <typename T>
    constexpr bool is_env_v = std::is_same_v<Env, remove_cvref_t<T>>;

    template <typename T, typename Option>
    constexpr bool is_option = std::is_base_of_v<option_flag, remove_cvref_t<Option>>
        or std::is_same_v<Comment, Option> or is_default<Option> or is_default_array<Option>
        or std::is_invocable_v<remove_cvref_t<Option>, T> or is_env_v<Option>;

  }  // namespace config

  struct OptionDefinitionBase
  {
    template <typename... T>
    OptionDefinitionBase(std::string section_, std::string name_, const T&...)
        : section(std::move(section_))
        , name(std::move(name_))
        , required{(std::is_same_v<T, config::Required_t> || ...)}
        , multiValued{(std::is_same_v<T, config::MultiValue_t> || ...)}
        , deprecated{(std::is_same_v<T, config::Deprecated_t> || ...)}
        , hidden{deprecated || (std::is_same_v<T, config::Hidden_t> || ...)}
        , relayOnly{(std::is_same_v<T, config::RelayOnly_t> || ...)}
        , clientOnly{(std::is_same_v<T, config::ClientOnly_t> || ...)}
    {}

    virtual ~OptionDefinitionBase() = default;

    virtual std::vector<std::string>
    defaultValuesAsString() = 0;

    virtual void
    parseValue(const std::string& input) = 0;

    virtual size_t
    getNumberFound() const = 0;

    virtual std::vector<std::string>
    valuesAsString() = 0;

    virtual void
    tryAccept() const = 0;

    std::string section;
    std::string name;
    bool required = false;
    bool multiValued = false;
    bool deprecated = false;
    bool hidden = false;
    bool relayOnly = false;
    bool clientOnly = false;
    // Temporarily holds comments given during construction until the option is actually added to
    // the owning ConfigDefinition.
    std::vector<std::string> comments;
  };

  template <typename T>
  struct OptionDefinition : public OptionDefinitionBase
  {
    template <
        typename... Options,
        std::enable_if_t<(config::is_option<T, Options> && ...), int> = 0>
    OptionDefinition(std::string section_, std::string name_, Options&&... opts)
        : OptionDefinitionBase(section_, name_, opts...)
    {
      constexpr bool has_default =
          ((config::is_default_array<Options> || config::is_default<Options>) || ...);
      constexpr bool has_required =
          (std::is_same_v<config::remove_cvref_t<Options>, config::Required_t> || ...);
      constexpr bool has_hidden =
          (std::is_same_v<config::remove_cvref_t<Options>, config::Hidden_t> || ...);
      static_assert(
          not(has_default and has_required), "Default{...} and Required are mutually exclusive");
      static_assert(not(has_hidden and has_required), "Hidden and Required are mutually exclusive");

      (extractDefault(std::forward<Options>(opts)), ...);
      (extractAcceptor(std::forward<Options>(opts)), ...);
      (extractComments(std::forward<Options>(opts)), ...);
      (extractEnv(std::forward<Options>(opts)), ...);
    }

    template <typename U>
    void
    extractEnv(U&& envValue)
    {
      if constexpr (config::is_env_v<U>)
      {
        if (auto maybe = envValue())
        {
          for (auto part : split(*maybe, ","))
          {
            trim(part);
            envValues.emplace_back(fromString(std::string{part}));
          }
        }
      }
    }
    template <typename U>
    void
    extractDefault(U&& defaultValue_)
    {
      if constexpr (config::is_default_array<U>)
      {
        if (!multiValued)
          throw std::logic_error{"Array config defaults require multiValue mode"};

        defaultValues.clear();
        defaultValues.reserve(defaultValue_.size());
        for (const auto& def : defaultValue_)
          defaultValues.push_back(def.val);
      }
      else if constexpr (config::is_default<U>)
      {
        static_assert(
            std::is_convertible_v<decltype(std::forward<U>(defaultValue_).val), T>,
            "Cannot convert given llarp::config::Default to the required value type");
        defaultValues = {std::forward<U>(defaultValue_).val};
      }
    }

    template <typename U>
    void
    extractAcceptor(U&& acceptor_)
    {
      if constexpr (std::is_invocable_v<U, T>)
        acceptor = std::forward<U>(acceptor_);
    }

    template <typename U>
    void
    extractComments(U&& comment)
    {
      if constexpr (std::is_same_v<config::remove_cvref_t<U>, config::Comment>)
        comments = std::forward<U>(comment).comments;
    }

    std::optional<T>
    getValue() const
    {
      // prefer env vars.
      if (not envValues.empty())
        return envValues.front();

      if (parsedValues.empty())
      {
        if (required || defaultValues.empty())
          return std::nullopt;
        return defaultValues.front();
      }
      return parsedValues.front();
    }

    size_t
    getNumberFound() const override
    {
      if (envValues.empty())
        return parsedValues.size();
      return envValues.size();
    }

    std::vector<std::string>
    defaultValuesAsString() override
    {
      if (defaultValues.empty())
        return {};
      if constexpr (std::is_same_v<fs::path, T>)
        return {{defaultValues.front().string()}};
      else
      {
        std::vector<std::string> def_strs;
        def_strs.reserve(defaultValues.size());
        for (const auto& v : defaultValues)
        {
          if constexpr (std::is_same_v<bool, T>)
            def_strs.push_back(fmt::format("{}", (bool)v));
          else
            def_strs.push_back(fmt::format("{}", v));
        }
        return def_strs;
      }
    }

    void
    parseValue(const std::string& input) override
    {
      if (not multiValued and parsedValues.size() > 0)
      {
        throw std::invalid_argument{fmt::format("duplicate value for {}", name)};
      }

      parsedValues.emplace_back(fromString(input));
    }

    T
    fromString(const std::string& input)
    {
      if constexpr (std::is_same_v<T, std::string>)
      {
        return input;
      }
      else
      {
        std::istringstream iss(input);
        T t;
        iss >> t;
        if (iss.fail())
          throw std::invalid_argument{fmt::format("{} is not a valid {}", input, typeid(T).name())};
        return t;
      }
    }

    std::vector<std::string>
    valuesAsString() override
    {
      if (parsedValues.empty())
        return {};
      std::vector<std::string> result;
      result.reserve(parsedValues.size());
      for (const auto& v : parsedValues)
      {
        if constexpr (std::is_same_v<bool, T>)
          result.push_back(fmt::format("{}", (bool)v));
        else
          result.push_back(fmt::format("{}", v));
      }
      return result;
    }

    void
    tryAccept() const override
    {
      if (required and parsedValues.empty() and envValues.empty())
      {
        throw std::runtime_error{fmt::format(
            "cannot call tryAccept() on [{}]:{} when required but no value available",
            section,
            name)};
      }

      if (acceptor)
      {
        if (multiValued)
        {
          if (envValues.empty())
          {
            // add default value in multi value mode
            if (parsedValues.empty() and not defaultValues.empty())
              for (const auto& v : defaultValues)
                acceptor(v);

            for (auto value : parsedValues)
            {
              acceptor(value);
            }
          }
          else
          {
            for (auto value : envValues)
            {
              acceptor(value);
            }
          }
        }
        else
        {
          auto maybe = getValue();
          if (maybe)
            acceptor(*maybe);
        }
      }
    }

    std::vector<T> defaultValues;
    std::vector<T> parsedValues;
    std::vector<T> envValues;
    std::function<void(T)> acceptor;
  };

  template <>
  bool
  OptionDefinition<bool>::fromString(const std::string& input);

  using UndeclaredValueHandler =
      std::function<void(std::string_view section, std::string_view name, std::string_view value)>;

  using OptionDefinition_ptr = std::unique_ptr<OptionDefinitionBase>;

  // map of k:v pairs
  using DefinitionMap = std::unordered_map<std::string, OptionDefinition_ptr>;

  // map of section-name to map-of-definitions
  using SectionMap = std::unordered_map<std::string, DefinitionMap>;

  struct ConfigDefinition
  {
    explicit ConfigDefinition(bool relay) : relay{relay}
    {}

    ConfigDefinition&
    defineOption(OptionDefinition_ptr def);

    template <typename T, typename... Params>
    ConfigDefinition&
    defineOption(Params&&... args)
    {
      return defineOption(std::make_unique<OptionDefinition<T>>(std::forward<Params>(args)...));
    }

    ConfigDefinition&
    addConfigValue(std::string_view section, std::string_view name, std::string_view value);

    //          provided
    template <typename T>
    std::optional<T>
    getConfigValue(std::string_view section, std::string_view name)
    {
      OptionDefinition_ptr& definition = lookupDefinitionOrThrow(section, name);

      auto derived = dynamic_cast<const OptionDefinition<T>*>(definition.get());
      if (not derived)
        throw std::invalid_argument{
            fmt::format("{} is the incorrect type for [{}]:{}", typeid(T).name(), section, name)};

      return derived->getValue();
    }

    void
    addUndeclaredHandler(const std::string& section, UndeclaredValueHandler handler);

    void
    removeUndeclaredHandler(const std::string& section);

    void
    validateRequiredFields();

    void
    acceptAllOptions();

    inline void
    process()
    {
      validateRequiredFields();
      acceptAllOptions();
    }

    void
    addSectionComments(const std::string& section, std::vector<std::string> comments);

    void
    addOptionComments(
        const std::string& section, const std::string& name, std::vector<std::string> comments);

    std::string
    generateINIConfig(bool useValues = false);

   private:
    // If true skip client-only options; if false skip relay-only options.
    bool relay;

    OptionDefinition_ptr&
    lookupDefinitionOrThrow(std::string_view section, std::string_view name);
    const OptionDefinition_ptr&
    lookupDefinitionOrThrow(std::string_view section, std::string_view name) const;

    using SectionVisitor = std::function<void(const std::string&, const DefinitionMap&)>;
    void
    visitSections(SectionVisitor visitor) const;

    using DefVisitor = std::function<void(const std::string&, const OptionDefinition_ptr&)>;
    void
    visitDefinitions(const std::string& section, DefVisitor visitor) const;

    SectionMap m_definitions;

    std::unordered_map<std::string, UndeclaredValueHandler> m_undeclaredHandlers;

    // track insertion order. the vector<string>s are ordered list of section/option names.
    std::vector<std::string> m_sectionOrdering;
    std::unordered_map<std::string, std::vector<std::string>> m_definitionOrdering;

    // comments for config file generation
    using CommentList = std::vector<std::string>;
    using CommentsMap = std::unordered_map<std::string, CommentList>;
    CommentsMap m_sectionComments;
    std::unordered_map<std::string, CommentsMap> m_definitionComments;
  };

}  // namespace llarp

Updated on 2026-01-10 at 22:49:45 +0000