command-handler: rework for new sexp
Rework / cleanup the command-handler (and rename for command-parser). Update tests (and integrate with sources)
This commit is contained in:
@ -16,14 +16,13 @@
|
|||||||
|
|
||||||
|
|
||||||
lib_mu_utils=static_library('mu-utils', [
|
lib_mu_utils=static_library('mu-utils', [
|
||||||
'mu-command-parser.cc',
|
'mu-command-handler.cc',
|
||||||
'mu-logger.cc',
|
'mu-logger.cc',
|
||||||
'mu-option.cc',
|
'mu-option.cc',
|
||||||
'mu-readline.cc',
|
'mu-readline.cc',
|
||||||
'mu-sexp.cc',
|
'mu-sexp.cc',
|
||||||
'mu-test-utils.cc',
|
'mu-test-utils.cc',
|
||||||
'mu-util.c',
|
'mu-util.c',
|
||||||
'mu-util.h',
|
|
||||||
'mu-utils.cc'],
|
'mu-utils.cc'],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
glib_dep,
|
glib_dep,
|
||||||
@ -39,4 +38,19 @@ lib_mu_utils_dep = declare_dependency(
|
|||||||
include_directories: include_directories(['.', '..'])
|
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')
|
subdir('tests')
|
||||||
|
|||||||
248
lib/utils/mu-command-handler.cc
Normal file
248
lib/utils/mu-command-handler.cc
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
/*
|
||||||
|
** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||||
|
**
|
||||||
|
** 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 <iostream>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
using namespace Mu;
|
||||||
|
|
||||||
|
Option<std::vector<std::string>>
|
||||||
|
Command::string_vec_arg(const std::string& name) const
|
||||||
|
{
|
||||||
|
auto&& val{arg_val(name, Sexp::Type::List)};
|
||||||
|
if (!val)
|
||||||
|
return Nothing;
|
||||||
|
|
||||||
|
std::vector<std::string> 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<void>
|
||||||
|
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<void>
|
||||||
|
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*/
|
||||||
298
lib/utils/mu-command-handler.hh
Normal file
298
lib/utils/mu-command-handler.hh
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
/*
|
||||||
|
** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||||
|
**
|
||||||
|
** 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 <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <ostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <functional>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#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<Command> make(Sexp&& sexp) try {
|
||||||
|
return Ok(Command{std::move(sexp)});
|
||||||
|
} catch (const Error& e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Result<Command> 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<const std::string&> 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<std::vector<std::string>> 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<const std::string&> 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<int> number_arg(const std::string& name) const {
|
||||||
|
if (auto&& val{arg_val(name, Sexp::Type::Number)}; !val)
|
||||||
|
return Nothing;
|
||||||
|
else
|
||||||
|
return static_cast<int>(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> 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<Command&&>(s));
|
||||||
|
if (!listp() || empty() || !cbegin()->symbolp() ||
|
||||||
|
!plistp(cbegin() + 1, cend()))
|
||||||
|
throw Error(Error::Code::Command,
|
||||||
|
"expected command, got '%s'", to_string().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Option<const Sexp&> 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<std::string, ArgInfo>;
|
||||||
|
|
||||||
|
// A handler function
|
||||||
|
using Handler = std::function<void(const Command&)>;
|
||||||
|
|
||||||
|
/// 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<std::string> sorted_argnames() const {
|
||||||
|
// sort args -- by required, then alphabetical.
|
||||||
|
std::vector<std::string> 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<std::string, CommandInfo>;
|
||||||
|
|
||||||
|
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<void> 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__ */
|
||||||
@ -1,204 +0,0 @@
|
|||||||
/*
|
|
||||||
** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
|
||||||
**
|
|
||||||
** 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 <iostream>
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
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<std::string>
|
|
||||||
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<std::string>
|
|
||||||
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<int>
|
|
||||||
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<unsigned>
|
|
||||||
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<bool>
|
|
||||||
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<std::string>
|
|
||||||
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<std::string> 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;
|
|
||||||
}
|
|
||||||
@ -1,180 +0,0 @@
|
|||||||
/*
|
|
||||||
** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
|
||||||
**
|
|
||||||
** 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 <vector>
|
|
||||||
#include <string>
|
|
||||||
#include <ostream>
|
|
||||||
#include <stdexcept>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <functional>
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
#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<std::string, ArgInfo>;
|
|
||||||
// The parameters to a Handler.
|
|
||||||
using Parameters = Sexp::Seq;
|
|
||||||
|
|
||||||
Option<int> get_int(const Parameters& parms, const std::string& argname);
|
|
||||||
Option<unsigned> get_unsigned(const Parameters& parms, const std::string& argname);
|
|
||||||
Option<bool> get_bool(const Parameters& parms, const std::string& argname);
|
|
||||||
Option<std::string> get_string(const Parameters& parms, const std::string& argname);
|
|
||||||
Option<std::string> get_symbol(const Parameters& parms, const std::string& argname);
|
|
||||||
|
|
||||||
std::vector<std::string> 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<void(const Parameters&)>;
|
|
||||||
|
|
||||||
/// 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<std::string> sorted_argnames() const
|
|
||||||
{ // sort args -- by required, then alphabetical.
|
|
||||||
std::vector<std::string> 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<std::string, CommandInfo>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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__ */
|
|
||||||
@ -18,11 +18,6 @@
|
|||||||
################################################################################
|
################################################################################
|
||||||
# tests
|
# 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',
|
test('test-mu-util',
|
||||||
executable('test-mu-util',
|
executable('test-mu-util',
|
||||||
'test-mu-util.c',
|
'test-mu-util.c',
|
||||||
@ -38,8 +33,3 @@ test('test-mu-utils',
|
|||||||
'test-utils.cc',
|
'test-utils.cc',
|
||||||
install: false,
|
install: false,
|
||||||
dependencies: [glib_dep, lib_mu_utils_dep]))
|
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] ))
|
|
||||||
|
|||||||
@ -1,149 +0,0 @@
|
|||||||
/*
|
|
||||||
** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
|
||||||
**
|
|
||||||
** 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 <vector>
|
|
||||||
#include <glib.h>
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
@ -1,190 +0,0 @@
|
|||||||
/*
|
|
||||||
** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
|
||||||
**
|
|
||||||
** 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 <vector>
|
|
||||||
#include <glib.h>
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user