diff options
Diffstat (limited to 'lib/configuration')
-rw-r--r-- | lib/configuration/configuration.cpp | 68 | ||||
-rw-r--r-- | lib/configuration/configuration.h | 77 | ||||
-rw-r--r-- | lib/configuration/meson.build | 11 | ||||
-rw-r--r-- | lib/configuration/qt_specialization.cpp | 55 | ||||
-rw-r--r-- | lib/configuration/qt_specialization.h | 16 | ||||
-rw-r--r-- | lib/configuration/test/defaultrc.ini | 20 | ||||
-rw-r--r-- | lib/configuration/test/extrarc.ini | 8 | ||||
-rw-r--r-- | lib/configuration/test/parser.cpp | 126 |
8 files changed, 320 insertions, 61 deletions
diff --git a/lib/configuration/configuration.cpp b/lib/configuration/configuration.cpp index 34e84af..0fdd0c0 100644 --- a/lib/configuration/configuration.cpp +++ b/lib/configuration/configuration.cpp @@ -9,6 +9,7 @@ #include "configuration.h" #include <QStandardPaths> #include <algorithm> +#include <fstream> #include <iostream> #include <sstream> #include <stdexcept> @@ -36,20 +37,42 @@ Configuration::Configuration(std::initializer_list<std::pair<std::string, conf_v } } +void Configuration::read_file(const std::string &location) +{ + std::fstream fs(location, std::fstream::in); + if(fs.is_open()) { + read(fs); + fs.close(); + } +} + void Configuration::read(std::basic_istream<char> &input) { - std::string line, key, value; + std::string line, section, key, value; std::istringstream is_line; while(std::getline(input, line)) { + if(line.rfind("@@") == 0) { + if(line.rfind("@@include ") == 0) { + read_file(line.substr(10)); + } + continue; + } if(line[0] == '#' || line.length() == 0) continue; + if(line.front() == '[' && line.back() == ']') { + section = line.substr(1, line.length() - 2) + '/'; + continue; + } + is_line.clear(); is_line.str(line); - if(std::getline(is_line, key, '=')) { - is_line >> value; + const auto pos = line.find_first_of('='); + if(pos != std::string::npos) { + key = section + line.substr(0, pos); + value = line.substr(pos + 1); strip(key); strip(value); @@ -82,45 +105,26 @@ Configuration *Configuration::instance() return s_conf.get(); } -void setShortcut(QAction *action, const char *name) +std::ostream &operator<<(std::ostream &out, const Configuration &obj) { - if(!s_conf) - throw new std::runtime_error("Trying to set a shortcut, but no configuration has been set!"); + if(obj.use_global) { + if(!s_conf) { + throw new std::runtime_error("Trying to use default Configuration, but none has been set!"); + } - if(const auto shortcutText = s_conf->value<QString>(name)) { - const QString tooltip = action->toolTip(); - action->setShortcut(QKeySequence::fromString(shortcutText.value())); - action->setToolTip(QString("%1 (%2)").arg(tooltip, shortcutText.value())); + out << *s_conf; + return out; } -#ifdef QT_DEBUG - else { - std::cout << "fixme: setShortcut called for " << name << ", but no such value exists!" << std::endl; - } -#endif -} -std::ostream &operator<<(std::ostream &out, const Configuration &obj) -{ // unordered_map is, well, unordered, so grab the keys and sort them before printing them std::vector<std::string> keys; - - if(obj.use_global) { - if(!s_conf) - throw new std::runtime_error("Trying to use default Configuration, but none has been set!"); - - for(const auto &pair : *s_conf) - keys.emplace_back(pair.first); - } else { - for(const auto &pair : obj) - out << pair.first << "\n"; + for(const auto &pair : obj) { + keys.emplace_back(pair.first); } std::sort(keys.begin(), keys.end()); for(const auto &key : keys) { - if(obj.use_global) - out << key << "=" << s_conf->value<std::string>(key.c_str()).value() << "\n"; - else - out << key << "=" << obj.value<std::string>(key.c_str()).value() << "\n"; + out << key << "=" << obj.value<std::string>(key.c_str()).value() << "\n"; } return out; diff --git a/lib/configuration/configuration.h b/lib/configuration/configuration.h index 4459dd0..3d0a49d 100644 --- a/lib/configuration/configuration.h +++ b/lib/configuration/configuration.h @@ -10,11 +10,11 @@ #define SMOLBOTE_CONFIGURATION_H #include <QAction> -#include <QString> #include <initializer_list> #include <memory> #include <optional> #include <string> +#include <type_traits> #include <unordered_map> #include <variant> #include <vector> @@ -33,23 +33,37 @@ typedef std::variant<std::string, int, bool> conf_value_t; +template <typename T> +concept concept_value_t = std::is_arithmetic<T>::value || std::is_same<T, bool>::value || std::is_constructible<T, std::string>::value; + class consumable(unconsumed) Configuration : private std::unordered_map<std::string, conf_value_t> { friend std::ostream &operator<<(std::ostream &out, const Configuration &obj); public: return_typestate(unconsumed) explicit Configuration(); - return_typestate(unconsumed) explicit Configuration(std::initializer_list<std::pair<std::string, conf_value_t>> l) noexcept; - - explicit Configuration(Configuration && other param_typestate(unconsumed)) = default; + return_typestate(unconsumed) explicit Configuration(Configuration && other param_typestate(unconsumed)) = default; ~Configuration() = default; + callable_when(unconsumed) void read_file(const std::string &location); callable_when(unconsumed) void read(std::basic_istream<char> & input); template <typename T> - callable_when(unconsumed) std::optional<T> value(const char *path) const + callable_when(unconsumed) [[nodiscard]] std::optional<T> value(const char *path) const + { + if(use_global) + return instance()->value<T>(path); + + if(count(path) == 0) + return std::nullopt; + + return std::get<T>(at(path)); + } + + template <concept_value_t T> + callable_when(unconsumed) [[nodiscard]] std::optional<T> value(const char *path) const { if(use_global) return instance()->value<T>(path); @@ -61,33 +75,39 @@ public: // path is guaranteed to exist const auto value = at(path); - if constexpr(std::is_same_v<T, QString> || std::is_same_v<T, std::string>) { - auto r = [&value]() { - if(std::holds_alternative<std::string>(value)) - return std::get<std::string>(value); - else if(std::holds_alternative<int>(value)) - return std::to_string(std::get<int>(value)); + if(std::holds_alternative<int>(value)) { + if constexpr(std::is_arithmetic<T>::value) + return static_cast<T>(std::get<int>(value)); + else if constexpr(std::is_constructible<T, std::string>::value) + return T{ std::to_string(std::get<int>(value)) }; + + } else if(std::holds_alternative<bool>(value)) { + if constexpr(std::is_constructible<T, bool>::value) + return std::get<bool>(value); + else if constexpr(std::is_constructible<T, const char *>::value) { + if(std::get<bool>(value)) + return T{ "true" }; else - return std::get<bool>(value) ? std::string("true") : std::string("false"); - }(); + return T{ "false" }; + } - if(r.front() == '~') - r.replace(0, 1, m_homePath); + } else if(std::holds_alternative<std::string>(value)) { + auto str = std::get<std::string>(value); + if(str.front() == '~') + str.replace(0, 1, m_homePath); - if constexpr(std::is_same_v<T, QString>) - return std::make_optional(QString::fromStdString(r)); - else - return std::make_optional(r); - - } else if constexpr(std::is_same_v<T, QStringList>) { - return std::make_optional(QString::fromStdString(std::get<std::string>(value)).split(';')); + if constexpr(std::is_constructible<T, std::string>::value) + return T{ str }; + } - } else if(std::holds_alternative<T>(value)) { - return std::optional<T>(std::get<T>(value)); - } else - return std::nullopt; + return std::nullopt; + } - } // std::optional<T> value(path) const + template <typename T> + callable_when(unconsumed) T &shortcut(T &, const char *) const + { + return T{}; + } static void move_global(std::unique_ptr<Configuration> && conf); @@ -98,7 +118,8 @@ private: const bool use_global = false; }; -void setShortcut(QAction *action, const char *name); +#include "qt_specialization.h" + std::ostream &operator<<(std::ostream &out, const Configuration &obj); #endif // SMOLBOTE_CONFIGURATION_H diff --git a/lib/configuration/meson.build b/lib/configuration/meson.build index 939a493..a0d915c 100644 --- a/lib/configuration/meson.build +++ b/lib/configuration/meson.build @@ -1,5 +1,14 @@ dep_configuration = declare_dependency( include_directories: include_directories('.'), - link_with: static_library('configuration', ['configuration.cpp'], dependencies: dep_qt5) + link_with: static_library('configuration', ['configuration.cpp', 'qt_specialization.cpp'], dependencies: dep_qt5) +) + +test('configuration: parser', + executable('configuration-parser', + sources: [ 'test/parser.cpp' ], + dependencies: [ dep_qt5, dep_gtest, dep_configuration ] + ), + env: environment({ 'CONFIGFILE' : meson.current_source_dir()/'test/defaultrc.ini' }), + workdir: meson.current_source_dir()/'test' ) diff --git a/lib/configuration/qt_specialization.cpp b/lib/configuration/qt_specialization.cpp new file mode 100644 index 0000000..4e79492 --- /dev/null +++ b/lib/configuration/qt_specialization.cpp @@ -0,0 +1,55 @@ +#include "configuration.h" +#include <stdexcept> + +template <> +callable_when(unconsumed) [[nodiscard]] std::optional<QString> Configuration::value(const char *path) const +{ + const auto v = value<std::string>(path); + if(!v) + return std::nullopt; + else + return QString::fromStdString(v.value()); +} + +template <> +callable_when(unconsumed) [[nodiscard]] std::optional<QStringList> Configuration::value(const char *path) const +{ + const auto v = value<std::string>(path); + if(!v) + return std::nullopt; + else + return QString::fromStdString(v.value()).split(';'); +} + +void setShortcut(QAction *action, const char *name) +{ + Configuration conf; + + if(const auto shortcutText = conf.value<QString>(name)) { + const QString tooltip = action->toolTip(); + action->setShortcut(QKeySequence::fromString(shortcutText.value())); + action->setToolTip(QString("%1 (%2)").arg(tooltip, shortcutText.value())); + } else { + throw new std::runtime_error(std::string("fixme: setShortcut found no such value for ") + name); + } +} + +template <> +callable_when(unconsumed) QAction &Configuration::shortcut(QAction &action, const char *name) const +{ + if(const auto shortcut = value<QString>(name)) { + const QString old_tooltip = action.toolTip(); + action.setShortcut(QKeySequence::fromString(shortcut.value())); + action.setToolTip(QString("%1 (%2)").arg(old_tooltip, shortcut.value())); + } + return action; +} + +template <> +callable_when(unconsumed) QKeySequence &Configuration::shortcut(QKeySequence &sequence, const char *name) const +{ + if(const auto shortcut = value<QString>(name)) { + sequence = QKeySequence::fromString(shortcut.value()); + } + return sequence; +} diff --git a/lib/configuration/qt_specialization.h b/lib/configuration/qt_specialization.h new file mode 100644 index 0000000..8aabf6b --- /dev/null +++ b/lib/configuration/qt_specialization.h @@ -0,0 +1,16 @@ +#pragma once + +#include <QString> +#include <QStringList> + +template <> +callable_when(unconsumed) [[nodiscard]] std::optional<QString> Configuration::value(const char *path) const; +template <> +callable_when(unconsumed) [[nodiscard]] std::optional<QStringList> Configuration::value(const char *path) const; + +template <> +callable_when(unconsumed) QAction &Configuration::shortcut(QAction &, const char *) const; +template <> +callable_when(unconsumed) QKeySequence &Configuration::shortcut(QKeySequence &, const char *) const; + +[[deprecated("Use Configuration::shortcut<QAction>(name) instead")]] void setShortcut(QAction *action, const char *name); diff --git a/lib/configuration/test/defaultrc.ini b/lib/configuration/test/defaultrc.ini new file mode 100644 index 0000000..f83bf73 --- /dev/null +++ b/lib/configuration/test/defaultrc.ini @@ -0,0 +1,20 @@ +# this is a comment and should be ignored + +name= Top level +over=default +#comment=val +number=12 +toggle=true +list=one;two;three;for four + +[main] +name=Section Testing +number=10 +toggle=false + +[qt] +shortcut=Ctrl+Q + +# include another file, absolute path or relative path to the rundir +@@include extrarc.ini + diff --git a/lib/configuration/test/extrarc.ini b/lib/configuration/test/extrarc.ini new file mode 100644 index 0000000..e22bf09 --- /dev/null +++ b/lib/configuration/test/extrarc.ini @@ -0,0 +1,8 @@ + +over=extra + +[extra] +name=extra section +number=12 +toggle=true + diff --git a/lib/configuration/test/parser.cpp b/lib/configuration/test/parser.cpp new file mode 100644 index 0000000..3ad64a5 --- /dev/null +++ b/lib/configuration/test/parser.cpp @@ -0,0 +1,126 @@ +#include "configuration.h" +#include <QApplication> +#include <cstdlib> +#include <fstream> +#include <gtest/gtest.h> +#include <sstream> + +class ConfigurationTest : public ::testing::Test +{ +protected: + void SetUp() override + { + conf.read_file(std::getenv("CONFIGFILE")); + } + + Configuration conf{ + { "name", std::string() }, + { "over", std::string() }, + { "other", std::string("not in cfg") }, // this entry is not in the conf file + { "comment", std::string() }, // commented out entry in the conf file + { "number", int(0) }, + { "toggle", bool(false) }, + + { "main/name", std::string() }, + { "main/number", int(0) }, + { "main/toggle", bool(true) }, + + { "extra/name", std::string() }, + { "extra/number", int(0) }, + { "extra/toggle", bool(false) }, + }; + + std::unique_ptr<Configuration> global_conf = std::make_unique<Configuration, std::initializer_list<std::pair<std::string, conf_value_t>>>({ + { "name", std::string("global") }, + { "number", int(123) }, + { "toggle", bool(true) }, + + }); +}; + +TEST_F(ConfigurationTest, NoSection) +{ + EXPECT_EQ(conf.value<std::string>("name").value(), "Top level"); + EXPECT_EQ(conf.value<std::string>("other").value(), "not in cfg"); + EXPECT_EQ(conf.value<std::string>("comment").value(), ""); + EXPECT_EQ(conf.value<int>("number").value(), 12); + EXPECT_TRUE(conf.value<bool>("toggle").value()); + EXPECT_FALSE(conf.value<int>("nullopt")); + EXPECT_FALSE(conf.value<bool>("nullopt")); + EXPECT_FALSE(conf.value<std::string>("nullopt")); +} + +TEST_F(ConfigurationTest, TypeCasts) +{ + EXPECT_EQ(conf.value<std::string>("number").value(), "12"); + EXPECT_EQ(conf.value<std::string>("toggle").value(), "true"); + EXPECT_EQ(conf.value<std::string>("main/toggle").value(), "false"); +} + +TEST_F(ConfigurationTest, QtSpecialization) +{ + EXPECT_EQ(conf.value<QString>("name").value(), "Top level"); + EXPECT_EQ(conf.value<QString>("number").value(), "12"); + EXPECT_EQ(conf.value<QString>("toggle").value(), "true"); + EXPECT_EQ(conf.value<QString>("main/toggle").value(), "false"); + EXPECT_FALSE(conf.value<QString>("nullopt")); + + EXPECT_EQ(conf.value<QStringList>("list").value(), QStringList({ "one", "two", "three", "for four" })); + EXPECT_FALSE(conf.value<QStringList>("nullopt")); +} + +TEST_F(ConfigurationTest, QtShortcut) +{ + QAction action; + EXPECT_EQ(conf.shortcut<QAction>(action, "qt/shortcut").shortcut().toString(), "Ctrl+Q"); + EXPECT_EQ(conf.shortcut<QAction>(action, "qt/nil").shortcut().toString(), "Ctrl+Q"); + + QKeySequence sequence; + EXPECT_EQ(conf.shortcut<QKeySequence>(sequence, "qt/shortcut").toString(), "Ctrl+Q"); + EXPECT_EQ(conf.shortcut<QKeySequence>(sequence, "qt/nil").toString(), "Ctrl+Q"); +} + +TEST_F(ConfigurationTest, MainSection) +{ + EXPECT_EQ(conf.value<std::string>("main/name").value(), "Section Testing"); + EXPECT_EQ(conf.value<int>("main/number").value(), 10); + EXPECT_FALSE(conf.value<bool>("main/toggle").value()); +} + +TEST_F(ConfigurationTest, ExtraSection) +{ + EXPECT_EQ(conf.value<std::string>("over").value(), "extra"); + EXPECT_EQ(conf.value<std::string>("extra/name").value(), "extra section"); + EXPECT_EQ(conf.value<int>("extra/number").value(), 12); + EXPECT_TRUE(conf.value<bool>("extra/toggle").value()); +} + +TEST_F(ConfigurationTest, GlobalInstance) +{ + std::stringstream output; + + output << *global_conf; + EXPECT_EQ(output.str(), "name=global\nnumber=123\ntoggle=true\n") << "operator<< on global_conf before move"; + + Configuration::move_global(std::move(global_conf)); + Configuration g; + EXPECT_TRUE(g.value<std::string>("name")); + EXPECT_EQ(g.value<std::string>("name").value(), "global"); + EXPECT_TRUE(g.value<int>("number")); + EXPECT_EQ(g.value<int>("number").value(), 123); + EXPECT_TRUE(g.value<bool>("toggle")); + EXPECT_EQ(g.value<bool>("toggle").value(), true); + EXPECT_FALSE(g.value<std::string>("nullopt")); + + output.str(std::string()); + output << g; + EXPECT_EQ(output.str(), "name=global\nnumber=123\ntoggle=true\n") << "operator<< on global_conf after move"; +} + +int main(int argc, char **argv) +{ + QApplication a(argc, argv); + + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} |