// 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 . //! \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 #include #include #include #include #include #include #include #include #include #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 concept Exception = std::derived_from; //! \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 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 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(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(); // TODO(afiore): log levels std::cout << "Interpreting: '" << line << "'" << std::endl; gInterpreter->ProcessLine(line.c_str(), ecode.get()); throw_ex_if(*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( !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& 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& 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( !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& 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& 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( !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( !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 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( !std::regex_match(m_path.repo(), regex), msg); if (!m_path.custom().empty()) throw_ex_if( !std::regex_match(m_path.custom(), regex), msg); throw_ex_if(!std::regex_match(m_pseudo, regex), msg); // Parse out pseudo tag auto pos = m_pseudo.find('/'); throw_ex_if(!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(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( "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( 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/' or 'custom/') //! \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( !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 concept PPath = std::derived_from; //! \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 concept PSpace = std::derived_from; //! \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 concept PArgs = std::derived_from; //! \brief Base pluggable handler //! \ingroup cpp_plugin_impl cpp_macro_impl //! \since docker-finance 1.1.0 template class Pluggable { protected: explicit Pluggable(const type::Pluggable& 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 m_plug; }; } // namespace common } // namespace dfi #endif // CONTAINER_SRC_ROOT_COMMON_UTILITY_HH_ // # vim: sw=2 sts=2 si ai et