diff --git a/src/app/CliArgs.cpp b/src/app/CliArgs.cpp index 4673c9f90..2cfa04b2f 100644 --- a/src/app/CliArgs.cpp +++ b/src/app/CliArgs.cpp @@ -21,6 +21,7 @@ #include "migration/MigrationApplication.hpp" #include "util/build/Build.hpp" +#include "util/newconfig/ConfigDescription.hpp" #include #include @@ -29,6 +30,7 @@ #include #include +#include #include #include #include @@ -42,12 +44,13 @@ CliArgs::parse(int argc, char const* argv[]) // clang-format off po::options_description description("Options"); description.add_options() - ("help,h", "print help message and exit") - ("version,v", "print version and exit") - ("conf,c", po::value()->default_value(kDEFAULT_CONFIG_PATH), "configuration file") + ("help,h", "Print help message and exit") + ("version,v", "Print version and exit") + ("conf,c", po::value()->default_value(kDEFAULT_CONFIG_PATH), "Configuration file") ("ng-web-server,w", "Use ng-web-server") - ("migrate", po::value(), "start migration helper") + ("migrate", po::value(), "Start migration helper") ("verify", "Checks the validity of config values") + ("config-description,d", po::value(), "Generate config description markdown file") ; // clang-format on po::positional_options_description positional; @@ -67,6 +70,17 @@ CliArgs::parse(int argc, char const* argv[]) return Action{Action::Exit{EXIT_SUCCESS}}; } + if (parsed.count("config-description") != 0u) { + std::filesystem::path filePath = parsed["config-description"].as(); + + auto const res = util::config::ClioConfigDescription::generateConfigDescriptionToFile(filePath); + if (res.has_value()) + return Action{Action::Exit{EXIT_SUCCESS}}; + + std::cerr << res.error().error << std::endl; + return Action{Action::Exit{EXIT_FAILURE}}; + } + auto configPath = parsed["conf"].as(); if (parsed.count("migrate") != 0u) { diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index 8c28fdf95..ffc836833 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -33,6 +33,7 @@ target_sources( newconfig/ConfigDefinition.cpp newconfig/ConfigFileJson.cpp newconfig/ObjectView.cpp + newconfig/Types.cpp newconfig/ValueView.cpp ) diff --git a/src/util/newconfig/Array.hpp b/src/util/newconfig/Array.hpp index a6ccbe958..672757655 100644 --- a/src/util/newconfig/Array.hpp +++ b/src/util/newconfig/Array.hpp @@ -25,6 +25,7 @@ #include #include +#include #include #include @@ -103,4 +104,18 @@ class Array { std::vector elements_; }; +/** + * @brief Custom output stream for Array + * + * @param stream The output stream + * @param arr The Array + * @return The same ostream we were given + */ +inline std::ostream& +operator<<(std::ostream& stream, Array arr) +{ + stream << arr.getArrayPattern(); + return stream; +} + } // namespace util::config diff --git a/src/util/newconfig/ConfigConstraints.hpp b/src/util/newconfig/ConfigConstraints.hpp index 315ea05f7..910dff1e5 100644 --- a/src/util/newconfig/ConfigConstraints.hpp +++ b/src/util/newconfig/ConfigConstraints.hpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -149,6 +150,28 @@ class Constraint { */ virtual std::optional checkValueImpl(Value const& val) const = 0; + + /** + * @brief Prints to the output stream for this specific constraint. + * + * @param stream The output stream + */ + virtual void + print(std::ostream& stream) const = 0; + + /** + * @brief Custom output stream for constraint + * + * @param stream The output stream + * @param cons The constraint + * @return The same ostream we were given + */ + friend std::ostream& + operator<<(std::ostream& stream, Constraint const& cons) + { + cons.print(stream); + return stream; + } }; /** @@ -177,6 +200,17 @@ class PortConstraint final : public Constraint { [[nodiscard]] std::optional checkValueImpl(Value const& port) const override; + /** + * @brief Prints to the output stream for this specific constraint. + * + * @param stream The output stream + */ + void + print(std::ostream& stream) const override + { + stream << fmt::format("The minimum value is `{}`. The maximum value is `{}", kPORT_MIN, kPORT_MAX); + } + static constexpr uint32_t kPORT_MIN = 1; static constexpr uint32_t kPORT_MAX = 65535; }; @@ -206,6 +240,17 @@ class ValidIPConstraint final : public Constraint { */ [[nodiscard]] std::optional checkValueImpl(Value const& ip) const override; + + /** + * @brief Prints to the output stream for this specific constraint. + * + * @param stream The output stream + */ + void + print(std::ostream& stream) const override + { + stream << "The value must be a valid IP address"; + } }; /** @@ -260,6 +305,17 @@ class OneOf final : public Constraint { return Error{makeErrorMsg(key_, val, arr_)}; } + /** + * @brief Prints to the output stream for this specific constraint. + * + * @param stream The output stream + */ + void + print(std::ostream& stream) const override + { + stream << fmt::format("The value must be one of the following: `{}`", fmt::join(arr_, ", ")); + } + std::string_view key_; std::array arr_; }; @@ -312,6 +368,17 @@ class NumberValueConstraint final : public Constraint { return Error{fmt::format("Number must be between {} and {}", min_, max_)}; } + /** + * @brief Prints to the output stream for this specific constraint. + * + * @param stream The output stream + */ + void + print(std::ostream& stream) const override + { + stream << fmt::format("The minimum value is `{}`. The maximum value is `{}`", min_, max_); + } + NumType min_; NumType max_; }; @@ -341,6 +408,17 @@ class PositiveDouble final : public Constraint { */ [[nodiscard]] std::optional checkValueImpl(Value const& num) const override; + + /** + * @brief Prints to the output stream for this specific constraint. + * + * @param stream The output stream + */ + void + print(std::ostream& stream) const override + { + stream << fmt::format("The value must be a positive double number"); + } }; static constinit PortConstraint gValidatePort{}; diff --git a/src/util/newconfig/ConfigDefinition.cpp b/src/util/newconfig/ConfigDefinition.cpp index bad33717e..bbfff1d94 100644 --- a/src/util/newconfig/ConfigDefinition.cpp +++ b/src/util/newconfig/ConfigDefinition.cpp @@ -65,6 +65,7 @@ ClioConfigDefinition::getObject(std::string_view prefix, std::optional(mapVal)) { ASSERT(std::get(mapVal).size() > idx.value(), "Index provided is out of scope"); + // we want to support getObject("array") and getObject("array.[]"), so we check if "[]" exists if (!prefix.contains("[]")) return ObjectView{prefixWithDot + "[]", idx.value(), *this}; diff --git a/src/util/newconfig/ConfigDefinition.hpp b/src/util/newconfig/ConfigDefinition.hpp index 94a70b671..b3c5761a4 100644 --- a/src/util/newconfig/ConfigDefinition.hpp +++ b/src/util/newconfig/ConfigDefinition.hpp @@ -23,7 +23,6 @@ #include "util/Assert.hpp" #include "util/newconfig/Array.hpp" #include "util/newconfig/ConfigConstraints.hpp" -#include "util/newconfig/ConfigDescription.hpp" #include "util/newconfig/ConfigFileInterface.hpp" #include "util/newconfig/ConfigValue.hpp" #include "util/newconfig/Error.hpp" @@ -31,16 +30,10 @@ #include "util/newconfig/Types.hpp" #include "util/newconfig/ValueView.hpp" -#include -#include -#include - #include -#include #include #include #include -#include #include #include #include @@ -84,26 +77,6 @@ class ClioConfigDefinition { [[nodiscard]] std::optional> parse(ConfigFileInterface const& config); - /** - * @brief Validates the configuration file - * - * Should only check for valid values, without populating - * - * @param config The configuration file interface - * @return An optional vector of Error objects stating all the failures if validation fails - */ - [[nodiscard]] std::optional> - validate(ConfigFileInterface const& config) const; - - /** - * @brief Generate markdown file of all the clio config descriptions - * - * @param configDescription The configuration description object - * @return An optional Error if generating markdown fails - */ - [[nodiscard]] std::expected - getMarkdown(ClioConfigDescription const& configDescription) const; - /** * @brief Returns the ObjectView specified with the prefix * diff --git a/src/util/newconfig/ConfigDescription.hpp b/src/util/newconfig/ConfigDescription.hpp index 71335a66c..52fbc6d06 100644 --- a/src/util/newconfig/ConfigDescription.hpp +++ b/src/util/newconfig/ConfigDescription.hpp @@ -20,9 +20,19 @@ #pragma once #include "util/Assert.hpp" +#include "util/newconfig/ConfigDefinition.hpp" +#include "util/newconfig/Error.hpp" + +#include #include #include +#include +#include +#include +#include +#include +#include #include namespace util::config { @@ -61,26 +71,90 @@ struct ClioConfigDescription { return itr->value; } + /** + * @brief Generate markdown file of all the clio config descriptions + * + * @param path The path location to generate the Config-description file + * @return An Error if generating markdown fails, otherwise nothing + */ + [[nodiscard]] static std::expected + generateConfigDescriptionToFile(std::filesystem::path path) + { + namespace fs = std::filesystem; + + // Validate the directory exists + auto const dir = path.parent_path(); + if (!dir.empty() && !fs::exists(dir)) { + return std::unexpected{ + fmt::format("Error: Directory '{}' does not exist or provided path is invalid", dir.string()) + }; + } + + std::ofstream file(path.string()); + if (!file.is_open()) { + return std::unexpected{fmt::format("Failed to create file '{}': {}", path.string(), std::strerror(errno))}; + } + + writeConfigDescriptionToFile(file); + file.close(); + + std::cout << "Markdown file generated successfully: " << path << "\n"; + return {}; + } + + /** + * @brief Writes to Config description to file + * + * @param file The config file to write to + */ + static void + writeConfigDescriptionToFile(std::ostream& file) + { + file << "# Clio Config Description\n"; + file << "This file lists all Clio Configuration definitions in detail.\n\n"; + file << "## Configuration Details\n\n"; + + for (auto const& [key, val] : kCONFIG_DESCRIPTION) { + file << "### Key: " << key << "\n"; + + // Every type of value is directed to operator<< in ConfigValue.hpp + // as ConfigValue is the one that holds all the info regarding the config values + if (key.contains("[]")) { + file << gClioConfig.asArray(key); + } else { + file << gClioConfig.getValueView(key); + } + file << " - **Description**: " << val << "\n"; + } + file << "\n"; + } + private: static constexpr auto kCONFIG_DESCRIPTION = std::array{ - KV{.key = "database.type", .value = "Type of database to use. Default is Scylladb."}, + KV{.key = "database.type", + .value = "Type of database to use. We currently support Cassandra and Scylladb. We default to Scylladb."}, KV{.key = "database.cassandra.contact_points", - .value = - "A list of IP addresses or hostnames of the initial nodes (Cassandra/Scylladb cluster nodes) that the " - "client will connect to when establishing a connection with the database."}, + .value = "A list of IP addresses or hostnames of the initial nodes (Cassandra/Scylladb cluster nodes) that " + "the client will connect to when establishing a connection with the database. If you're running " + "locally, it should be 'localhost' or 127.0.0.1"}, KV{.key = "database.cassandra.secure_connect_bundle", .value = "Configuration file that contains the necessary security credentials and connection details for " "securely " "connecting to a Cassandra database cluster."}, - KV{.key = "database.cassandra.port", .value = "Port number to connect to Cassandra."}, - KV{.key = "database.cassandra.keyspace", .value = "Keyspace to use in Cassandra."}, - KV{.key = "database.cassandra.replication_factor", .value = "Number of replicated nodes for Scylladb."}, - KV{.key = "database.cassandra.table_prefix", .value = "Prefix for Cassandra table names."}, + KV{.key = "database.cassandra.port", .value = "Port number to connect to the database."}, + KV{.key = "database.cassandra.keyspace", .value = "Keyspace to use for the database."}, + KV{.key = "database.cassandra.replication_factor", + .value = "Number of replicated nodes for Scylladb. Visit this link for more details : " + "https://university.scylladb.com/courses/scylla-essentials-overview/lessons/high-availability/" + "topic/fault-tolerance-replication-factor/ "}, + KV{.key = "database.cassandra.table_prefix", .value = "Prefix for Database table names."}, KV{.key = "database.cassandra.max_write_requests_outstanding", - .value = "Maximum number of outstanding write requests."}, + .value = "Maximum number of outstanding write requests. Write requests are api calls that write to database " + }, KV{.key = "database.cassandra.max_read_requests_outstanding", - .value = "Maximum number of outstanding read requests."}, - KV{.key = "database.cassandra.threads", .value = "Number of threads for Cassandra operations."}, + .value = "Maximum number of outstanding read requests, which reads from database"}, + KV{.key = "database.cassandra.threads", .value = "Number of threads that will be used for database operations." + }, KV{.key = "database.cassandra.core_connections_per_host", .value = "Number of core connections per host for Cassandra."}, KV{.key = "database.cassandra.queue_size_io", .value = "Queue size for I/O operations in Cassandra."}, @@ -106,10 +180,10 @@ struct ClioConfigDescription { .value = "Timeout duration for the forwarding cache used in Rippled communication."}, KV{.key = "forwarding.request_timeout", .value = "Timeout duration for the forwarding request used in Rippled communication."}, - KV{.key = "rpc.cache_timeout", .value = "Timeout duration for the rpc request."}, + KV{.key = "rpc.cache_timeout", .value = "Timeout duration for RPC requests."}, KV{.key = "num_markers", - .value = "The number of markers is the number of coroutines to load the cache concurrently."}, - KV{.key = "dos_guard.[].whitelist", .value = "List of IP addresses to whitelist for DOS protection."}, + .value = "The number of markers is the number of coroutines to download the initial ledger"}, + KV{.key = "dos_guard.whitelist.[]", .value = "List of IP addresses to whitelist for DOS protection."}, KV{.key = "dos_guard.max_fetches", .value = "Maximum number of fetch operations allowed by DOS guard."}, KV{.key = "dos_guard.max_connections", .value = "Maximum number of concurrent connections allowed by DOS guard." }, @@ -120,39 +194,53 @@ struct ClioConfigDescription { KV{.key = "server.port", .value = "Port number of the Clio HTTP server."}, KV{.key = "server.max_queue_size", .value = "Maximum size of the server's request queue. Value of 0 is no limit."}, - KV{.key = "server.local_admin", .value = "Indicates if the server should run with admin privileges."}, - KV{.key = "server.admin_password", .value = "Password for Clio admin-only APIs."}, + KV{.key = "server.local_admin", + .value = "Indicates if the server should run with admin privileges. Only one of local_admin or " + "admin_password can be set."}, + KV{.key = "server.admin_password", + .value = "Password for Clio admin-only APIs. Only one of local_admin or admin_password can be set."}, KV{.key = "server.processing_policy", .value = R"(Could be "sequent" or "parallel". For the sequent policy, requests from a single client connection are processed one by one, with the next request read only after the previous one is processed. For the parallel policy, Clio will accept all requests and process them in parallel, sending a reply for each request as soon as it is ready.)"}, KV{.key = "server.parallel_requests_limit", - .value = R"(Optional parameter, used only if "processing_strategy" is - "parallel". It limits the number of requests for a single client connection that are processed in parallel. If not specified, the limit is infinite.)" + .value = + R"(Optional parameter, used only if processing_strategy `parallel`. It limits the number of requests for a single client connection that are processed in parallel. If not specified, the limit is infinite.)" }, KV{.key = "server.ws_max_sending_queue_size", .value = "Maximum size of the websocket sending queue."}, KV{.key = "prometheus.enabled", .value = "Enable or disable Prometheus metrics."}, KV{.key = "prometheus.compress_reply", .value = "Enable or disable compression of Prometheus responses."}, - KV{.key = "io_threads", .value = "Number of I/O threads. Value must be greater than 1"}, + KV{.key = "io_threads", .value = "Number of I/O threads. Value cannot be less than 1"}, KV{.key = "subscription_workers", .value = "The number of worker threads or processes that are responsible for managing and processing " - "subscription-based tasks."}, + "subscription-based tasks from rippled"}, KV{.key = "graceful_period", .value = "Number of milliseconds server will wait to shutdown gracefully."}, - KV{.key = "cache.num_diffs", .value = "Number of diffs to cache."}, + KV{.key = "cache.num_diffs", .value = "Number of diffs to cache. For more info, consult readme.md in etc"}, KV{.key = "cache.num_markers", .value = "Number of markers to cache."}, KV{.key = "cache.num_cursors_from_diff", .value = "Num of cursors that are different."}, KV{.key = "cache.num_cursors_from_account", .value = "Number of cursors from an account."}, KV{.key = "cache.page_fetch_size", .value = "Page fetch size for cache operations."}, KV{.key = "cache.load", .value = "Cache loading strategy ('sync' or 'async')."}, - KV{.key = "log_channels.[].channel", .value = "Name of the log channel."}, - KV{.key = "log_channels.[].log_level", .value = "Log level for the log channel."}, - KV{.key = "log_level", .value = "General logging level of Clio."}, + KV{.key = "log_channels.[].channel", + .value = "Name of the log channel." + "'RPC', 'ETL', and 'Performance'"}, + KV{.key = "log_channels.[].log_level", + .value = "Log level for the specific log channel." + "`warning`, `error`, `fatal`"}, + KV{.key = "log_level", + .value = "General logging level of Clio. This level will be applied to all log channels that do not have an " + "explicitly defined logging level."}, KV{.key = "log_format", .value = "Format string for log messages."}, KV{.key = "log_to_console", .value = "Enable or disable logging to console."}, KV{.key = "log_directory", .value = "Directory path for log files."}, - KV{.key = "log_rotation_size", .value = "Log rotation size in megabytes."}, + KV{.key = "log_rotation_size", + .value = + "Log rotation size in megabytes. When the log file reaches this particular size, a new log file starts." + }, KV{.key = "log_directory_max_size", .value = "Maximum size of the log directory in megabytes."}, - KV{.key = "log_rotation_hour_interval", .value = "Interval in hours for log rotation."}, + KV{.key = "log_rotation_hour_interval", + .value = "Interval in hours for log rotation. If the current log file reaches this value in logging, a new " + "log file starts."}, KV{.key = "log_tag_style", .value = "Style for log tags."}, KV{.key = "extractor_threads", .value = "Number of extractor threads."}, KV{.key = "read_only", .value = "Indicates if the server should have read-only privileges."}, @@ -164,8 +252,8 @@ struct ClioConfigDescription { KV{.key = "api_version.default", .value = "Default API version Clio will run on."}, KV{.key = "api_version.min", .value = "Minimum API version."}, KV{.key = "api_version.max", .value = "Maximum API version."}, - KV{.key = "migration.full_scan_threads", .value = "The number of threads used to scan table."}, - KV{.key = "migration.full_scan_jobs", .value = "The number of coroutines used to scan table."}, + KV{.key = "migration.full_scan_threads", .value = "The number of threads used to scan the table."}, + KV{.key = "migration.full_scan_jobs", .value = "The number of coroutines used to scan the table."}, KV{.key = "migration.cursors_per_job", .value = "The number of cursors each coroutine will scan."} }; }; diff --git a/src/util/newconfig/ConfigValue.hpp b/src/util/newconfig/ConfigValue.hpp index d68daaa60..d83025acf 100644 --- a/src/util/newconfig/ConfigValue.hpp +++ b/src/util/newconfig/ConfigValue.hpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -141,7 +142,7 @@ class ConfigValue { * * @return An optional reference to the associated Constraint. */ - [[nodiscard]] std::optional> + [[nodiscard]] constexpr std::optional> getConstraint() const { return cons_; @@ -201,6 +202,29 @@ class ConfigValue { return value_.value(); } + /** + * @brief Prints all the info of this config value to the output stream. + * + * @param stream The output stream + * @param val The config value to output to osstream + * @return The same ostream we were given + */ + friend std::ostream& + operator<<(std::ostream& stream, ConfigValue val) + { + stream << "- **Required**: " << (val.isOptional() ? "False" : "True") << "\n"; + stream << "- **Type**: " << val.type() << "\n"; + stream << "- **Default value**: " << (val.hasValue() ? *val.value_ : "None") << "\n"; + stream << "- **Constraints**: "; + + if (val.getConstraint().has_value()) { + stream << val.getConstraint()->get() << "\n"; + } else { + stream << "None" << "\n"; + } + return stream; + } + private: /** * @brief Checks if the value type is consistent with the specified ConfigType diff --git a/src/util/newconfig/Types.cpp b/src/util/newconfig/Types.cpp new file mode 100644 index 000000000..b8a6330db --- /dev/null +++ b/src/util/newconfig/Types.cpp @@ -0,0 +1,66 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2025, the clio developers. + + Permission to use, copy, modify, and 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. +*/ +//============================================================================== + +#include "util/newconfig/Types.hpp" + +#include +#include +#include +#include + +namespace util::config { + +std::ostream& +operator<<(std::ostream& stream, ConfigType type) +{ + switch (type) { + case ConfigType::Integer: + stream << "int"; + break; + case ConfigType::String: + stream << "string"; + break; + case ConfigType::Double: + stream << "double"; + break; + case ConfigType::Boolean: + stream << "boolean"; + break; + default: + stream << "unsupported type"; + } + return stream; +} + +std::ostream& +operator<<(std::ostream& stream, Value value) +{ + if (std::holds_alternative(value)) { + stream << std::get(value); + } else if (std::holds_alternative(value)) { + stream << (std::get(value) ? "False" : "True"); + } else if (std::holds_alternative(value)) { + stream << std::get(value); + } else if (std::holds_alternative(value)) { + stream << std::get(value); + } + return stream; +} + +} // namespace util::config diff --git a/src/util/newconfig/Types.hpp b/src/util/newconfig/Types.hpp index 42c9ddffa..bc2f1cfdf 100644 --- a/src/util/newconfig/Types.hpp +++ b/src/util/newconfig/Types.hpp @@ -22,8 +22,9 @@ #include "util/UnsupportedType.hpp" #include +#include +#include #include -#include #include namespace util::config { @@ -31,9 +32,29 @@ namespace util::config { /** @brief Custom clio config types */ enum class ConfigType { Integer, String, Double, Boolean }; +/** + * @brief Prints the specified config type to output stream + * + * @param stream The output stream + * @param type The config type + * @return The same ostream we were given + */ +std::ostream& +operator<<(std::ostream& stream, ConfigType type); + /** @brief Represents the supported Config Values */ using Value = std::variant; +/** + * @brief Prints the specified value to output stream + * + * @param stream The output stream + * @param value The value type + * @return The same ostream we were given + */ +std::ostream& +operator<<(std::ostream& stream, Value value); + /** * @brief Get the corresponding clio config type * diff --git a/src/util/newconfig/ValueView.hpp b/src/util/newconfig/ValueView.hpp index 078296fa6..d23848542 100644 --- a/src/util/newconfig/ValueView.hpp +++ b/src/util/newconfig/ValueView.hpp @@ -20,6 +20,7 @@ #pragma once #include "util/Assert.hpp" +#include "util/newconfig/ConfigConstraints.hpp" #include "util/newconfig/ConfigValue.hpp" #include "util/newconfig/Types.hpp" @@ -30,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -141,6 +143,17 @@ class ValueView { return configVal_.get().isOptional(); } + /** + * @brief Retrieves the constraint associated with the ConfigValue in this ValueView, if any. + * + * @return An optional reference to the associated Constraint + */ + [[nodiscard]] constexpr std::optional> + getConstraint() const + { + return configVal_.get().getConstraint(); + } + /** * @brief Retrieves the stored value as the specified type T * @@ -186,6 +199,20 @@ class ValueView { return std::make_optional(getValueImpl()); } + /** + * @brief Custom output stream for ValueView + * + * @param stream The output stream + * @param value The ValueView + * @return The same ostream we were given + */ + friend std::ostream& + operator<<(std::ostream& stream, ValueView value) + { + stream << value.configVal_; + return stream; + } + private: std::reference_wrapper configVal_; }; diff --git a/tests/common/util/TmpFile.hpp b/tests/common/util/TmpFile.hpp index f09647035..a74f80ff4 100644 --- a/tests/common/util/TmpFile.hpp +++ b/tests/common/util/TmpFile.hpp @@ -37,6 +37,12 @@ struct TmpFile { ofs << content; } + static TmpFile + empty() + { + return TmpFile{""}; + } + TmpFile(TmpFile const&) = delete; TmpFile(TmpFile&& other) : path{std::move(other.path)} { diff --git a/tests/unit/app/CliArgsTests.cpp b/tests/unit/app/CliArgsTests.cpp index d6e098420..fd5ecb5f2 100644 --- a/tests/unit/app/CliArgsTests.cpp +++ b/tests/unit/app/CliArgsTests.cpp @@ -18,12 +18,19 @@ //============================================================================== #include "app/CliArgs.hpp" +#include "util/TmpFile.hpp" +#include "util/newconfig/ConfigDefinition.hpp" +#include "util/newconfig/ConfigDescription.hpp" #include #include #include #include +#include +#include +#include +#include #include using namespace app; @@ -145,3 +152,71 @@ TEST_F(CliArgsTests, Parse_VerifyConfig) returnCode ); } + +TEST_F(CliArgsTests, Parse_ConfigDescriptionInvalidPath) +{ + using namespace util::config; + std::array argv{"clio_server", "--config-description", ""}; + auto const action = CliArgs::parse(argv.size(), argv.data()); + EXPECT_CALL(onExitMock, Call).WillOnce([](CliArgs::Action::Exit const& exit) { return exit.exitCode; }); + + EXPECT_EQ( + action.apply( + onRunMock.AsStdFunction(), + onExitMock.AsStdFunction(), + onMigrateMock.AsStdFunction(), + onVerifyMock.AsStdFunction() + ), + EXIT_FAILURE + ); +} + +struct CliArgsTestsWithTmpFile : CliArgsTests { + TmpFile tmpFile = TmpFile::empty(); +}; + +TEST_F(CliArgsTestsWithTmpFile, Parse_ConfigDescription) +{ + std::array argv{"clio_server", "--config-description", tmpFile.path.c_str()}; + auto const action = CliArgs::parse(argv.size(), argv.data()); + EXPECT_CALL(onExitMock, Call).WillOnce([](CliArgs::Action::Exit const& exit) { return exit.exitCode; }); + + // user provide config markdown file name as well + ASSERT_TRUE(std::filesystem::exists(tmpFile.path)); + + EXPECT_EQ( + action.apply( + onRunMock.AsStdFunction(), + onExitMock.AsStdFunction(), + onMigrateMock.AsStdFunction(), + onVerifyMock.AsStdFunction() + ), + EXIT_SUCCESS + ); +} + +TEST_F(CliArgsTestsWithTmpFile, Parse_ConfigDescriptionFileContent) +{ + using namespace util::config; + + std::ofstream file(tmpFile.path); + ASSERT_TRUE(file.is_open()); + ClioConfigDescription::writeConfigDescriptionToFile(file); + file.close(); + + std::ifstream inFile(tmpFile.path); + ASSERT_TRUE(inFile.is_open()); + + std::stringstream buffer; + buffer << inFile.rdbuf(); + inFile.close(); + + auto const fileContent = buffer.str(); + EXPECT_TRUE(fileContent.find("# Clio Config Description") != std::string::npos); + EXPECT_TRUE(fileContent.find("This file lists all Clio Configuration definitions in detail.") != std::string::npos); + EXPECT_TRUE(fileContent.find("## Configuration Details") != std::string::npos); + + // all keys that exist in clio config should be listed in config description file + for (auto const& key : gClioConfig) + EXPECT_TRUE(fileContent.find(key.first)); +} diff --git a/tests/unit/util/newconfig/ClioConfigDefinitionTests.cpp b/tests/unit/util/newconfig/ClioConfigDefinitionTests.cpp index 6d5846dd2..b2358a2e2 100644 --- a/tests/unit/util/newconfig/ClioConfigDefinitionTests.cpp +++ b/tests/unit/util/newconfig/ClioConfigDefinitionTests.cpp @@ -165,7 +165,10 @@ TEST(ConfigDescription, GetValues) { ClioConfigDescription const definition{}; - EXPECT_EQ(definition.get("database.type"), "Type of database to use. Default is Scylladb."); + EXPECT_EQ( + definition.get("database.type"), + "Type of database to use. We currently support Cassandra and Scylladb. We default to Scylladb." + ); EXPECT_EQ(definition.get("etl_sources.[].ip"), "IP address of the ETL source."); EXPECT_EQ(definition.get("prometheus.enabled"), "Enable or disable Prometheus metrics."); }