diff --git a/lib/utils/Makefile.am b/lib/utils/Makefile.am index 684993fc..5a638acb 100644 --- a/lib/utils/Makefile.am +++ b/lib/utils/Makefile.am @@ -1,4 +1,4 @@ -## Copyright (C) 2019 Dirk-Jan C. Binnema +## 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 @@ -16,72 +16,92 @@ include $(top_srcdir)/gtest.mk -AM_CFLAGS= \ - $(WARN_CFLAGS) \ - $(GLIB_CFLAGS) \ - $(ASAN_CFLAGS) \ - $(CODE_COVERAGE_CFLAGS) \ - -DMU_TESTMAILDIR=\"${abs_top_srcdir}/lib/testdir\" \ - -DMU_TESTMAILDIR2=\"${abs_top_srcdir}/lib/testdir2\" \ - -Wno-format-nonliteral \ - -Wno-switch-enum \ - -Wno-deprecated-declarations \ - -Wno-inline +AM_CFLAGS= \ + $(WARN_CFLAGS) \ + $(GLIB_CFLAGS) \ + $(ASAN_CFLAGS) \ + $(CODE_COVERAGE_CFLAGS) \ + -DMU_TESTMAILDIR=\"${abs_top_srcdir}/lib/testdir\" \ + -DMU_TESTMAILDIR2=\"${abs_top_srcdir}/lib/testdir2\" \ + -Wno-format-nonliteral \ + -Wno-switch-enum \ + -Wno-deprecated-declarations \ + -Wno-inline \ + -I${top_srcdir}/lib -AM_CPPFLAGS= \ +AM_CPPFLAGS= \ $(CODE_COVERAGE_CPPFLAGS) -AM_CXXFLAGS= \ - $(WARN_CXXFLAGS) \ - $(GLIB_CFLAGS) \ - $(ASAN_CXXFLAGS) \ - $(CODE_COVERAGE_CFLAGS) +AM_CXXFLAGS= \ + $(WARN_CXXFLAGS) \ + $(GLIB_CFLAGS) \ + $(ASAN_CXXFLAGS) \ + $(CODE_COVERAGE_CFLAGS) \ + -I${top_srcdir}/lib -AM_LDFLAGS= \ +AM_LDFLAGS= \ $(ASAN_LDFLAGS) -noinst_LTLIBRARIES= \ +noinst_LTLIBRARIES= \ libmu-utils.la -libmu_utils_la_SOURCES= \ - mu-date.c \ - mu-date.h \ - mu-error.hh \ - mu-log.c \ - mu-log.h \ - mu-str.c \ - mu-str.h \ - mu-util.c \ - mu-util.h \ - mu-utils.cc \ +libmu_utils_la_SOURCES= \ + mu-date.c \ + mu-date.h \ + mu-error.hh \ + mu-log.c \ + mu-log.h \ + mu-command-parser.cc \ + mu-command-parser.hh \ + mu-sexp-parser.cc \ + mu-sexp-parser.hh \ + mu-str.c \ + mu-str.h \ + mu-util.c \ + mu-util.h \ + mu-utils.cc \ mu-utils.hh -libmu_utils_la_LIBADD= \ - $(GLIB_LIBS) \ +libmu_utils_la_LIBADD= \ + $(GLIB_LIBS) \ $(CODE_COVERAGE_LIBS) -noinst_PROGRAMS= \ +noinst_PROGRAMS= \ $(TEST_PROGS) -TEST_PROGS+= \ +TEST_PROGS+= \ test-mu-util -test_mu_util_SOURCES= \ +test_mu_util_SOURCES= \ test-mu-util.c -test_mu_util_LDADD= \ +test_mu_util_LDADD= \ libmu-utils.la -TEST_PROGS+= \ +TEST_PROGS+= \ test-mu-utils -test_mu_utils_SOURCES= \ +test_mu_utils_SOURCES= \ test-utils.cc -test_mu_utils_LDADD= \ +test_mu_utils_LDADD= \ libmu-utils.la -TEST_PROGS+= \ +TEST_PROGS+= \ test-mu-str -test_mu_str_SOURCES= \ +test_mu_str_SOURCES= \ test-mu-str.c -test_mu_str_LDADD= \ +test_mu_str_LDADD= \ + libmu-utils.la + +TEST_PROGS+= \ + test-sexp-parser +test_sexp_parser_SOURCES= \ + test-sexp-parser.cc +test_sexp_parser_LDADD= \ + libmu-utils.la + +TEST_PROGS+= \ + test-command-parser +test_command_parser_SOURCES= \ + test-command-parser.cc +test_command_parser_LDADD= \ libmu-utils.la TESTS=$(TEST_PROGS) diff --git a/lib/utils/mu-command-parser.cc b/lib/utils/mu-command-parser.cc new file mode 100644 index 00000000..719ed393 --- /dev/null +++ b/lib/utils/mu-command-parser.cc @@ -0,0 +1,189 @@ +/* +** 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-utils.hh" + +#include +#include + +using namespace Mu; +using namespace Command; +using namespace Sexp; + +static Mu::Error +command_error(const std::string& msg) +{ + return Mu::Error(Error::Code::Command, msg); +} + + +void +Command::invoke(const Command::CommandMap& cmap, const Node& call) +{ + if (call.type != Type::List || call.children.empty() || + call.children[0].type != Type::Symbol) + throw command_error("call must be a list starting with a symbol"); + + const auto& params{call.children}; + const auto cmd_it = cmap.find(params[0].value); + if (cmd_it == cmap.end()) + throw command_error("unknown command '" + params[0].value + "'"); + + 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 = [&]() { + for (size_t i = 1; i < params.size(); i += 2) + if (params[i].type == Type::Symbol && + params[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 command_error("missing required parameter '" + argname + "'"); + continue; // not required + } + + // the types must match, but the 'nil' symbol is acceptable as + // "no value" + if (param_it->type != arginfo.type && + !(param_it->type == Type::Symbol && param_it->value == "nil")) + throw command_error("parameter '" + argname + "' expects type " + + to_string(arginfo.type) + + " but got " + to_string(param_it->type)); + } + + if (cinfo.handler) + cinfo.handler(params); +} + +static Parameters::const_iterator +find_param_node (const Parameters& params, const std::string& argname) +{ + for (size_t i = 1; i < params.size(); i += 2) { + if (i + 1 != params.size() && + params[i].type == Type::Symbol && + params[i].value == ':' + argname) + return params.begin() + i + 1; + } + + return params.end(); +} + +constexpr auto Nil = "nil"; + +static bool +is_nil(const Node& node) +{ + return node.type == Type::Symbol && node.value == Nil; +} + +const std::string& +Command::get_string_or (const Parameters& params, const std::string& argname, + const std::string& alt) +{ + const auto it = find_param_node (params, argname); + if (it == params.end() || is_nil(*it)) + return alt; + else if (it->type != Type::String) + throw Error(Error::Code::InvalidArgument, "expected but got %s (value: '%s')", + to_string(it->type).c_str(), + it->value.c_str()); + + return it->value; +} + +const std::string& +Command::get_symbol_or (const Parameters& params, const std::string& argname, + const std::string& alt) +{ + const auto it = find_param_node (params, argname); + if (it == params.end() || is_nil(*it)) + return alt; + else if (it->type != Type::Symbol) + throw Error(Error::Code::InvalidArgument, "expected but got %s (value: '%s')", + to_string(it->type).c_str(), + it->value.c_str()); + + return it->value; +} + + +int +Command::get_int_or (const Parameters& params, const std::string& argname, + int alt) +{ + const auto it = find_param_node (params, argname); + if (it == params.end() || is_nil(*it)) + return alt; + else if (it->type != Type::Integer) + throw Error(Error::Code::InvalidArgument, "expected but got %s", + to_string(it->type).c_str()); + else + return ::atoi(it->value.c_str()); +} + +bool +Command::get_bool_or (const Parameters& params, const std::string& argname, + bool alt) +{ + const auto it = find_param_node (params, argname); + if (it == params.end()) + return alt; + else if (it->type != Type::Symbol) + throw Error(Error::Code::InvalidArgument, "expected but got %s", + to_string(it->type).c_str()); + else + return it->value != Nil; +} + +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() || is_nil(*it)) + return {}; + else if (it->type != Type::List) + throw Error(Error::Code::InvalidArgument, "expected but got %s", + to_string(it->type).c_str()); + + std::vector vec; + for (const auto& n: it->children) { + if (n.type != Type::String) + throw Error(Error::Code::InvalidArgument, + "expected string element but got %s", + to_string(n.type).c_str()); + vec.emplace_back (n.value); + } + + return vec; +} diff --git a/lib/utils/mu-command-parser.hh b/lib/utils/mu-command-parser.hh new file mode 100644 index 00000000..228832d4 --- /dev/null +++ b/lib/utils/mu-command-parser.hh @@ -0,0 +1,135 @@ +/* +** 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 "utils/mu-error.hh" +#include "utils/mu-sexp-parser.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 convience 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 = std::vector; + +int get_int_or (const Parameters& parms, const std::string& argname, int alt=0); +bool get_bool_or (const Parameters& parms, const std::string& argname, bool alt=false); +const std::string& get_string_or (const Parameters& parms, const std::string& argname, const std::string& alt=""); +const std::string& get_symbol_or (const Parameters& parms, const std::string& argname, const std::string& alt="nil"); + + +std::vector get_string_vec (const Parameters& params, const std::string& argname); + + +// 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; +}; +/// All commands, mapping their name to information about them. +using CommandMap = std::unordered_map; + +/** + * Validate that the call (a Sexp::Node) 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::Node& call); + + +} // namespace Command + + +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 Mu + + +#endif /* MU_COMMAND_PARSER_HH__ */ diff --git a/lib/utils/mu-error.hh b/lib/utils/mu-error.hh index 991e71dd..42600862 100644 --- a/lib/utils/mu-error.hh +++ b/lib/utils/mu-error.hh @@ -70,6 +70,9 @@ struct Error final: public std::exception { va_end(args); } + Error(Error&& rhs) = default; + Error(const Error& rhs) = delete; + /** * Build an error from a GError an error-code and a format string * diff --git a/lib/utils/mu-sexp-parser.cc b/lib/utils/mu-sexp-parser.cc new file mode 100644 index 00000000..662feffa --- /dev/null +++ b/lib/utils/mu-sexp-parser.cc @@ -0,0 +1,170 @@ +/* +** Copyright (C) 2020 djcb +** +** 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-sexp-parser.hh" +#include "mu-utils.hh" + +using namespace Mu; +using namespace Sexp; + +__attribute__((format(printf, 2, 0))) static Mu::Error +parsing_error(size_t pos, const char* frm, ...) +{ + va_list args; + va_start(args, frm); + auto msg = format(frm, args); + va_end(args); + + if (pos == 0) + return Mu::Error(Error::Code::Parsing, "%s", msg); + else + return Mu::Error(Error::Code::Parsing, "%zu: %s", msg.c_str()); +} +static size_t +skip_whitespace (const std::string& s, size_t pos) +{ + while (pos != s.size()) { + if (s[pos] == ' ' || s[pos] == '\t' || s[pos] == '\n') + ++pos; + else + break; + } + + return pos; +} + +static Node parse (const std::string& expr, size_t& pos); + +static Node +parse_list (const std::string& expr, size_t& pos) +{ + if (expr[pos] != '(') // sanity check. + throw parsing_error(pos, "expected: '(' but got '%c", expr[pos]); + + std::vector children; + + ++pos; + while (expr[pos] != ')' && pos != expr.size()) + children.emplace_back(parse(expr, pos)); + + if (expr[pos] != ')') + throw parsing_error(pos, "expected: ')' but got '%c'", expr[pos]); + ++pos; + return Node{std::move(children)}; +} + +// parse string +static Node +parse_string (const std::string& expr, size_t& pos) +{ + if (expr[pos] != '"') // sanity check. + throw parsing_error(pos, "expected: '\"'' but got '%c", expr[pos]); + + bool escape{}; + std::string str; + for (++pos; pos != expr.size(); ++pos) { + + auto kar = expr[pos]; + if (escape && (kar == '"' || kar == '\\')) { + str += kar; + escape = false; + continue; + } + + if (kar == '"') + break; + else if (kar == '\\') + escape = true; + else + str += kar; + } + + if (escape || expr[pos] != '"') + throw parsing_error(pos, "unterminated string '%s'", str.c_str()); + + ++pos; + return Node{Type::String, std::move(str)}; +} + +static Node +parse_integer (const std::string& expr, size_t& pos) +{ + if (!isdigit(expr[pos])) // sanity check. + throw parsing_error(pos, "expected: but got '%c", expr[pos]); + + std::string num; + for (; isdigit(expr[pos]); ++pos) + num += expr[pos]; + + return Node {Type::Integer, std::move(num)}; +} + +static Node +parse_symbol (const std::string& expr, size_t& pos) +{ + if (!isalpha(expr[pos]) && expr[pos] != ':') // sanity check. + throw parsing_error(pos, "expected: |: but got '%c", expr[pos]); + + std::string symbol(1, expr[pos]); + for (++pos; isalnum(expr[pos]) || expr[pos] == '-'; ++pos) + symbol += expr[pos]; + + return Node { Type::Symbol, std::move(symbol)}; +} + + +static Node +parse (const std::string& expr, size_t& pos) +{ + pos = skip_whitespace(expr, pos); + + if (pos == expr.size()) + throw parsing_error(pos, "expected: character '%c", expr[pos]); + + const auto kar = expr[pos]; + const auto node =[&]() -> Node { + if (kar == '(') + return parse_list (expr, pos); + else if (kar == '"') + return parse_string(expr, pos); + else if (isdigit(kar)) + return parse_integer(expr, pos); + else if (isalpha(kar) || kar == ':') + return parse_symbol(expr, pos); + else + throw parsing_error(pos, "unexpected character '%c" + kar); + }(); + + pos = skip_whitespace(expr, pos); + + return node; +} + +Node +Sexp::parse (const std::string& expr) +{ + size_t pos{}; + auto node{::parse (expr, pos)}; + + if (pos != expr.size()) + throw parsing_error(pos, "trailing data starting with '%c'", expr[pos]); + + return node; +} diff --git a/lib/utils/mu-sexp-parser.hh b/lib/utils/mu-sexp-parser.hh new file mode 100644 index 00000000..ea7b90b4 --- /dev/null +++ b/lib/utils/mu-sexp-parser.hh @@ -0,0 +1,113 @@ +/* +** Copyright (C) 2020 djcb +** +** 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_SEXP_PARSER_HH__ +#define MU_SEXP_PARSER_HH__ + +#include +#include + +#include "utils/mu-error.hh" + +namespace Mu { +namespace Sexp { + +/// Simple s-expression parser that parses lists () and atoms (strings +/// ("-quoted), (positive) integers ([0..9]+) and symbol starting with alpha or +/// ':', then alphanum and '-') +/// +/// (:foo (1234 "bar" nil) :quux (a b c)) + +/// Node type +enum struct Type { List, String, Integer, Symbol }; + +/// Parse node +struct Node { + /** + * Construct a new non-list node + * + * @param typearg the type of node + * @param valuearg the value + */ + Node(Type typearg, std::string&& valuearg): + type{typearg}, value{std::move(valuearg)} { + if (typearg == Type::List) + throw Error(Error::Code::Parsing, + "atomic type cannot be a "); + } + + /** + * Construct a list node + + * @param childrenarg the list children + * + * @return + */ + explicit Node(std::vector&& childrenarg): + type{Type::List}, children{std::move(childrenarg)} + {} + + const Type type; /**< Type of node */ + const std::string value; /**< String value of node (only for non-Type::List)*/ + const std::vector children; /**< Chiidren of node (only for Type::List) */ +}; + +/** + * Parse the string as an s-expressi9on. + * + * @param expr an s-expression string + * + * @return the parsed s-expression, or throw Error. + */ +Node parse(const std::string& expr); + +} // Sexp + +static inline std::ostream& +operator<<(std::ostream& os, Sexp::Type id) +{ + switch (id) { + case Sexp::Type::List: os << ""; break; + case Sexp::Type::String: os << ""; break; + case Sexp::Type::Integer: os << ""; break; + case Sexp::Type::Symbol: os << ""; break; + default: throw std::runtime_error ("unknown node type"); + } + + return os; +} + +static inline std::ostream& +operator<<(std::ostream& os, const Sexp::Node& node) +{ + os << node.type; + if (node.type == Sexp::Type::List) { + os << '('; + for (auto&& elm: node.children) + os << elm; + os << ')'; + } else + os << '{' << node.value << '}'; + + return os; +} + +} // Mu + +#endif /* MU_SEXP_PARSER_HH__ */ diff --git a/lib/utils/test-command-parser.cc b/lib/utils/test-command-parser.cc new file mode 100644 index 00000000..fad90a48 --- /dev/null +++ b/lib/utils/test-command-parser.cc @@ -0,0 +1,142 @@ +/* +** Copyright (C) 2017 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" + +using namespace Mu; + +static void +test_param_getters() +{ + const auto node { Sexp::parse(R"((foo :bar 123 :cuux "456" :boo nil :bah true))")}; + + std::cout << node << "\n"; + + g_assert_cmpint(Command::get_int_or(node.children,"bar"), ==, 123); + assert_equal(Command::get_string_or(node.children, "bra", "bla"), "bla"); + assert_equal(Command::get_string_or(node.children, "cuux"), "456"); + + g_assert_true(Command::get_bool_or(node.children,"boo") == false); + g_assert_true(Command::get_bool_or(node.children,"bah") == true); +} + + +static bool +call (const Command::CommandMap& cmap, const std::string& sexp) try +{ + const auto node{Sexp::parse(sexp)}; + g_message ("invoking %s", to_string(node).c_str()); + + invoke (cmap, node); + + return true; + +} catch (const Error& err) { + g_warning ("%s", err.what()); + return false; +} + +static void +test_command() +{ + using namespace Command; + + CommandMap cmap; + + cmap.emplace("my-command", + CommandInfo{ + ArgMap{ {"param1", ArgInfo{Sexp::Type::String, true, "some string" }}, + {"param2", ArgInfo{Sexp::Type::Integer, false, "some integer"}}}, + "My command,", + {}}); + + //std::cout << cmap << "\n"; + + g_assert_true(call(cmap, "(my-command :param1 \"hello\")")); + g_assert_true(call(cmap, "(my-command :param1 \"hello\" :param2 123)")); + g_assert_true(call(cmap, "(my-command :param1 \"hello\" :param2 123 :param3 xxx)")); +} + +static void +test_command2() +{ + using namespace Command; + + CommandMap cmap; + cmap.emplace("bla", + CommandInfo{ + ArgMap{ + {"foo", ArgInfo{Sexp::Type::Integer, false, "foo"}}, + {"bar", ArgInfo{Sexp::Type::String, false, "bar"}}, + },"yeah", + [&](const auto& params){}}); + + + g_assert_true (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::Integer, 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 +{ + g_test_init (&argc, &argv, NULL); + + 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); + + return g_test_run (); + + +} catch (const std::runtime_error& re) { + std::cerr << re.what() << "\n"; + return 1; +} diff --git a/lib/utils/test-sexp-parser.cc b/lib/utils/test-sexp-parser.cc new file mode 100644 index 00000000..a056049f --- /dev/null +++ b/lib/utils/test-sexp-parser.cc @@ -0,0 +1,75 @@ +/* +** 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" + +using namespace Mu; + +static bool +check_parse (const std::string& expr, const std::string& expected) +{ + try { + const auto parsed{to_string(Sexp::parse(expr))}; + g_assert_cmpstr(parsed.c_str(), ==, expected.c_str()); + 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(R"(:foo-123)", "{:foo-123}"); + check_parse(R"("foo")", "{foo}"); + check_parse(R"(12345)", "{12345}"); + check_parse(R"((123 bar "cuux"))", "({123}{bar}{cuux})"); + + check_parse(R"("\"")", "{\"}"); + check_parse(R"("\\")", "{\\}"); +} + +int +main (int argc, char *argv[]) try +{ + g_test_init (&argc, &argv, NULL); + + if (argc == 2) { + std::cout << Sexp::parse(argv[1]) << '\n'; + return 0; + } + + g_test_add_func ("/utils/command-parser/parse", test_parser); + + return g_test_run (); + + +} catch (const std::runtime_error& re) { + std::cerr << re.what() << "\n"; + return 1; +}