Files
docker-finance/container/src/root/common/utility.hh
Aaron Fiore 321751d009 container: root: common: add cppcheck-suppress stlIfStrFind
`basic_string::starts_with()` is not applicable and breaks functionality
2026-02-05 17:29:27 -08:00

679 lines
22 KiB
C++

// docker-finance | modern accounting for the power-user
//
// Copyright (C) 2021-2026 Aaron Fiore (Founder, Evergreen Crypto LLC)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! \file
//! \author Aaron Fiore (Founder, Evergreen Crypto LLC)
//! \note File intended to be loaded into ROOT.cern framework / Cling interpreter
//! \since docker-finance 1.1.0
#ifndef CONTAINER_SRC_ROOT_COMMON_UTILITY_HH_
#define CONTAINER_SRC_ROOT_COMMON_UTILITY_HH_
#include <ctime>
#include <filesystem>
#include <initializer_list>
#include <iomanip>
#include <iostream>
#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include "./type.hh"
//! \namespace dfi
//! \since docker-finance 1.0.0
namespace dfi
{
//! \namespace dfi::common
//! \brief Shared common functionality
//! \since docker-finance 1.1.0
namespace common
{
//! \concept Exception
//! \brief Exception type concept for `dfi` exceptions
//! \ingroup cpp_common_impl
//! \since docker-finance 1.1.0
template <typename t_exception>
concept Exception = std::derived_from<t_exception, type::Exception>;
//! \brief Throw exception using `dfi` exception type
//! \param message Message to pass to exception type
//! \param location Source location of exception
//! \ingroup cpp_common_impl
//! \since docker-finance 1.1.0
template <Exception t_exception>
inline void throw_ex(
const std::string& message = {},
const std::source_location location = std::source_location::current())
{
// TODO(unassigned): when throwing because of heap allocation
std::string msg;
msg += std::string(" \n\tFILE = ") + location.file_name();
msg += std::string(" \n\tFUNC = ") + location.function_name();
msg += std::string(" \n\tLINE = ") + location.line();
msg += std::string(" \n\tWHAT = ") + message;
throw t_exception(msg);
}
//! \brief Throw exception using `dfi` exception type (but only on condition)
//! \param condition If boolean condition is true, throw
//! \param message Message to pass to exception type
//! \param location Source location of exception
//! \ingroup cpp_common_impl
//! \since docker-finance 1.1.0
template <Exception t_exception>
inline void throw_ex_if(
const bool condition,
const std::string& message = {},
const std::source_location location = std::source_location::current())
{
if (condition)
{
throw_ex<t_exception>(message, location);
}
}
// TODO(afiore): add logger with loglevels
//! \brief Process a line of code or interpreter command
//! \since docker-finance 1.1.0
inline void line(const std::string& line)
{
using EErrorCode = ::TInterpreter::EErrorCode;
auto ecode = std::make_unique<EErrorCode>();
// TODO(afiore): log levels
std::cout << "Interpreting: '" << line << "'" << std::endl;
gInterpreter->ProcessLine(line.c_str(), ecode.get());
throw_ex_if<type::RuntimeError>(*ecode != EErrorCode::kNoError, line);
}
//! \brief Load file with given path into interpreter
//! \details Can load a source file or library file
//! \param path Operating system path (absolute or relative)
//! \since docker-finance 1.1.0
inline void load(const std::string& path)
{
throw_ex_if<type::RuntimeError>(
!std::filesystem::is_regular_file(path), "invalid file: '" + path + "'");
// TODO(afiore): log levels
std::cout << "Loading: '" << path << "'" << std::endl;
gInterpreter->LoadFile(path.c_str());
}
//! \brief Load files with given paths into interpreter
//! \details Can load source files and/or library files
//! \param path Operating system paths (absolute or relative)
//! \since docker-finance 1.1.0
inline void load(const std::initializer_list<std::string>& paths)
{
for (const auto& path : paths)
load(path);
}
//! \brief Wrappers to commonly used interpreter abstractions
//! \ingroup cpp_common_impl
//! \deprecated This will be removed in the v2 API; use the `dfi::common` free functions instead
//! \since docker-finance 1.0.0
class Command
{
public:
Command() = default;
~Command() = default;
Command(const Command&) = default;
Command& operator=(const Command&) = default;
Command(Command&&) = default;
Command& operator=(Command&&) = default;
public:
//! \deprecated This will be removed in the v2 API; use the `dfi::common` free function instead
//! \since docker-finance 1.0.0
static void load(const std::string& path) { ::dfi::common::load(path); }
//! \deprecated This will be removed in the v2 API; use the `dfi::common` free function instead
//! \since docker-finance 1.0.0
static void load(const std::initializer_list<std::string>& paths)
{
::dfi::common::load(paths);
}
};
//! \brief Unload file with given path out of interpreter
//! \details Can unload a source file or library file
//! \param path Operating system path (absolute or relative)
//! \since docker-finance 1.1.0
inline void unload(const std::string& path)
{
throw_ex_if<type::RuntimeError>(
!std::filesystem::is_regular_file(path), "invalid file: '" + path + "'");
// TODO(afiore): log levels
std::cout << "Unloading: '" << path << "'" << std::endl;
gInterpreter->UnloadFile(path.c_str());
}
//! \brief Unload files with given paths out of interpreter
//! \details Can unload source files and/or library files
//! \param path Operating system paths (absolute or relative)
//! \since docker-finance 1.1.0
inline void unload(const std::initializer_list<std::string>& paths)
{
for (const auto& path : paths)
unload(path);
}
//! \brief Reload file with given path out of/into interpreter
//! \details Can reload source files and/or library files
//! \param path Operating system paths (absolute or relative)
//! \since docker-finance 1.1.0
inline void reload(const std::string& path)
{
unload(path);
load(path);
}
//! \brief Reload files with given paths out of/into interpreter
//! \details Can reload source files and/or library files
//! \param path Operating system paths (absolute or relative)
//! \since docker-finance 1.1.0
inline void reload(const std::initializer_list<std::string>& paths)
{
unload(paths);
load(paths);
}
//! \brief Add an include directory to interpreter
//! \param path Path to include (absolute or relative)
//! \warning Only pass the complete path to directory and not "-I"
//! \warning To pass multiple paths, call this function multiple times
//! \exception type::RuntimeError if path doesn't exist or cannot be included
//! \since docker-finance 1.1.0
inline void add_include_dir(const std::string& path)
{
throw_ex_if<type::RuntimeError>(
!std::filesystem::is_directory(path),
"invalid directory: '" + path + "'");
// TODO(afiore): log levels
std::cout << "Adding: '" << path << "'" << std::endl;
gInterpreter->AddIncludePath(path.c_str());
}
//! \brief Add a linked library into interpreter
//! \param path Path of library to add (absolute or relative)
//! \warning Only pass the complete path to library and not "-l"
//! \warning To pass multiple libraries, call this function multiple times
//! \exception type::RuntimeError if library doesn't exist or cannot be linked
//! \since docker-finance 1.1.0
inline void add_linked_lib(const std::string& path)
{
load(path);
}
//! \brief Remove a linked library from interpreter
//! \param path Path of library to remove (absolute or relative)
//! \warning Only pass the complete path to library and not "-l"
//! \warning To pass multiple libraries, call this function multiple times
//! \exception type::RuntimeError if library doesn't exist or cannot be linked
//! \since docker-finance 1.1.0
inline void remove_linked_lib(const std::string& path)
{
unload(path);
}
//! \brief Get underlying environment variable
//! \param var Environment variable
//! \return Value of given environment variable
//! \exception type::RuntimeError Throws if env var is not set or unavailable
//! \note ROOT environment variables include shell (and shell caller) environment
//! \since docker-finance 1.0.0
std::string get_env(const std::string& var)
{
const auto* env = gSystem->Getenv(var.c_str());
throw_ex_if<type::RuntimeError>(
!env, "'" + var + "' is not set or is unavailable");
return std::string{env};
}
//! \brief Get `dfi`'s root-related code directory
//! \details Operating system path to where all `dfi` root-related code resides
//! \return Operating system absolute path (with trailing slash)
//! \exception type::RuntimeError if unable to get environment
//! \since docker-finance 1.1.0
std::string get_root_path()
{
return get_env("DOCKER_FINANCE_CONTAINER_REPO") + "/src/root/";
}
//! \brief Execute a command in operating system shell
//! \param cmd Operating system shell command [args]
//! \return Return value of command
//! \since docker-finance 1.0.0
int exec(const std::string& cmd)
{
return gSystem->Exec(cmd.c_str());
}
//! \brief Make current timestamp
//! \return timestamp in "yyyy-mm-ddThh:mm:ssZ" format
//! \since docker-finance 1.0.0
std::string make_timestamp()
{
const std::time_t t{std::time({})};
std::vector<char> time(std::size("yyyy-mm-ddThh:mm:ssZ"));
std::strftime(time.data(), time.size(), "%FT%TZ", std::gmtime(&t));
return std::string{time.data()};
}
//! \brief Base pseudo-path handler for various pluggables (plugins and macros)
//!
//! \details Handles pluggable paths, typically used by pluggable
//! implementations when auto-(un|re)loading.
//!
//! \ingroup cpp_plugin_impl cpp_macro_impl
//! \since docker-finance 1.1.0
class PluggablePath
{
public:
//! \brief Parses (or re-parses) constructed types
//! \warning Only call this function after constructing if underlying type::PluggablePath has been changed (post-construction)
//! \return NPI reference
auto& parse()
{
// Invalid characters
const std::regex regex{"[a-zA-Z0-9/_\\-\\.]+"};
const std::string msg{"invalid characters in path"};
if (!m_path.repo().empty())
throw_ex_if<type::RuntimeError>(
!std::regex_match(m_path.repo(), regex), msg);
if (!m_path.custom().empty())
throw_ex_if<type::RuntimeError>(
!std::regex_match(m_path.custom(), regex), msg);
throw_ex_if<type::RuntimeError>(!std::regex_match(m_pseudo, regex), msg);
// Parse out pseudo tag
auto pos = m_pseudo.find('/');
throw_ex_if<type::RuntimeError>(!pos, "no pseudo tag");
const std::string tag{m_pseudo.substr(0, pos)};
// Set family group
m_family = m_pseudo;
m_family.erase(0, pos + 1);
throw_ex_if<type::RuntimeError>(m_family.empty(), "no family found");
// Set absolute path
m_absolute = m_family;
if (tag == "repo")
{
m_is_repo = true;
m_absolute.insert(0, m_path.repo());
}
else if (tag == "custom")
{
m_is_custom = true;
m_absolute.insert(0, m_path.custom());
}
else
{
throw_ex<type::RuntimeError>(
"must be of tag 'repo' or 'custom' | was given: '" + tag + "'");
}
// Set parent path (director(y|ies))
pos = m_family.find_last_of('/');
m_parent = m_family.substr(0, pos);
// Set child (filename)
m_child = m_family.substr(pos + 1);
throw_ex_if<type::RuntimeError>(
m_child.empty() || m_child == m_parent, "child not found");
return *this;
}
protected:
//! \brief Sets pluggable absolute path from given pseudo path
//! \param pseudo Pseudo-path ('repo/<relative>' or 'custom/<relative>')
//! \param base Operating system absolute paths to pluggable locations
//! \exception type::RuntimeError If not a valid pseudo-path
PluggablePath(const std::string& pseudo, const type::PluggablePath& path)
: m_pseudo(pseudo), m_path(path), m_is_repo(false), m_is_custom(false)
{
parse();
}
~PluggablePath() = default;
PluggablePath(const PluggablePath&) = default;
PluggablePath& operator=(const PluggablePath&) = default;
PluggablePath(PluggablePath&&) = default;
PluggablePath& operator=(PluggablePath&&) = default;
public:
//! \brief Mutator to underlying type::PluggablePath
//! \warning Use with caution: underlying implementation may change
auto& operator()() { return m_path; }
//! \brief Accessor to underlying type::PluggablePath
//! \warning Use with caution: underlying implementation may change
const auto& operator()() const { return m_path; }
public:
//! \return The pluggable's complete pseudo-path
const std::string& pseudo() const { return m_pseudo; }
//! \return The pluggable's operating system absolute path (with parsed pseudo-path)
const std::string& absolute() const { return m_absolute; }
//! \return The pluggable's relative parent director(y|ies) derived from pseudo-path
//! \warning Trailing slash is removed
//! \note This also represents the expected namespace used for pluggable auto-loading
const std::string& parent() const { return m_parent; }
//! \return The pluggable's child (filename)
const std::string& child() const { return m_child; }
//! \return The pluggable's group of parent and child members
const std::string& family() const { return m_family; }
//! \return true if pseudo-path describes a repository location
bool is_repo() const { return m_is_repo; }
//! \return true if pseudo-path describes a custom location
bool is_custom() const { return m_is_custom; }
private:
type::PluggablePath m_path;
std::string m_pseudo, m_absolute;
std::string m_family, m_parent, m_child;
bool m_is_repo, m_is_custom;
};
//! \brief Base pluggable space handler for various pluggables (plugins and macros)
//!
//! \details Handles pluggable namespace and entrypoint class name,
//! typically used by pluggable implementations when auto-(un|re)loading.
//!
//! \ingroup cpp_plugin_impl cpp_macro_impl
//! \since docker-finance 1.1.0
class PluggableSpace
{
public:
//! \brief Parses (or re-parses) constructed types
//! \warning Only call this function after constructing if underlying type::PluggableSpace has been changed (post-construction)
//! \return NPI reference
auto& parse()
{
auto const parser = [](const std::string& space) -> std::string {
std::string parsed{space};
// NOTE: allowed to be empty (for now)
if (!parsed.empty())
{
throw_ex_if<type::RuntimeError>(
!std::regex_match(
parsed,
std::regex{
"[a-zA-Z0-9:/_\\-\\.]+"} /* TODO(unassigned): refine */),
"invalid characters in pluggable space");
if (parsed.find('/')) // cppcheck-suppress stlIfStrFind
{
parsed = std::regex_replace(parsed, std::regex{"/"}, "::");
}
if (parsed.find('-')) // cppcheck-suppress stlIfStrFind
{
parsed = std::regex_replace(parsed, std::regex{"-"}, "_");
}
if (parsed.find('.')) // cppcheck-suppress stlIfStrFind
{
parsed = std::regex_replace(parsed, std::regex{"\\."}, "_");
}
}
return parsed;
};
m_space.outer(parser(m_space.outer()));
m_space.inner(parser(m_space.inner()));
m_space.entry(parser(m_space.entry()));
return *this;
}
protected:
// \note Since the current presumption is that a PluggableSpace is likely
// to be derived from an operating system path (via PluggablePath),
// there's leeway for path-to-space conversions to be done here.
explicit PluggableSpace(const type::PluggableSpace& space) : m_space(space)
{
parse();
}
~PluggableSpace() = default;
PluggableSpace(const PluggableSpace&) = default;
PluggableSpace& operator=(const PluggableSpace&) = default;
PluggableSpace(PluggableSpace&&) = default;
PluggableSpace& operator=(PluggableSpace&&) = default;
public:
//! \brief Mutator to underlying type::PluggableSpace
//! \warning Use with caution: underlying implementation may change
auto& operator()() { return m_space; }
//! \brief Accessor to underlying type::PluggableSpace
//! \warning Use with caution: underlying implementation may change
const auto& operator()() const { return m_space; }
public:
//! \return The pluggable's outer namespace
const std::string& outer() const { return m_space.outer(); }
//! \return The pluggable's inner namespace
const std::string& inner() const { return m_space.inner(); }
//! \return The pluggable's entrypoint class name
const std::string& entry() const { return m_space.entry(); }
//! \return true if the outer namespace was set
bool has_outer() const { return !m_space.outer().empty(); }
//! \return true if the inner namespace was set
bool has_inner() const { return !m_space.inner().empty(); }
//! \return true if the entrypoint class name was set
bool has_entry() const { return !m_space.entry().empty(); }
private:
type::PluggableSpace m_space;
};
//! \brief Base argument handler for various pluggables (plugins and macros)
//!
//! \details Handles pluggable arguments, typically used by pluggable
//! implementations when auto-(un|re)loading.
//!
//! \ingroup cpp_plugin_impl cpp_macro_impl
//! \since docker-finance 1.1.0
class PluggableArgs
{
protected:
explicit PluggableArgs(const type::PluggableArgs& args) : m_args(args) {}
~PluggableArgs() = default;
PluggableArgs(const PluggableArgs&) = default;
PluggableArgs& operator=(const PluggableArgs&) = default;
PluggableArgs(PluggableArgs&&) = default;
PluggableArgs& operator=(PluggableArgs&&) = default;
public:
//! \brief Mutator to underlying type::PluggableArgs
//! \warning Use with caution: underlying implementation may change
auto& operator()() { return m_args; }
//! \brief Accessor to underlying type::PluggableArgs
//! \warning Use with caution: underlying implementation may change
const auto& operator()() const { return m_args; }
public:
//! \return The pluggable's loader argument
const std::string& load() const { return m_args.load(); }
//! \return The pluggable's unloader argument
const std::string& unload() const { return m_args.unload(); }
//! \return true if the load argument was set
bool has_load() const { return !m_args.load().empty(); }
//! \return true if the unload argument was set
bool has_unload() const { return !m_args.unload().empty(); }
private:
type::PluggableArgs m_args;
};
//! \concept dfi::common::PPath
//! \brief Pluggable base implementation constraint (PluggablePath)
//! \ref dfi::common::PluggablePath
//! \ingroup cpp_plugin_impl cpp_macro_impl
//! \since docker-finance 1.1.0
template <typename t_path>
concept PPath = std::derived_from<t_path, PluggablePath>;
//! \concept dfi::common::PSpace
//! \brief Pluggable base implementation constraint (PluggableSpace)
//! \ref dfi::common::PluggableSpace
//! \ingroup cpp_plugin_impl cpp_macro_impl
//! \since docker-finance 1.1.0
template <typename t_space>
concept PSpace = std::derived_from<t_space, PluggableSpace>;
//! \concept dfi::common::PArgs
//! \brief Pluggable base implementation constraint (PluggableArgs)
//! \ref dfi::common::PluggableArgs
//! \ingroup cpp_plugin_impl cpp_macro_impl
//! \since docker-finance 1.1.0
template <typename t_args>
concept PArgs = std::derived_from<t_args, PluggableArgs>;
//! \brief Base pluggable handler
//! \ingroup cpp_plugin_impl cpp_macro_impl
//! \since docker-finance 1.1.0
template <PPath t_path, PSpace t_space, PArgs t_args>
class Pluggable
{
protected:
explicit Pluggable(const type::Pluggable<t_path, t_space, t_args>& plug)
: m_plug(plug)
{
}
~Pluggable() = default;
Pluggable(const Pluggable&) = default;
Pluggable& operator=(const Pluggable&) = default;
Pluggable(Pluggable&&) = default;
Pluggable& operator=(Pluggable&&) = default;
public:
//! \brief Mutator to underlying type::Pluggable
//! \warning Use with caution: underlying implementation may change
auto& operator()() { return m_plug; }
//! \brief Accessor to underlying type::Pluggable
//! \warning Use with caution: underlying implementation may change
const auto& operator()() const { return m_plug; }
public:
//! \brief Loads a single pluggable
//! \return NPI reference
//! \ingroup cpp_plugin_impl cpp_macro_impl
//! \since docker-finance 1.1.0
const auto& load() const
{
// Load pluggable file
::dfi::common::load(m_plug.path().absolute());
// Prepare pluggable entry
const std::string entry{
"dfi::" + m_plug.space().outer() + "::" + m_plug.space().inner()
+ "::" + m_plug.space().entry()};
// Allow quotations in loader argument
std::stringstream arg;
arg << std::quoted(m_plug.args().load());
// Execute pluggable's loader
::dfi::common::line(entry + "::load(" + arg.str() + ")");
return *this;
}
//! \brief Unloads a single pluggable
//! \return NPI reference
//! \ingroup cpp_plugin_impl cpp_macro_impl
//! \since docker-finance 1.1.0
const auto& unload() const
{
// Prepare pluggable entry
const std::string entry{
"dfi::" + m_plug.space().outer() + "::" + m_plug.space().inner()
+ "::" + m_plug.space().entry()};
// Allow quotations in unloader argument
std::stringstream arg;
arg << std::quoted(m_plug.args().unload());
// Execute pluggable's unloader
::dfi::common::line(entry + "::unload(" + arg.str() + ")");
// Unload pluggable file
::dfi::common::unload(m_plug.path().absolute());
return *this;
}
//! \brief Reloads a single pluggable
//! \return NPI reference
//! \ingroup cpp_plugin_impl cpp_macro_impl
//! \since docker-finance 1.1.0
const auto& reload() const { return unload().load(); }
private:
type::Pluggable<t_path, t_space, t_args> m_plug;
};
} // namespace common
} // namespace dfi
#endif // CONTAINER_SRC_ROOT_COMMON_UTILITY_HH_
// # vim: sw=2 sts=2 si ai et