diff --git a/lib/utils/meson.build b/lib/utils/meson.build index 62773962..93902e2b 100644 --- a/lib/utils/meson.build +++ b/lib/utils/meson.build @@ -16,14 +16,13 @@ lib_mu_utils=static_library('mu-utils', [ - 'mu-command-parser.cc', + 'mu-command-handler.cc', 'mu-logger.cc', 'mu-option.cc', 'mu-readline.cc', 'mu-sexp.cc', 'mu-test-utils.cc', 'mu-util.c', - 'mu-util.h', 'mu-utils.cc'], dependencies: [ glib_dep, @@ -39,4 +38,19 @@ lib_mu_utils_dep = declare_dependency( include_directories: include_directories(['.', '..']) ) +# +# tests +# +test('test-sexp', + executable('test-sexp', 'mu-sexp.cc', + install: false, + cpp_args: ['-DBUILD_TESTS'], + dependencies: [glib_dep, lib_mu_utils_dep])) + +test('test-command-handler', + executable('test-command-handler', 'mu-command-handler.cc', + install: false, + cpp_args: ['-DBUILD_TESTS'], + dependencies: [glib_dep, lib_mu_utils_dep])) + subdir('tests') diff --git a/lib/utils/mu-command-handler.cc b/lib/utils/mu-command-handler.cc new file mode 100644 index 00000000..efa2233e --- /dev/null +++ b/lib/utils/mu-command-handler.cc @@ -0,0 +1,248 @@ +/* +** Copyright (C) 2020-2022 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-command-handler.hh" +#include "mu-error.hh" +#include "mu-utils.hh" + +#include +#include + +using namespace Mu; + +Option> +Command::string_vec_arg(const std::string& name) const +{ + auto&& val{arg_val(name, Sexp::Type::List)}; + if (!val) + return Nothing; + + std::vector vec; + for (const auto& item : val->list()) { + if (!item.stringp()) { + // g_warning("command: non-string in string-list for %s: %s", + // name.c_str(), to_string().c_str()); + return Nothing; + } else + vec.emplace_back(item.string()); + } + + return vec; +} + +static Result +validate(const CommandHandler::CommandInfoMap& cmap, + const CommandHandler::CommandInfo& cmd_info, + const Command& cmd) +{ + if (g_test_verbose()) + std::cout << cmd.to_string(Sexp::Format::TypeInfo) << '\n'; + + // all required parameters must be present + for (auto&& arg : cmd_info.args) { + + const auto& argname{arg.first}; + const auto& arginfo{arg.second}; + + // calls use keyword-parameters, e.g. + // + // (my-function :bar 1 :cuux "fnorb") + // + // so, we're looking for the odd-numbered parameters. + const auto param_it = cmd.find_arg(argname); + const auto&& param_val = std::next(param_it); + // it's an error when a required parameter is missing. + if (param_it == cmd.cend()) { + if (arginfo.required) + return Err(Error::Code::Command, + "missing required parameter %s in command '%s'", + argname.c_str(), cmd.to_string().c_str()); + continue; // not required + } + + // the types must match, but the 'nil' symbol is acceptable as "no value" + if (param_val->type() != arginfo.type && !(param_val->nilp())) + return Err(Error::Code::Command, + "parameter %s expects type %s, but got %s in command '%s'", + argname.c_str(), + to_string(arginfo.type).c_str(), + to_string(param_val->type()).c_str(), + cmd.to_string().c_str()); + } + + // all parameters must be known + for (auto it = cmd.cbegin() + 1; it != cmd.cend() && it + 1 != cmd.cend(); it += 2) { + const auto& cmdargname{it->symbol()}; + if (std::none_of(cmd_info.args.cbegin(), cmd_info.args.cend(), + [&](auto&& arg) { return cmdargname == arg.first; })) + return Err(Error::Code::Command, + "unknown parameter '%s 'in command '%s'", + cmdargname.c_str(), cmd.to_string().c_str()); + } + + return Ok(); + +} + +Result +CommandHandler::invoke(const Command& cmd, bool do_validate) const +{ + const auto cmit{cmap_.find(cmd.name())}; + if (cmit == cmap_.cend()) + return Err(Error::Code::Command, + "unknown command in command '%s'", + cmd.to_string().c_str()); + + const auto& cmd_info{cmit->second}; + if (do_validate) { + if (auto&& res = validate(cmap_, cmd_info, cmd); !res) + return Err(res.error()); + } + + if (cmd_info.handler) + cmd_info.handler(cmd); + + return Ok(); +} + + +#ifdef BUILD_TESTS + +#include "mu-test-utils.hh" + + +static void +test_args() +{ + const auto cmd = Command::make_parse(R"((foo :bar 123 :cuux "456" :boo nil :bah true))"); + assert_valid_result(cmd); + + assert_equal(cmd->name(), "foo"); + g_assert_true(cmd->find_arg(":bar") != cmd->cend()); + g_assert_true(cmd->find_arg(":bxr") == cmd->cend()); + + g_assert_cmpint(cmd->number_arg(":bar").value_or(-1), ==, 123); + g_assert_cmpint(cmd->number_arg(":bor").value_or(-1), ==, -1); + + assert_equal(cmd->string_arg(":cuux").value_or(""), "456"); + assert_equal(cmd->string_arg(":caax").value_or(""), ""); // not present + assert_equal(cmd->string_arg(":bar").value_or("abc"), "abc"); // wrong type + + g_assert_false(cmd->boolean_arg(":boo")); + + + g_assert_true(cmd->boolean_arg(":bah")); +} + +using CommandInfoMap = CommandHandler::CommandInfoMap; +using ArgMap = CommandHandler::ArgMap; +using ArgInfo = CommandHandler::ArgInfo; +using CommandInfo = CommandHandler::CommandInfo; + + +static bool +call(const CommandInfoMap& cmap, const std::string& str) try { + + const auto cmd{Command::make_parse(str)}; + if (!cmd) + throw Error(Error::Code::Internal, "invalid sexp str"); + + const auto res{CommandHandler(cmap).invoke(*cmd)}; + return !!res; + +} catch (const Error& err) { + g_warning("%s", err.what()); + return false; +} + +static void +test_command() +{ + allow_warnings(); + + CommandInfoMap cmap; + cmap.emplace( + "my-command", + CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}}, + {":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}}, + "My command,", + {}}); + + g_assert_true(call(cmap, "(my-command :param1 \"hello\")")); + g_assert_true(call(cmap, "(my-command :param1 \"hello\" :param2 123)")); + + g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 123 :param3 xxx)")); +} + +static void +test_command2() +{ + allow_warnings(); + + CommandInfoMap cmap; + cmap.emplace("bla", + CommandInfo{ArgMap{ + {":foo", ArgInfo{Sexp::Type::Number, false, "foo"}}, + {":bar", ArgInfo{Sexp::Type::String, false, "bar"}}, + }, "yeah", + [&](const auto& params) {}}); + + g_assert_true(call(cmap, "(bla :foo nil)")); + g_assert_false(call(cmap, "(bla :foo nil :bla nil)")); +} + +static void +test_command_fail() +{ + allow_warnings(); + + CommandInfoMap cmap; + + cmap.emplace( + "my-command", + CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}}, + {":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}}, + "My command,", + {}}); + + g_assert_false(call(cmap, "(my-command)")); + g_assert_false(call(cmap, "(my-command2)")); + g_assert_false(call(cmap, "(my-command :param1 123 :param2 123)")); + g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 \"123\")")); +} + + +int +main(int argc, char* argv[]) try { + + mu_test_init(&argc, &argv); + + g_test_add_func("/utils/command-parser/args", test_args); + g_test_add_func("/utils/command-parser/command", test_command); + g_test_add_func("/utils/command-parser/command2", test_command2); + g_test_add_func("/utils/command-parser/command-fail", test_command_fail); + + return g_test_run(); + +} catch (const std::runtime_error& re) { + std::cerr << re.what() << "\n"; + return 1; +} + +#endif /*BUILD_TESTS*/ diff --git a/lib/utils/mu-command-handler.hh b/lib/utils/mu-command-handler.hh new file mode 100644 index 00000000..06dd49de --- /dev/null +++ b/lib/utils/mu-command-handler.hh @@ -0,0 +1,298 @@ +/* +** Copyright (C) 2020-2022 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ +#ifndef MU_COMMAND_HANDLER_HH__ +#define MU_COMMAND_HANDLER_HH__ + +#include +#include +#include +#include +#include +#include +#include + +#include "utils/mu-error.hh" +#include "utils/mu-sexp.hh" +#include "utils/mu-option.hh" + +namespace Mu { + +/// +/// Commands are s-expressions with the follow properties: + +/// 1) a command is a list with a command-name as its first argument +/// 2) the rest of the parameters are pairs of colon-prefixed symbol and a value of some +/// type (ie. 'keyword arguments') +/// 3) each command is described by its CommandInfo structure, which defines the type +/// 4) calls to the command must include all required parameters +/// 5) all parameters must be of the specified type; however the symbol 'nil' is allowed +/// for specify a non-required parameter to be absent; this is for convenience on the +/// call side. + +struct Command: public Sexp { + + using iterator = List::iterator; + using const_iterator = List::const_iterator; + + static Result make(Sexp&& sexp) try { + return Ok(Command{std::move(sexp)}); + } catch (const Error& e) { + return Err(e); + } + + static Result make_parse(const std::string& cmdstr) try { + if (auto&& sexp{Sexp::parse(cmdstr)}; !sexp) + return Err(sexp.error()); + else + return Ok(Command(std::move(*sexp))); + } catch (const Error& e) { + return Err(e); + } + + /** + * Get name of the command (first element) in a command exp + * + * @return name + */ + const std::string& name() const { + return cbegin()->symbol(); + } + + /** + * Find the argument with the given name. + * + * @param arg name + * + * @return iterator point at the argument, or cend + */ + const_iterator find_arg(const std::string& arg) const { + return find_prop(arg, cbegin() + 1, cend()); + } + + /** + * Get a string argument + * + * @param name of the argument + * + * @return ref to string, or Nothing if not found + */ + Option string_arg(const std::string& name) const { + if (auto&& val{arg_val(name, Sexp::Type::String)}; !val) + return Nothing; + else + return val->string(); + } + + /** + * Get a string-vec argument + * + * @param name of the argument + * + * @return ref to string-vec, or Nothing if not found or some error. + */ + Option> string_vec_arg(const std::string& name) const; + + /** + * Get a symbol argument + * + * @param name of the argument + * + * @return ref to symbol name, or Nothing if not found + */ + Option symbol_arg(const std::string& name) const { + if (auto&& val{arg_val(name, Sexp::Type::String)}; !val) + return Nothing; + else + return val->symbol(); + } + + /** + * Get a number argument + * + * @param name of the argument + * + * @return number or Nothing if not found + */ + Option number_arg(const std::string& name) const { + if (auto&& val{arg_val(name, Sexp::Type::Number)}; !val) + return Nothing; + else + return static_cast(val->number()); + } + + /* + * helpers + */ + + /** + * Get a boolean argument + * + * @param name of the argument + * + * @return true if there's a non-nil symbol value for the given + * name; false otherwise. + */ + Option bool_arg(const std::string& name) const { + if (auto&& symb{symbol_arg(name)}; !symb) + return Nothing; + else + return symb.value() == "nil" ? false : true; + } + + /** + * Treat any argument as a boolean + * + * @param name name of the argument + * + * @return false if the the argument is absent or the symbol false; + * otherwise true. + */ + bool boolean_arg(const std::string& name) const { + auto&& it{find_arg(name)}; + return (it == cend() || std::next(it)->nilp()) ? false : true; + } + +private: + explicit Command(Sexp&& s){ + *this = std::move(static_cast(s)); + if (!listp() || empty() || !cbegin()->symbolp() || + !plistp(cbegin() + 1, cend())) + throw Error(Error::Code::Command, + "expected command, got '%s'", to_string().c_str()); + } + + + Option arg_val(const std::string& name, Sexp::Type type) const { + if (auto&& it{find_arg(name)}; it == cend()) { + //std::cerr << "--> %s name found " << name << '\n'; + return Nothing; + } else if (auto&& val{it + 1}; val->type() != type) { + //std::cerr << "--> type " << Sexp::type_name(it->type()) << '\n'; + return Nothing; + } else + return *val; + } +}; + +struct CommandHandler { + + /// Information about a function argument + struct ArgInfo { + ArgInfo(Sexp::Type typearg, bool requiredarg, std::string&& docarg) + : type{typearg}, required{requiredarg}, docstring{std::move(docarg)} {} + const Sexp::Type type; /**< Sexp::Type of the argument */ + const bool required; /**< Is this argument required? */ + const std::string docstring; /**< Documentation */ + }; + + /// The arguments for a function, which maps their names to the information. + using ArgMap = std::unordered_map; + + // A handler function + using Handler = std::function; + + /// Information about some command + struct CommandInfo { + CommandInfo(ArgMap&& argmaparg, std::string&& docarg, Handler&& handlerarg) + : args{std::move(argmaparg)}, docstring{std::move(docarg)}, + handler{std::move(handlerarg)} {} + const ArgMap args; + const std::string docstring; + const Handler handler; + + /** + * Get a sorted list of argument names, for display. Required args come + * first, then alphabetical. + * + * @return vec with the sorted names. + */ + std::vector sorted_argnames() const { + // sort args -- by required, then alphabetical. + std::vector names; + for (auto&& arg : args) + names.emplace_back(arg.first); + std::sort(names.begin(), names.end(), [&](const auto& name1, const auto& name2) { + const auto& arg1{args.find(name1)->second}; + const auto& arg2{args.find(name2)->second}; + if (arg1.required != arg2.required) + return arg1.required; + else + return name1 < name2; + }); + return names; + } + + }; + + /// All commands, mapping their name to information about them. + using CommandInfoMap = std::unordered_map; + + CommandHandler(const CommandInfoMap& cmap): cmap_{cmap} {} + CommandHandler(CommandInfoMap&& cmap): cmap_{std::move(cmap)} {} + + const CommandInfoMap& info_map() const { return cmap_; } + + /** + * Invoke some command + * + * A command uses keyword arguments, e.g. something like: (foo :bar 1 + * :cuux "fnorb") + * + * @param cmd a Sexp describing a command call + * @param validate whether to validate before invoking. Useful during + * development. + * + * Return Ok() or some Error + */ + Result invoke(const Command& cmd, bool validate=true) const; + +private: + const CommandInfoMap cmap_; +}; + +static inline std::ostream& +operator<<(std::ostream& os, const CommandHandler::ArgInfo& info) +{ + os << info.type << " (" << (info.required ? "required" : "optional") << ")"; + + return os; +} + +static inline std::ostream& +operator<<(std::ostream& os, const CommandHandler::CommandInfo& info) +{ + for (auto&& arg : info.args) + os << " " << arg.first << " " << arg.second << '\n' + << " " << arg.second.docstring << "\n"; + + return os; +} + +static inline std::ostream& +operator<<(std::ostream& os, const CommandHandler::CommandInfoMap& map) +{ + for (auto&& c : map) + os << c.first << '\n' << c.second; + + return os; +} + +} // namespace Mu + +#endif /* MU_COMMAND_HANDLER_HH__ */ diff --git a/lib/utils/mu-command-parser.cc b/lib/utils/mu-command-parser.cc deleted file mode 100644 index 40be1e9c..00000000 --- a/lib/utils/mu-command-parser.cc +++ /dev/null @@ -1,204 +0,0 @@ -/* -** Copyright (C) 2020 Dirk-Jan C. Binnema -** -** 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, 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, write to the Free Software Foundation, -** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -** -*/ - -#include "mu-command-parser.hh" -#include "mu-error.hh" -#include "mu-utils.hh" - -#include -#include - -using namespace Mu; -using namespace Command; - -void -Command::invoke(const Command::CommandMap& cmap, const Sexp& call) -{ - if (!call.is_call()) { - throw Mu::Error{Error::Code::Command, - "expected call-sexpr but got %s", - call.to_sexp_string().c_str()}; - } - - const auto& params{call.list()}; - const auto cmd_it = cmap.find(params.at(0).value()); - if (cmd_it == cmap.end()) - throw Mu::Error{Error::Code::Command, - "unknown command in call %s", - call.to_sexp_string().c_str()}; - - const auto& cinfo{cmd_it->second}; - - // all required parameters must be present - for (auto&& arg : cinfo.args) { - const auto& argname{arg.first}; - const auto& arginfo{arg.second}; - - // calls used keyword-parameters, e.g. - // (my-function :bar 1 :cuux "fnorb") - // so, we're looking for the odd-numbered parameters. - const auto param_it = [&]() -> Sexp::Seq::const_iterator { - for (size_t i = 1; i < params.size(); i += 2) - if (params.at(i).is_symbol() && params.at(i).value() == argname) - return params.begin() + i + 1; - - return params.end(); - }(); - - // it's an error when a required parameter is missing. - if (param_it == params.end()) { - if (arginfo.required) - throw Mu::Error{Error::Code::Command, - "missing required parameter %s in call %s", - argname.c_str(), - call.to_sexp_string().c_str()}; - continue; // not required - } - - // the types must match, but the 'nil' symbol is acceptable as - // "no value" - if (param_it->type() != arginfo.type && !(param_it->is_nil())) - throw Mu::Error{Error::Code::Command, - "parameter %s expects type %s, but got %s in call %s", - argname.c_str(), - to_string(arginfo.type).c_str(), - to_string(param_it->type()).c_str(), - call.to_sexp_string().c_str()}; - } - - // all passed parameters must be known - for (size_t i = 1; i < params.size(); i += 2) { - if (std::none_of(cinfo.args.begin(), cinfo.args.end(), [&](auto&& arg) { - return params.at(i).value() == arg.first; - })) - throw Mu::Error{Error::Code::Command, - "unknown parameter %s in call %s", - params.at(i).value().c_str(), - call.to_sexp_string().c_str()}; - } - - if (cinfo.handler) - cinfo.handler(params); -} - -static Sexp::Seq::const_iterator -find_param_node(const Parameters& params, const std::string& argname) -{ - if (params.empty()) - throw Error(Error::Code::InvalidArgument, "params must not be empty"); - - if (argname.empty() || argname.at(0) != ':') - throw Error(Error::Code::InvalidArgument, - "property key must start with ':' but got '%s')", - argname.c_str()); - - for (size_t i = 1; i < params.size(); i += 2) { - if (i + 1 != params.size() && params.at(i).is_symbol() && - params.at(i).value() == argname) - return params.begin() + i + 1; - } - - return params.end(); -} - -static Error -wrong_type(Sexp::Type expected, Sexp::Type got) -{ - return Error(Error::Code::InvalidArgument, - "expected <%s> but got <%s>", - to_string(expected).c_str(), - to_string(got).c_str()); -} - -Option -Command::get_string(const Parameters& params, const std::string& argname) -{ - const auto it = find_param_node(params, argname); - if (it == params.end() || it->is_nil()) - return Nothing; - else if (!it->is_string()) - throw wrong_type(Sexp::Type::String, it->type()); - else - return it->value(); -} - -Option -Command::get_symbol(const Parameters& params, const std::string& argname) -{ - const auto it = find_param_node(params, argname); - if (it == params.end() || it->is_nil()) - return Nothing; - else if (!it->is_symbol()) - throw wrong_type(Sexp::Type::Symbol, it->type()); - else - return it->value(); -} - -Option -Command::get_int(const Parameters& params, const std::string& argname) -{ - const auto it = find_param_node(params, argname); - if (it == params.end() || it->is_nil()) - return Nothing; - else if (!it->is_number()) - throw wrong_type(Sexp::Type::Number, it->type()); - else - return ::atoi(it->value().c_str()); -} - -Option -Command::get_unsigned(const Parameters& params, const std::string& argname) -{ - if (auto val = get_int(params, argname); val && *val >= 0) - return val; - else - return Nothing; -} - - -Option -Command::get_bool(const Parameters& params, const std::string& argname) -{ - const auto it = find_param_node(params, argname); - if (it == params.end()) - return Nothing; - else if (!it->is_symbol()) - throw wrong_type(Sexp::Type::Symbol, it->type()); - else - return it->is_nil() ? false : true; -} - -std::vector -Command::get_string_vec(const Parameters& params, const std::string& argname) -{ - const auto it = find_param_node(params, argname); - if (it == params.end() || it->is_nil()) - return {}; - else if (!it->is_list()) - throw wrong_type(Sexp::Type::List, it->type()); - - std::vector vec; - for (const auto& n : it->list()) { - if (!n.is_string()) - throw wrong_type(Sexp::Type::String, n.type()); - vec.emplace_back(n.value()); - } - - return vec; -} diff --git a/lib/utils/mu-command-parser.hh b/lib/utils/mu-command-parser.hh deleted file mode 100644 index 751e5d8e..00000000 --- a/lib/utils/mu-command-parser.hh +++ /dev/null @@ -1,180 +0,0 @@ -/* -** Copyright (C) 2020 Dirk-Jan C. Binnema -** -** 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, 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, write to the Free Software Foundation, -** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -** -*/ -#ifndef MU_COMMAND_PARSER_HH__ -#define MU_COMMAND_PARSER_HH__ - -#include -#include -#include -#include -#include -#include -#include - -#include "utils/mu-error.hh" -#include "utils/mu-sexp.hh" -#include "utils/mu-option.hh" - -namespace Mu { -namespace Command { - -/// -/// Commands are s-expressions with the follow properties: - -/// 1) a command is a list with a command-name as its first argument -/// 2) the rest of the parameters are pairs of colon-prefixed symbol and a value of some -/// type (ie. 'keyword arguments') -/// 3) each command is described by its CommandInfo structure, which defines the type -/// 4) calls to the command must include all required parameters -/// 5) all parameters must be of the specified type; however the symbol 'nil' is allowed -/// for specify a non-required parameter to be absent; this is for convenience on the -/// call side. - -/// Information about a function argument -struct ArgInfo { - ArgInfo(Sexp::Type typearg, bool requiredarg, std::string&& docarg) - : type{typearg}, required{requiredarg}, docstring{std::move(docarg)} - { - } - const Sexp::Type type; /**< Sexp::Type of the argument */ - const bool required; /**< Is this argument required? */ - const std::string docstring; /**< Documentation */ -}; - -/// The arguments for a function, which maps their names to the information. -using ArgMap = std::unordered_map; -// The parameters to a Handler. -using Parameters = Sexp::Seq; - -Option get_int(const Parameters& parms, const std::string& argname); -Option get_unsigned(const Parameters& parms, const std::string& argname); -Option get_bool(const Parameters& parms, const std::string& argname); -Option get_string(const Parameters& parms, const std::string& argname); -Option get_symbol(const Parameters& parms, const std::string& argname); - -std::vector get_string_vec(const Parameters& params, const std::string& argname); - -/* - * backward compat - */ -static inline int -get_int_or(const Parameters& parms, const std::string& arg, int alt = 0) { - return get_int(parms, arg).value_or(alt); -} - -static inline bool -get_bool_or(const Parameters& parms, const std::string& arg, bool alt = false) { - return get_bool(parms, arg).value_or(alt); -} -static inline std::string -get_string_or(const Parameters& parms, const std::string& arg, const std::string& alt = ""){ - return get_string(parms, arg).value_or(alt); -} - -static inline std::string -get_symbol_or(const Parameters& parms, const std::string& arg, const std::string& alt = "nil") { - return get_symbol(parms, arg).value_or(alt); -} - - - - -// A handler function -using Handler = std::function; - -/// Information about some command -struct CommandInfo { - CommandInfo(ArgMap&& argmaparg, std::string&& docarg, Handler&& handlerarg) - : args{std::move(argmaparg)}, docstring{std::move(docarg)}, handler{ - std::move(handlerarg)} - { - } - const ArgMap args; - const std::string docstring; - const Handler handler; - - /** - * Get a sorted list of argument names, for display. Required args come - * first, then alphabetical. - * - * @return vec with the sorted names. - */ - std::vector sorted_argnames() const - { // sort args -- by required, then alphabetical. - std::vector names; - for (auto&& arg : args) - names.emplace_back(arg.first); - std::sort(names.begin(), names.end(), [&](const auto& name1, const auto& name2) { - const auto& arg1{args.find(name1)->second}; - const auto& arg2{args.find(name2)->second}; - if (arg1.required != arg2.required) - return arg1.required; - else - return name1 < name2; - }); - return names; - } -}; -/// All commands, mapping their name to information about them. -using CommandMap = std::unordered_map; - -/** - * Validate that the call (a Sexp) specifies a valid call, then invoke it. - * - * A call uses keyword arguments, e.g. something like: - * (foo :bar 1 :cuux "fnorb") - * - * On error, throw Error. - * - * @param cmap map of commands - * @param call node describing a call. - */ -void invoke(const Command::CommandMap& cmap, const Sexp& call); - -static inline std::ostream& -operator<<(std::ostream& os, const Command::ArgInfo& info) -{ - os << info.type << " (" << (info.required ? "required" : "optional") << ")"; - - return os; -} - -static inline std::ostream& -operator<<(std::ostream& os, const Command::CommandInfo& info) -{ - for (auto&& arg : info.args) - os << " " << arg.first << " " << arg.second << '\n' - << " " << arg.second.docstring << "\n"; - - return os; -} - -static inline std::ostream& -operator<<(std::ostream& os, const Command::CommandMap& map) -{ - for (auto&& c : map) - os << c.first << '\n' << c.second; - - return os; -} - -} // namespace Command -} // namespace Mu - -#endif /* MU_COMMAND_PARSER_HH__ */ diff --git a/lib/utils/tests/meson.build b/lib/utils/tests/meson.build index 85dc3c5b..335fcfa7 100644 --- a/lib/utils/tests/meson.build +++ b/lib/utils/tests/meson.build @@ -18,11 +18,6 @@ ################################################################################ # tests # -test('test-command-parser', - executable('test-command-parser', - 'test-command-parser.cc', - install: false, - dependencies: [glib_dep, lib_mu_utils_dep])) test('test-mu-util', executable('test-mu-util', 'test-mu-util.c', @@ -38,8 +33,3 @@ test('test-mu-utils', 'test-utils.cc', install: false, dependencies: [glib_dep, lib_mu_utils_dep])) -test('test-sexp', - executable('test-sexp', - 'test-sexp.cc', - install: false, - dependencies: [glib_dep, lib_mu_utils_dep] )) diff --git a/lib/utils/tests/test-command-parser.cc b/lib/utils/tests/test-command-parser.cc deleted file mode 100644 index 4156b03b..00000000 --- a/lib/utils/tests/test-command-parser.cc +++ /dev/null @@ -1,149 +0,0 @@ -/* -** Copyright (C) 2022 Dirk-Jan C. Binnema -** -** This library is free software; you can redistribute it and/or -** modify it under the terms of the GNU Lesser General Public License -** as published by the Free Software Foundation; either version 2.1 -** of the License, or (at your option) any later version. -** -** This library 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 -** Lesser General Public License for more details. -** -** You should have received a copy of the GNU Lesser General Public -** License along with this library; if not, write to the Free -** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA -** 02110-1301, USA. -*/ - -#include -#include - -#include -#include - -#include "mu-command-parser.hh" -#include "mu-utils.hh" -#include "mu-test-utils.hh" - -using namespace Mu; - -static void -test_param_getters() -{ - const auto sexp{Sexp::make_parse(R"((foo :bar 123 :cuux "456" :boo nil :bah true))")}; - - if (g_test_verbose()) - std::cout << sexp << "\n"; - - g_assert_cmpint(Command::get_int_or(sexp.list(), ":bar"), ==, 123); - assert_equal(Command::get_string_or(sexp.list(), ":bra", "bla"), "bla"); - assert_equal(Command::get_string_or(sexp.list(), ":cuux"), "456"); - - g_assert_true(Command::get_bool_or(sexp.list(), ":boo") == false); - g_assert_true(Command::get_bool_or(sexp.list(), ":bah") == true); -} - -static bool -call(const Command::CommandMap& cmap, const std::string& str) -try { - const auto sexp{Sexp::make_parse(str)}; - invoke(cmap, sexp); - - return true; - -} catch (const Error& err) { - g_warning("%s", err.what()); - return false; -} - -static void -test_command() -{ - using namespace Command; - allow_warnings(); - - CommandMap cmap; - - cmap.emplace( - "my-command", - CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}}, - {":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}}, - "My command,", - {}}); - - g_assert_true(call(cmap, "(my-command :param1 \"hello\")")); - g_assert_true(call(cmap, "(my-command :param1 \"hello\" :param2 123)")); - - g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 123 :param3 xxx)")); -} - -static void -test_command2() -{ - using namespace Command; - allow_warnings(); - - CommandMap cmap; - cmap.emplace("bla", - CommandInfo{ArgMap{ - {":foo", ArgInfo{Sexp::Type::Number, false, "foo"}}, - {":bar", ArgInfo{Sexp::Type::String, false, "bar"}}, - }, - "yeah", - [&](const auto& params) {}}); - - g_assert_true(call(cmap, "(bla :foo nil)")); - g_assert_false(call(cmap, "(bla :foo nil :bla nil)")); -} - -static void -test_command_fail() -{ - using namespace Command; - - allow_warnings(); - - CommandMap cmap; - - cmap.emplace( - "my-command", - CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}}, - {":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}}, - "My command,", - {}}); - - g_assert_false(call(cmap, "(my-command)")); - g_assert_false(call(cmap, "(my-command2)")); - g_assert_false(call(cmap, "(my-command :param1 123 :param2 123)")); - g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 \"123\")")); -} - -static void -black_hole() -{ -} - -int -main(int argc, char* argv[]) try { - - mu_test_init(&argc, &argv); - - g_test_add_func("/utils/command-parser/param-getters", test_param_getters); - g_test_add_func("/utils/command-parser/command", test_command); - g_test_add_func("/utils/command-parser/command2", test_command2); - g_test_add_func("/utils/command-parser/command-fail", test_command_fail); - - g_log_set_handler( - NULL, - (GLogLevelFlags)(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION), - (GLogFunc)black_hole, - NULL); - - return g_test_run(); - -} catch (const std::runtime_error& re) { - std::cerr << re.what() << "\n"; - return 1; -} diff --git a/lib/utils/tests/test-sexp.cc b/lib/utils/tests/test-sexp.cc deleted file mode 100644 index 3cb1c5a5..00000000 --- a/lib/utils/tests/test-sexp.cc +++ /dev/null @@ -1,190 +0,0 @@ -/* -** Copyright (C) 2020 Dirk-Jan C. Binnema -** -** This library is free software; you can redistribute it and/or -** modify it under the terms of the GNU Lesser General Public License -** as published by the Free Software Foundation; either version 2.1 -** of the License, or (at your option) any later version. -** -** This library 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 -** Lesser General Public License for more details. -** -** You should have received a copy of the GNU Lesser General Public -** License along with this library; if not, write to the Free -** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA -** 02110-1301, USA. -*/ - -#include -#include - -#include -#include - -#include "mu-command-parser.hh" -#include "mu-utils.hh" -#include "mu-test-utils.hh" - -using namespace Mu; - -static bool -check_parse(const std::string& expr, const std::string& expected) -{ - try { - const auto parsed{to_string(Sexp::make_parse(expr))}; - assert_equal(parsed, expected); - return true; - - } catch (const Error& err) { - g_warning("caught exception parsing '%s': %s", expr.c_str(), err.what()); - return false; - } -} - -static void -test_parser() -{ - check_parse(":foo-123", ":foo-123"); - check_parse("foo", "foo"); - check_parse(R"(12345)", "12345"); - check_parse(R"(-12345)", "-12345"); - check_parse(R"((123 bar "cuux"))", "(123 bar \"cuux\")"); - - check_parse(R"("foo\"bar\"cuux")", "\"foo\\\"bar\\\"cuux\""); - - check_parse(R"("foo -bar")", - "\"foo\nbar\""); -} - -static void -test_list() -{ - const auto nstr{Sexp::make_string("foo")}; - g_assert_true(nstr.value() == "foo"); - g_assert_true(nstr.type() == Sexp::Type::String); - assert_equal(nstr.to_sexp_string(), "\"foo\""); - - const auto nnum{Sexp::make_number(123)}; - g_assert_true(nnum.value() == "123"); - g_assert_true(nnum.type() == Sexp::Type::Number); - assert_equal(nnum.to_sexp_string(), "123"); - - const auto nsym{Sexp::make_symbol("blub")}; - g_assert_true(nsym.value() == "blub"); - g_assert_true(nsym.type() == Sexp::Type::Symbol); - assert_equal(nsym.to_sexp_string(), "blub"); - - Sexp::List list; - list.add(Sexp::make_string("foo")) - .add(Sexp::make_number(123)) - .add(Sexp::make_symbol("blub")); - - const auto nlst = Sexp::make_list(std::move(list)); - g_assert_true(nlst.list().size() == 3); - g_assert_true(nlst.type() == Sexp::Type::List); - g_assert_true(nlst.list().at(1).value() == "123"); - - assert_equal(nlst.to_sexp_string(), "(\"foo\" 123 blub)"); -} - -static void -test_prop_list() -{ - Sexp::List l1; - l1.add_prop(":foo", Sexp::make_string("bar")); - Sexp s2{Sexp::make_list(std::move(l1))}; - assert_equal(s2.to_sexp_string(), "(:foo \"bar\")"); - g_assert_true(s2.is_prop_list()); - - Sexp::List l2; - const std::string x{"bar"}; - l2.add_prop(":foo", Sexp::make_string(x)); - l2.add_prop(":bar", Sexp::make_number(77)); - Sexp::List l3; - l3.add_prop(":cuux", Sexp::make_list(std::move(l2))); - Sexp s3{Sexp::make_list(std::move(l3))}; - assert_equal(s3.to_sexp_string(), "(:cuux (:foo \"bar\" :bar 77))"); -} - -static void -test_props() -{ - auto sexp2 = Sexp::make_list(Sexp::make_string("foo"), - Sexp::make_number(123), - Sexp::make_symbol("blub")); - - auto sexp = Sexp::make_prop_list(":foo", - Sexp::make_string("bär"), - ":cuux", - Sexp::make_number(123), - ":flub", - Sexp::make_symbol("fnord"), - ":boo", - std::move(sexp2)); - - assert_equal(sexp.to_sexp_string(), - "(:foo \"b\303\244r\" :cuux 123 :flub fnord :boo (\"foo\" 123 blub))"); -} - -static void -test_prop_list_remove() -{ - { - Sexp::List lst; - lst.add_prop(":foo", Sexp::make_string("123")) - .add_prop(":bar", Sexp::make_number(123)); - - assert_equal(Sexp::make_list(std::move(lst)).to_sexp_string(), - R"((:foo "123" :bar 123))"); - } - - { - Sexp::List lst; - lst.add_prop(":foo", Sexp::make_string("123")) - .add_prop(":bar", Sexp::make_number(123)); - - assert_equal(Sexp::make_list(Sexp::List{lst}).to_sexp_string(), - R"((:foo "123" :bar 123))"); - - lst.remove_prop(":bar"); - - assert_equal(Sexp::make_list(Sexp::List{lst}).to_sexp_string(), - R"((:foo "123"))"); - - lst.clear(); - g_assert_cmpuint(lst.size(), ==, 0); - } - - { - Sexp::List lst; - lst.add(Sexp::make_number(123)); - Sexp s2{Sexp::make_list(std::move(lst))}; - g_assert_false(s2.is_prop_list()); - } -} - -int -main(int argc, char* argv[]) -try { - mu_test_init(&argc, &argv); - - if (argc == 2) { - std::cout << Sexp::make_parse(argv[1]) << '\n'; - return 0; - } - - g_test_add_func("/utils/sexp/parser", test_parser); - g_test_add_func("/utils/sexp/list", test_list); - g_test_add_func("/utils/sexp/proplist", test_prop_list); - g_test_add_func("/utils/sexp/proplist-remove", test_prop_list_remove); - g_test_add_func("/utils/sexp/props", test_props); - - return g_test_run(); - -} catch (const std::runtime_error& re) { - std::cerr << re.what() << "\n"; - return 1; -}