diff --git a/lib/message/mu-document.cc b/lib/message/mu-document.cc index f0c230c5..3c43aa0f 100644 --- a/lib/message/mu-document.cc +++ b/lib/message/mu-document.cc @@ -19,6 +19,7 @@ #include "mu-document.hh" #include "mu-message.hh" +#include "utils/mu-sexp.hh" #include #include @@ -30,11 +31,37 @@ #include #include + using namespace Mu; constexpr uint8_t SepaChar1 = 0xfe; constexpr uint8_t SepaChar2 = 0xff; + +const Xapian::Document& +Document::xapian_document() const +{ + if (dirty_sexp_) { + xdoc_.set_data(sexp_.to_string()); + dirty_sexp_ = false; + } + return xdoc_; +} + +template void +Document::put_prop(const std::string& pname, SexpType&& val) +{ + sexp_.put_props(pname, std::forward(val)); + dirty_sexp_ = true; +} + +template void +Document::put_prop(const Field& field, SexpType&& val) +{ + put_prop(std::string(":") + std::string{field.name}, + std::forward(val)); +} + static void add_search_term(Xapian::Document& doc, const Field& field, const std::string& val) { @@ -57,12 +84,6 @@ add_search_term(Xapian::Document& doc, const Field& field, const std::string& va } -static std::string -make_prop_name(const Field& field) -{ - return ":" + std::string(field.name); -} - void Document::add(Field::Id id, const std::string& val) { @@ -74,9 +95,9 @@ Document::add(Field::Id id, const std::string& val) if (field.is_searchable()) add_search_term(xdoc_, field, val); - if (field.include_in_sexp()) - sexp_list().add_prop(make_prop_name(field), - Sexp::make_string(std::move(val))); + if (field.include_in_sexp()) { + put_prop(field, val); + } } void @@ -95,11 +116,10 @@ Document::add(Field::Id id, const std::vector& vals) add_search_term(xdoc_, field, val); }); if (field.include_in_sexp()) { - Sexp::List elms; + Sexp elms{}; for(auto&& val: vals) - elms.add(Sexp::make_string(val)); - sexp_list().add_prop(make_prop_name(field), - Sexp::make_list(std::move(elms))); + elms.add(val); + put_prop(field, std::move(elms)); } } @@ -113,19 +133,16 @@ Document::string_vec_value(Field::Id field_id) const noexcept static Sexp make_contacts_sexp(const Contacts& contacts) { - Sexp::List clist; + Sexp contacts_sexp; seq_for_each(contacts, [&](auto&& c) { + Sexp contact(":email"_sym, c.email); if (!c.name.empty()) - clist.add(Sexp::make_prop_list( - ":name", Sexp::make_string(c.name), - ":email", Sexp::make_string(c.email))); - else - clist.add(Sexp::make_prop_list( - ":email", Sexp::make_string(c.email))); + contact.add(":name"_sym, c.name); + contacts_sexp.add(std::move(contact)); }); - return Sexp::make_list(std::move(clist)); + return contacts_sexp; } void @@ -168,9 +185,7 @@ Document::add(Field::Id id, const Contacts& contacts) xdoc_.add_value(field.value_no(), join(cvec, SepaChar1)); if (field.include_in_sexp()) - sexp_list().add_prop(make_prop_name(field), - make_contacts_sexp(contacts)); - + put_prop(field, make_contacts_sexp(contacts)); } Contacts @@ -204,22 +219,19 @@ Document::contacts_value(Field::Id id) const noexcept void Document::add_extra_contacts(const std::string& propname, const Contacts& contacts) { - if (!contacts.empty()) - sexp_list().add_prop(std::string{propname}, - make_contacts_sexp(contacts)); + if (!contacts.empty()) { + put_prop(propname, make_contacts_sexp(contacts)); + dirty_sexp_ = true; + } } static Sexp make_emacs_time_sexp(::time_t t) { - Sexp::List dlist; - - dlist.add(Sexp::make_number(static_cast(t >> 16))); - dlist.add(Sexp::make_number(static_cast(t & 0xffff))); - dlist.add(Sexp::make_number(0)); - - return Sexp::make_list(std::move(dlist)); + return Sexp().add(static_cast(t >> 16), + static_cast(t & 0xffff), + 0); } void @@ -231,7 +243,6 @@ Document::add(Field::Id id, int64_t val) * we comply, by storing a number a base-16 and prefixing with 'f' + * length; such that the strings are sorted in the numerical order. */ - const auto field{field_from_id(id)}; if (field.is_value()) @@ -239,11 +250,9 @@ Document::add(Field::Id id, int64_t val) if (field.include_in_sexp()) { if (field.is_time_t()) - sexp_list().add_prop(make_prop_name(field), - make_emacs_time_sexp(val)); + put_prop(field, make_emacs_time_sexp(val)); else - sexp_list().add_prop(make_prop_name(field), - Sexp::make_number(val)); + put_prop(field, val); } } @@ -265,8 +274,7 @@ Document::add(Priority prio) xdoc_.add_boolean_term(field.xapian_term(to_char(prio))); if (field.include_in_sexp()) - sexp_list().add_prop(make_prop_name(field), - Sexp::make_symbol_sv(priority_name(prio))); + put_prop(field, Sexp::Symbol(priority_name(prio))); } Priority @@ -281,52 +289,21 @@ Document::add(Flags flags) { constexpr auto field{field_from_id(Field::Id::Flags)}; - Sexp::List flaglist; + Sexp flaglist; xdoc_.add_value(field.value_no(), to_lexnum(static_cast(flags))); flag_infos_for_each([&](auto&& flag_info) { auto term=[&](){return field.xapian_term(flag_info.shortcut_lower());}; if (any_of(flag_info.flag & flags)) { xdoc_.add_boolean_term(term()); - flaglist.add(Sexp::make_symbol_sv(flag_info.name)); + flaglist.add(Sexp::Symbol(flag_info.name)); } }); if (field.include_in_sexp()) - sexp_list().add_prop(make_prop_name(field), - Sexp::make_list(std::move(flaglist))); + put_prop(field, std::move(flaglist)); } -Sexp::List& -Document::sexp_list() -{ - /* perhaps we need get the sexp_ from the document first? */ - if (sexp_list_.empty()) { - const auto str{xdoc_.get_data()}; - if (!str.empty()) { - Sexp sexp{Sexp::make_parse(str)}; - sexp_list_ = sexp.list(); - } - } - - return sexp_list_; -} - -std::string -Document::cached_sexp() const -{ - return xdoc_.get_data(); -} - -void -Document::update_cached_sexp(void) -{ - if (sexp_list_.empty()) - return; /* nothing to do; i.e. the exisiting sexp is still up to - * date */ - xdoc_.set_data(Sexp::make_list(Sexp::List{sexp_list()}).to_sexp_string()); -} - Flags Document::flags_value() const noexcept { @@ -364,7 +341,6 @@ Document::remove(Field::Id field_id) } } }); - } diff --git a/lib/message/mu-document.hh b/lib/message/mu-document.hh index 9c923a19..4cfe2442 100644 --- a/lib/message/mu-document.hh +++ b/lib/message/mu-document.hh @@ -52,13 +52,23 @@ public: * * @param doc */ - Document(const Xapian::Document& doc): xdoc_{doc} {} + Document(const Xapian::Document& doc): xdoc_{doc} { + if (auto&& s{Sexp::parse(xdoc_.get_data())}; s) + sexp_ = std::move(*s); + } + + /** + * DTOR + */ + ~Document() { + xapian_document(); // for side-effect up updating sexp. + } /** * Get a reference to the underlying Xapian document. * */ - const Xapian::Document& xapian_document() const { return xdoc_; } + const Xapian::Document& xapian_document() const; /** * Get the doc-id for this document @@ -138,24 +148,12 @@ public: void remove(Field::Id field_id); /** - * Update the cached sexp from the sexp_list_ - */ - void update_cached_sexp(); - - /** - * Get the cached s-expression - * - * @return a string - */ - std::string cached_sexp() const; - - /** - * Get the cached s-expressionl useful for changing + * Get the cached s-expression useful for changing * it (call update_sexp_cache() when done) * - * @return the cache s-expression + * @return the cached s-expression */ - Sexp::List& sexp_list(); + const Sexp& sexp() const { return sexp_; } /** * Generically adds an optional value, if set, to the document @@ -184,6 +182,7 @@ public: return xdoc_.get_value(field_from_id(field_id).value_no()); }, std::string{}); } + /** * Get a vec of string values. * @@ -229,9 +228,13 @@ public: Flags flags_value() const noexcept; private: - Xapian::Document xdoc_; - Sexp::List sexp_list_; + template void put_prop(const Field& field, SexpType&& val); + template void put_prop(const std::string& pname, SexpType&& val); + + mutable Xapian::Document xdoc_; + Sexp sexp_; + mutable bool dirty_sexp_{}; /* xdoc's sexp is outdated */ }; } // namepace Mu diff --git a/lib/message/mu-fields.hh b/lib/message/mu-fields.hh index 22b486b5..2579a767 100644 --- a/lib/message/mu-fields.hh +++ b/lib/message/mu-fields.hh @@ -186,8 +186,8 @@ struct Field { * */ - constexpr char xapian_prefix() const - { /* xapian uses uppercase shortcuts; toupper is not constexpr */ + constexpr char xapian_prefix() const { + /* xapian uses uppercase shortcuts; toupper is not constexpr */ return shortcut == 0 ? 0 : shortcut - ('a' - 'A'); } @@ -542,5 +542,6 @@ Option field_from_number(size_t id) return field_from_id(static_cast(id)); } + } // namespace Mu #endif /* MU_FIELDS_HH__ */ diff --git a/lib/message/mu-message.cc b/lib/message/mu-message.cc index a8ff0f5f..58102609 100644 --- a/lib/message/mu-message.cc +++ b/lib/message/mu-message.cc @@ -182,23 +182,16 @@ Message::docid() const } -const Mu::Sexp::List& -Message::to_sexp_list() const +const Mu::Sexp& +Message::sexp() const { - return priv_->doc.sexp_list(); -} - -void -Message::update_cached_sexp() -{ - priv_->doc.update_cached_sexp(); + return priv_->doc.sexp(); } Result Message::set_maildir(const std::string& maildir) { /* sanity check a little bit */ - if (maildir.empty() || maildir.at(0) != '/' || (maildir.size() > 1 && maildir.at(maildir.length()-1) == '/')) diff --git a/lib/message/mu-message.hh b/lib/message/mu-message.hh index 3f2e001b..615b5d8c 100644 --- a/lib/message/mu-message.hh +++ b/lib/message/mu-message.hh @@ -23,6 +23,8 @@ #include #include #include +#include + #include "mu-contact.hh" #include "mu-priority.hh" #include "mu-flags.hh" @@ -336,37 +338,17 @@ public: .string_vec_value(Field::Id::Tags); } - /** - * Get the cached s-expression for this message, or {} if not available. - * - * @return sexp or empty. - */ - std::string cached_sexp() const { - return document().cached_sexp(); - } - /* * Convert to Sexp */ /** - * Get the s-expression for this message. Stays valid as long - * as this message is. + * Get the s-expression for this message. Stays valid as long as this + * message is. * - * @return a Mu::Sexp::List representing the message. + * @return an Sexp representing the message. */ - const Mu::Sexp::List& to_sexp_list() const; - Mu::Sexp to_sexp() const { - return Sexp::make_list(Sexp::List(to_sexp_list())); - } - - /** - * Update the cached sexp for this message which is stored in the - * document. This should be done immediately before storing it in the - * database. - * - */ - void update_cached_sexp(); + const Sexp& sexp() const; /* * And some non-const message, for updating an existing @@ -477,5 +459,15 @@ private: }; // Message MU_ENABLE_BITOPS(Message::Options); + + +static inline std::ostream& +operator<<(std::ostream& os, const Message& msg) +{ + os << msg.sexp(); + return os; +} + + } // Mu #endif /* MU_MESSAGE_HH__ */ diff --git a/lib/message/test-mu-message.cc b/lib/message/test-mu-message.cc index a96b44bf..9cfc1a4d 100644 --- a/lib/message/test-mu-message.cc +++ b/lib/message/test-mu-message.cc @@ -481,8 +481,7 @@ Content-Type: message/rfc822 )"; auto message{Message::make_from_text(msgtext)}; g_assert_true(!!message); - - g_assert_true(message->cached_sexp().empty()); + //g_assert_true(message->sexp().empty()); } diff --git a/lib/mu-server.cc b/lib/mu-server.cc index 9ea02eac..ddb87d1a 100644 --- a/lib/mu-server.cc +++ b/lib/mu-server.cc @@ -42,16 +42,16 @@ #include "utils/mu-utils.hh" #include "utils/mu-option.hh" -#include "utils/mu-command-parser.hh" +#include "utils/mu-command-handler.hh" #include "utils/mu-readline.hh" using namespace Mu; -using namespace Command; /// @brief object to manage the server-context for all commands. struct Server::Private { Private(Store& store, Output output) - : store_{store}, output_{output}, command_map_{make_command_map()}, + : store_{store}, output_{output}, + command_handler_{make_command_map()}, keep_going_{true} {} @@ -63,14 +63,14 @@ struct Server::Private { // // construction helpers // - CommandMap make_command_map(); + CommandHandler::CommandInfoMap make_command_map(); // // acccessors Store& store() { return store_; } const Store& store() const { return store_; } Indexer& indexer() { return store().indexer(); } - const CommandMap& command_map() const { return command_map_; } + //CommandMap& command_map() const { return command_map_; } // // invoke @@ -80,32 +80,29 @@ struct Server::Private { // // output // - void output_sexp(Sexp&& sexp,Server::OutputFlags flags = {}) const { + void output_sexp(const Sexp& sexp, Server::OutputFlags flags = {}) const { if (output_) - output_(std::move(sexp), flags); + output_(sexp, flags); } - void output_sexp(Sexp::List&& lst, Server::OutputFlags flags = {}) const { - output_sexp(Sexp::make_list(std::move(lst)), flags); - } size_t output_results(const QueryResults& qres, size_t batch_size) const; // // handlers for various commands. // - void add_handler(const Parameters& params); - void compose_handler(const Parameters& params); - void contacts_handler(const Parameters& params); - void find_handler(const Parameters& params); - void help_handler(const Parameters& params); - void index_handler(const Parameters& params); - void move_handler(const Parameters& params); - void mkdir_handler(const Parameters& params); - void ping_handler(const Parameters& params); - void quit_handler(const Parameters& params); - void remove_handler(const Parameters& params); - void sent_handler(const Parameters& params); - void view_handler(const Parameters& params); + void add_handler(const Command& cmd); + void compose_handler(const Command& cmd); + void contacts_handler(const Command& cmd); + void find_handler(const Command& cmd); + void help_handler(const Command& cmd); + void index_handler(const Command& cmd); + void move_handler(const Command& cmd); + void mkdir_handler(const Command& cmd); + void ping_handler(const Command& cmd); + void quit_handler(const Command& cmd); + void remove_handler(const Command& cmd); + void sent_handler(const Command& cmd); + void view_handler(const Command& cmd); private: // helpers @@ -113,88 +110,83 @@ private: Store::Id docid, const Option qm) const; - Sexp::List move_docid(Store::Id docid, Option flagstr, - bool new_name, bool no_view); + Sexp move_docid(Store::Id docid, Option flagstr, + bool new_name, bool no_view); - Sexp::List perform_move(Store::Id docid, - const Message& msg, - const std::string& maildirarg, - Flags flags, - bool new_name, - bool no_view); + Sexp perform_move(Store::Id docid, + const Message& msg, + const std::string& maildirarg, + Flags flags, + bool new_name, + bool no_view); - bool maybe_mark_as_read(Store::Id docid, Flags old_flags, bool rename); - bool maybe_mark_msgid_as_read(const std::string& msgid, bool rename); + bool view_mark_as_read(Store::Id docid, const Message& msg, bool rename); - Store& store_; - Server::Output output_; - const CommandMap command_map_; - std::atomic keep_going_{}; - std::thread index_thread_; + Store& store_; + Server::Output output_; + const CommandHandler command_handler_; + std::atomic keep_going_{}; + std::thread index_thread_; }; static Sexp build_metadata(const QueryMatch& qmatch) { - Sexp::List mdata; - - auto symbol_t = [] { return Sexp::make_symbol("t"); }; - - mdata.add_prop(":path", Sexp::make_string(qmatch.thread_path)); - mdata.add_prop(":level", Sexp::make_number(qmatch.thread_level)); - mdata.add_prop(":date", Sexp::make_string(qmatch.thread_date)); - - Sexp::List dlist; const auto td{::atoi(qmatch.thread_date.c_str())}; - dlist.add(Sexp::make_number((unsigned)(td >> 16))); - dlist.add(Sexp::make_number((unsigned)(td & 0xffff))); - dlist.add(Sexp::make_number(0)); - mdata.add_prop(":date-tstamp", Sexp::make_list(std::move(dlist))); - + auto mdata = Sexp().put_props(":path", qmatch.thread_path, + ":level", qmatch.thread_level, + ":date", qmatch.thread_date, + ":data-tstamp", Sexp().add(static_cast(td >> 16), + static_cast(td & 0xffff), + 0)); if (qmatch.has_flag(QueryMatch::Flags::Root)) - mdata.add_prop(":root", symbol_t()); + mdata.put_props(":root", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::Related)) - mdata.add_prop(":related", symbol_t()); + mdata.put_props(":related", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::First)) - mdata.add_prop(":first-child", symbol_t()); + mdata.put_props(":first-child", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::Last)) - mdata.add_prop(":last-child", symbol_t()); + mdata.put_props(":last-child", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::Orphan)) - mdata.add_prop(":orphan", symbol_t()); + mdata.put_props(":orphan", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::Duplicate)) - mdata.add_prop(":duplicate", symbol_t()); + mdata.put_props(":duplicate", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::HasChild)) - mdata.add_prop(":has-child", symbol_t()); + mdata.put_props(":has-child", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::ThreadSubject)) - mdata.add_prop(":thread-subject", symbol_t()); + mdata.put_props(":thread-subject", Sexp::t()); - return Sexp::make_list(std::move(mdata)); + return mdata; } /* - * A message here is a Sexp::List consists of a message s-expression with - * optionally a :meta expression added. + * A message here consists of a message s-expression with optionally a :docid + * and/or :meta expression added. */ Sexp Server::Private::build_message_sexp(const Message& msg, Store::Id docid, const Option qm) const { - auto sexp_list = msg.to_sexp_list(); + Sexp sexp{msg.sexp()}; // copy if (docid != 0) - sexp_list.add_prop(":docid", Sexp::make_number(docid)); + sexp.put_props(":docid", docid); if (qm) - sexp_list.add_prop(":meta", build_metadata(*qm)); + sexp.put_props(":meta", build_metadata(*qm)); - return Sexp::make_list(std::move(sexp_list)); + return sexp; } -CommandMap +CommandHandler::CommandInfoMap Server::Private::make_command_map() { - CommandMap cmap; + CommandHandler::CommandInfoMap cmap; - using Type = Sexp::Type; + using CommandInfo = CommandHandler::CommandInfo; + using ArgMap = CommandHandler::ArgMap; + using ArgInfo = CommandHandler::ArgInfo; + using Type = Sexp::Type; + using Type = Sexp::Type; cmap.emplace( "add", @@ -352,12 +344,10 @@ make_error(Error::Code errcode, const char* frm, ...) g_vasprintf(&msg, frm, ap); va_end(ap); - Sexp::List err; - err.add_prop(":error", Sexp::make_number(static_cast(errcode))); - err.add_prop(":message", Sexp::make_string(msg)); + auto err = Sexp().put_props(":error", static_cast(errcode), + ":message", msg); g_free(msg); - - return Sexp::make_list(std::move(err)); + return err; } bool @@ -365,10 +355,14 @@ Server::Private::invoke(const std::string& expr) noexcept { if (!keep_going_) return false; - try { - auto call{Sexp::Sexp::make_parse(expr)}; - Command::invoke(command_map(), call); + auto cmd{Command::make_parse(std::string{expr})}; + if (!cmd) + throw cmd.error(); + + auto res = command_handler_.invoke(*cmd); + if (!res) + throw res.error(); } catch (const Mu::Error& me) { output_sexp(make_error(me.code(), "%s", me.what())); @@ -394,32 +388,27 @@ Server::Private::invoke(const std::string& expr) noexcept * information about the newly added message (details: see code below) */ void -Server::Private::add_handler(const Parameters& params) +Server::Private::add_handler(const Command& cmd) { - auto path{get_string_or(params, ":path")}; - const auto docid_res{store().add_message(path)}; + auto path{cmd.string_arg(":path")}; + const auto docid_res{store().add_message(*path)}; if (!docid_res) throw docid_res.error(); const auto docid{docid_res.value()}; - - Sexp::List expr; - expr.add_prop(":info", Sexp::make_symbol("add")); - expr.add_prop(":path", Sexp::make_string(path)); - expr.add_prop(":docid", Sexp::make_number(docid)); - - output_sexp(Sexp::make_list(std::move(expr))); + output_sexp(Sexp().put_props(":info", "add"_sym, + ":path", *path, + ":docid", docid)); auto msg_res{store().find_message(docid)}; if (!msg_res) throw Error(Error::Code::Store, "failed to get message at %s (docid=%u): %s", - path.c_str(), docid); + path->c_str(), docid); - Sexp::List update; - update.add_prop(":update", build_message_sexp(msg_res.value(), docid, {})); - output_sexp(Sexp::make_list(std::move(update))); + output_sexp(Sexp().put_props(":update", + build_message_sexp(msg_res.value(), docid, {}))); } /* 'compose' produces the un-changed *original* message sexp (ie., the message @@ -452,44 +441,42 @@ maybe_add_attachment(Message& message, const MessagePart& part, size_t index) if (!res) throw res.error(); - Sexp::List pi; - + Sexp pi; if (auto cdescr = part.content_description(); cdescr) - pi.add_prop(":description", Sexp::make_string(*cdescr)); + pi.put_props(":description", *cdescr); else if (cooked_name) - pi.add_prop(":description", Sexp::make_string(cooked_name.value())); + pi.put_props(":description", cooked_name.value()); - pi.add_prop(":file-name", Sexp::make_string(fname)); - pi.add_prop(":mime-type", Sexp::make_string( - part.mime_type().value_or("application/octet-stream"))); + pi.put_props(":file-name", fname, + ":mime-type", + part.mime_type().value_or("application/octet-stream")); - return Some(Sexp::make_list(std::move(pi))); + return Some(std::move(pi)); } void -Server::Private::compose_handler(const Parameters& params) +Server::Private::compose_handler(const Command& cmd) { - const auto ctype{get_symbol_or(params, ":type")}; + const auto ctype = cmd.symbol_arg(":type").value_or(""); - Sexp::List comp_lst; - comp_lst.add_prop(":compose", Sexp::make_symbol(std::string(ctype))); + auto comp_lst = Sexp().put_props(":compose", Sexp::Symbol(ctype)); if (ctype == "reply" || ctype == "forward" || ctype == "edit" || ctype == "resend") { - const unsigned docid{(unsigned)get_int_or(params, ":docid")}; + const unsigned docid{static_cast(cmd.number_arg(":docid").value_or(0))}; auto msg{store().find_message(docid)}; if (!msg) throw Error{Error::Code::Store, "failed to get message %u", docid}; - comp_lst.add_prop(":original", build_message_sexp(msg.value(), docid, {})); + comp_lst.put_props(":original", build_message_sexp(msg.value(), docid, {})); if (ctype == "forward") { // when forwarding, attach any attachment in the orig size_t index{}; - Sexp::List attseq; + Sexp attseq; for (auto&& part: msg->parts()) { if (auto attsexp = maybe_add_attachment( *msg, part, index); attsexp) { @@ -498,10 +485,8 @@ Server::Private::compose_handler(const Parameters& params) } } if (!attseq.empty()) { - comp_lst.add_prop(":include", - Sexp::make_list(std::move(attseq))); - comp_lst.add_prop(":cache-path", - Sexp::make_string(*msg->cache_path())); + comp_lst.put_props(":include", std::move(attseq), + ":cache-path", *msg->cache_path()); } } @@ -509,16 +494,16 @@ Server::Private::compose_handler(const Parameters& params) throw Error(Error::Code::InvalidArgument, "invalid compose type '%s'", ctype.c_str()); - output_sexp(std::move(comp_lst)); + output_sexp(comp_lst); } void -Server::Private::contacts_handler(const Parameters& params) +Server::Private::contacts_handler(const Command& cmd) { - const auto personal = get_bool_or(params, ":personal"); - const auto afterstr = get_string_or(params, ":after"); - const auto tstampstr = get_string_or(params, ":tstamp"); - const auto maxnum = get_int_or(params, ":maxnum", 0 /*unlimited*/); + const auto personal = cmd.boolean_arg(":personal"); + const auto afterstr = cmd.string_arg(":after").value_or(""); + const auto tstampstr = cmd.string_arg(":tstamp").value_or(""); + const auto maxnum = cmd.number_arg(":maxnum").value_or(0 /*unlimited*/); const auto after{afterstr.empty() ? 0 : parse_date_time(afterstr, true).value_or(0)}; @@ -530,7 +515,7 @@ Server::Private::contacts_handler(const Parameters& params) static_cast(tstamp)); auto n{0}; - Sexp::List contacts; + Sexp contacts; store().contacts_cache().for_each([&](const Contact& ci) { /* since the last time we got some contacts */ @@ -545,19 +530,17 @@ Server::Private::contacts_handler(const Parameters& params) n++; - contacts.add(Sexp::make_string(ci.display_name(true/*encode-if-needed*/))); + contacts.add(ci.display_name(true/*encode-if-needed*/)); return maxnum == 0 || n < maxnum; }); - Sexp::List seq; - seq.add_prop(":contacts", Sexp::make_list(std::move(contacts))); - seq.add_prop(":tstamp", - Sexp::make_string(format("%" G_GINT64_FORMAT, - g_get_monotonic_time()))); + Sexp seq; + seq.put_props(":contacts", contacts, + ":tstamp", format("%" G_GINT64_FORMAT, g_get_monotonic_time())); /* dump the contacts cache as a giant sexp */ g_debug("sending %d of %zu contact(s)", n, store().contacts_cache().size()); - output_sexp(std::move(seq), Server::OutputFlags::SplitList); + output_sexp(seq, Server::OutputFlags::SplitList); } /* get a *list* of all messages with the given message id */ @@ -613,10 +596,10 @@ path_from_docid(const Store& store, Store::Id docid) } static std::vector -determine_docids(const Store& store, const Parameters& params) +determine_docids(const Store& store, const Command& cmd) { - auto docid{get_int_or(params, ":docid", 0)}; - const auto msgid{get_string_or(params, ":msgid")}; + auto docid{cmd.number_arg(":docid").value_or(0)}; + const auto msgid{cmd.string_arg(":msgid").value_or("")}; if ((docid == 0) == msgid.empty()) throw Error(Error::Code::InvalidArgument, @@ -632,12 +615,12 @@ size_t Server::Private::output_results(const QueryResults& qres, size_t batch_size) const { size_t n{}; - Sexp::List headers; + Sexp headers; - const auto output_batch = [&](Sexp::List&& hdrs) { - Sexp::List batch; - batch.add_prop(":headers", Sexp::make_list(std::move(hdrs))); - output_sexp(std::move(batch)); + const auto output_batch = [&](Sexp&& hdrs) { + Sexp batch; + batch.put_props(":headers", std::move(hdrs)); + output_sexp(batch); }; for (auto&& mi : qres) { @@ -649,7 +632,6 @@ Server::Private::output_results(const QueryResults& qres, size_t batch_size) con // construct sexp for a single header. auto qm{mi.query_match()}; auto msgsexp{build_message_sexp(*msg, mi.doc_id(), qm)}; - msgsexp.formatting_opts |= Sexp::FormattingOptions::SplitList; headers.add(std::move(msgsexp)); // we output up-to-batch-size lists of messages. It's much // faster (on the emacs side) to handle such batches than single @@ -668,17 +650,17 @@ Server::Private::output_results(const QueryResults& qres, size_t batch_size) con } void -Server::Private::find_handler(const Parameters& params) +Server::Private::find_handler(const Command& cmd) { - const auto q{get_string_or(params, ":query")}; - const auto threads{get_bool_or(params, ":threads", false)}; + const auto q{cmd.string_arg(":query").value_or("")}; + const auto threads{cmd.boolean_arg(":threads")}; // perhaps let mu4e set this as frame-lines of the appropriate frame. - const auto batch_size{get_int_or(params, ":batch-size", 110)}; - const auto sortfieldstr{get_symbol_or(params, ":sortfield", "")}; - const auto descending{get_bool_or(params, ":descending", false)}; - const auto maxnum{get_int_or(params, ":maxnum", -1 /*unlimited*/)}; - const auto skip_dups{get_bool_or(params, ":skip-dups", false)}; - const auto include_related{get_bool_or(params, ":include-related", false)}; + const auto batch_size{cmd.number_arg(":batch-size").value_or(110)}; + const auto sortfieldstr{cmd.symbol_arg(":sortfield").value_or("")}; + const auto descending{cmd.boolean_arg(":descending")}; + const auto maxnum{cmd.number_arg(":maxnum").value_or(-1) /*unlimited*/}; + const auto skip_dups{cmd.boolean_arg(":skip-dups")}; + const auto include_related{cmd.boolean_arg(":include-related")}; auto sort_field = std::invoke([&]()->Option{ if (sortfieldstr.size() < 2) @@ -710,26 +692,17 @@ Server::Private::find_handler(const Parameters& params) /* before sending new results, send an 'erase' message, so the frontend * knows it should erase the headers buffer. this will ensure that the * output of two finds will not be mixed. */ - { - Sexp::List lst; - lst.add_prop(":erase", Sexp::make_symbol("t")); - output_sexp(std::move(lst)); - } - + output_sexp(Sexp().put_props(":erase", Sexp::t())); const auto foundnum{output_results(*qres, static_cast(batch_size))}; - - { - Sexp::List lst; - lst.add_prop(":found", Sexp::make_number(foundnum)); - output_sexp(std::move(lst)); - } + output_sexp(Sexp().put_props(":found", foundnum)); } void -Server::Private::help_handler(const Parameters& params) +Server::Private::help_handler(const Command& cmd) { - const auto command{get_symbol_or(params, ":command", "")}; - const auto full{get_bool_or(params, ":full", !command.empty())}; + const auto command{cmd.symbol_arg(":command").value_or("")}; + const auto full{cmd.bool_arg(":full").value_or(!command.empty())}; + auto&& info_map{command_handler_.info_map()}; if (command.empty()) { std::cout << ";; Commands are s-expressions of the form\n" @@ -740,12 +713,13 @@ Server::Private::help_handler(const Parameters& params) } std::vector names; - for (auto&& name_cmd : command_map()) + for (auto&& name_cmd: info_map) names.emplace_back(name_cmd.first); + std::sort(names.begin(), names.end()); for (auto&& name : names) { - const auto& info{command_map().find(name)->second}; + const auto& info{info_map.find(name)->second}; if (!command.empty() && name != command) continue; @@ -771,26 +745,27 @@ Server::Private::help_handler(const Parameters& params) } } -static Sexp::List +static Sexp get_stats(const Indexer::Progress& stats, const std::string& state) { - Sexp::List lst; + Sexp sexp; - lst.add_prop(":info", Sexp::make_symbol("index")); - lst.add_prop(":status", Sexp::make_symbol(std::string{state})); - lst.add_prop(":checked", Sexp::make_number(stats.checked)); - lst.add_prop(":updated", Sexp::make_number(stats.updated)); - lst.add_prop(":cleaned-up", Sexp::make_number(stats.removed)); + sexp.put_props( + ":info", "index"_sym, + ":status", Sexp::Symbol(state), + ":checked", static_cast(stats.checked), + ":updated", static_cast(stats.updated), + ":cleaned-up", static_cast(stats.removed)); - return lst; + return sexp; } void -Server::Private::index_handler(const Parameters& params) +Server::Private::index_handler(const Command& cmd) { Mu::Indexer::Config conf{}; - conf.cleanup = get_bool_or(params, ":cleanup"); - conf.lazy_check = get_bool_or(params, ":lazy-check"); + conf.cleanup = cmd.boolean_arg(":cleanup"); + conf.lazy_check = cmd.boolean_arg(":lazy-check"); // ignore .noupdate with an empty store. conf.ignore_noupdate = store().empty(); @@ -813,20 +788,17 @@ Server::Private::index_handler(const Parameters& params) } void -Server::Private::mkdir_handler(const Parameters& params) +Server::Private::mkdir_handler(const Command& cmd) { - const auto path{get_string_or(params, ":path")}; - if (auto&& res = maildir_mkdir(path, 0755, FALSE); !res) + const auto path{cmd.string_arg(":path").value_or("")}; + if (auto&& res = maildir_mkdir(path, 0755, false); !res) throw res.error(); - Sexp::List lst; - lst.add_prop(":info", Sexp::make_string("mkdir")); - lst.add_prop(":message", Sexp::make_string(format("%s has been created", path.c_str()))); - - output_sexp(std::move(lst)); + output_sexp(Sexp().put_props(":info", "mkdir", + ":message", format("%s has been created", path.c_str()))); } -Sexp::List +Sexp Server::Private::perform_move(Store::Id docid, const Message& msg, const std::string& maildirarg, @@ -846,14 +818,14 @@ Server::Private::perform_move(Store::Id docid, if (!new_msg) throw new_msg.error(); - Sexp::List seq; - seq.add_prop(":update", build_message_sexp(new_msg.value(), docid, {})); + Sexp seq; + seq.put_props(":update", build_message_sexp(new_msg.value(), docid, {})); /* note, the :move t thing is a hint to the frontend that it * could remove the particular header */ if (different_mdir) - seq.add_prop(":move", Sexp::make_symbol("t")); + seq.put_props(":move", Sexp::t()); if (!no_view) - seq.add_prop(":maybe-view", Sexp::make_symbol("t")); + seq.put_props(":maybe-view", Sexp::t()); return seq; } @@ -876,7 +848,7 @@ calculate_message_flags(const Message& msg, Option flagopt) return flags.value(); } -Sexp::List +Sexp Server::Private::move_docid(Store::Id docid, Option flagopt, bool new_name, @@ -891,6 +863,7 @@ Server::Private::move_docid(Store::Id docid, const auto flags = calculate_message_flags(msg.value(), flagopt); auto lst = perform_move(docid, *msg, "", flags, new_name, no_view); + return lst; } @@ -904,18 +877,18 @@ Server::Private::move_docid(Store::Id docid, * */ void -Server::Private::move_handler(const Parameters& params) +Server::Private::move_handler(const Command& cmd) { - auto maildir{get_string_or(params, ":maildir")}; - const auto flagopt{get_string(params, ":flags")}; - const auto rename{get_bool_or(params, ":rename")}; - const auto no_view{get_bool_or(params, ":noupdate")}; - const auto docids{determine_docids(store_, params)}; + auto maildir{cmd.string_arg(":maildir").value_or("")}; + const auto flagopt{cmd.string_arg(":flags").value_or("")}; + const auto rename{cmd.boolean_arg(":rename")}; + const auto no_view{cmd.boolean_arg(":noupdate")}; + const auto docids{determine_docids(store_, cmd)}; if (docids.size() > 1) { if (!maildir.empty()) // ie. duplicate message-ids. throw Mu::Error{Error::Code::Store, - "can't move multiple messages at the same time"}; + "can't move multiple messages at the same time"}; // multi. for (auto&& docid : docids) output_sexp(move_docid(docid, flagopt, @@ -939,57 +912,50 @@ Server::Private::move_handler(const Parameters& params) } void -Server::Private::ping_handler(const Parameters& params) +Server::Private::ping_handler(const Command& cmd) { const auto storecount{store().size()}; if (storecount == (unsigned)-1) throw Error{Error::Code::Store, "failed to read store"}; - const auto queries{get_string_vec(params, ":queries")}; - Sexp::List qresults; + const auto queries{cmd.string_vec_arg(":queries") + .value_or(std::vector{})}; + Sexp qresults; for (auto&& q : queries) { const auto count{store_.count_query(q)}; const auto unreadq{format("flag:unread AND (%s)", q.c_str())}; const auto unread{store_.count_query(unreadq)}; - - Sexp::List lst; - lst.add_prop(":query", Sexp::make_string(q)); - lst.add_prop(":count", Sexp::make_number(count)); - lst.add_prop(":unread", Sexp::make_number(unread)); - - qresults.add(Sexp::make_list(std::move(lst))); + qresults.add(Sexp().put_props(":query", q, + ":count", count, + ":unread", unread)); } - Sexp::List addrs; + Sexp addrs; for (auto&& addr : store().properties().personal_addresses) - addrs.add(Sexp::make_string(addr)); + addrs.add(addr); - Sexp::List lst; - lst.add_prop(":pong", Sexp::make_string("mu")); + auto lst = Sexp().put_props(":pong", "mu"); + auto proplst = Sexp().put_props( + ":version", VERSION, + ":personal-addresses", std::move(addrs), + ":database-path", store().properties().database_path, + ":root-maildir", store().properties().root_maildir, + ":doccount", storecount, + ":queries", std::move(qresults)); - Sexp::List proplst; - proplst.add_prop(":version", Sexp::make_string(VERSION)); - proplst.add_prop(":personal-addresses", Sexp::make_list(std::move(addrs))); - proplst.add_prop(":database-path", Sexp::make_string(store().properties().database_path)); - proplst.add_prop(":root-maildir", Sexp::make_string(store().properties().root_maildir)); - proplst.add_prop(":doccount", Sexp::make_number(storecount)); - proplst.add_prop(":queries", Sexp::make_list(std::move(qresults))); - - lst.add_prop(":props", Sexp::make_list(std::move(proplst))); - - output_sexp(std::move(lst)); + output_sexp(lst.put_props(":props", std::move(proplst))); } void -Server::Private::quit_handler(const Parameters& params) +Server::Private::quit_handler(const Command& cmd) { keep_going_ = false; } void -Server::Private::remove_handler(const Parameters& params) +Server::Private::remove_handler(const Command& cmd) { - const auto docid{get_int_or(params, ":docid")}; + const auto docid{cmd.number_arg(":docid").value_or(0)}; const auto path{path_from_docid(store(), docid)}; if (::unlink(path.c_str()) != 0 && errno != ENOENT) @@ -1000,99 +966,83 @@ Server::Private::remove_handler(const Parameters& params) if (!store().remove_message(path)) g_warning("failed to remove message @ %s (%d) from store", path.c_str(), docid); - // act as if it worked. - - Sexp::List lst; - lst.add_prop(":remove", Sexp::make_number(docid)); - - output_sexp(std::move(lst)); + output_sexp(Sexp().put_props(":remove", docid)); // act as if it worked. } void -Server::Private::sent_handler(const Parameters& params) +Server::Private::sent_handler(const Command& cmd) { - const auto path{get_string_or(params, ":path")}; + const auto path{cmd.string_arg(":path").value_or("")}; const auto docid = store().add_message(path); if (!docid) throw Error{Error::Code::Store, "failed to add path: %s", docid.error().what()}; - - Sexp::List lst; - lst.add_prop(":sent", Sexp::make_symbol("t")); - lst.add_prop(":path", Sexp::make_string(path)); - lst.add_prop(":docid", Sexp::make_number(docid.value())); - - output_sexp(std::move(lst)); + output_sexp(Sexp().put_props( + ":sent", Sexp::t(), + ":path", path, + ":docid", docid.value())); } bool -Server::Private::maybe_mark_as_read(Store::Id docid, Flags oldflags, bool rename) +Server::Private::view_mark_as_read(Store::Id docid, const Message& msg, bool rename) { - const auto newflags{flags_from_delta_expr("+S-u-N", oldflags)}; - if (!newflags || oldflags == *newflags) - return false; // nothing to do. + /* move some message if the flags changes; and send either a :view (main message + * or :update (the rest))*/ + auto maybe_move = [&](Store::Id msg_docid, Flags old_flags, + bool do_rename, bool do_view)->bool { - const auto msg = store().move_message(docid, {}, newflags, rename); - if (!msg) - throw msg.error(); + const auto newflags{flags_from_delta_expr("+S-u-N", old_flags)}; + if (!newflags || old_flags == *newflags) + return false; - /* send an update */ - Sexp::List update; - update.add_prop(":update", build_message_sexp(*msg, docid, {})); - output_sexp(Sexp::make_list(std::move(update))); + auto updated_msg = store().move_message(msg_docid, {}, newflags, do_rename); + if (!updated_msg) + throw updated_msg.error(); - g_debug("marked message %d as read => %s", docid, msg->path().c_str()); + output_sexp(Sexp().put_props(do_view ? ":view" : ":update", + build_message_sexp(*updated_msg, docid, {}))); + return true; + }; - return true; -} + /* now get _al_ the message-ids for the given message-id, + * since, we want to apply the read-status to _all_. */ -bool -Server::Private::maybe_mark_msgid_as_read(const std::string& msgid, bool rename) try -{ - const auto docids = docids_for_msgid(store_, msgid); - if (!docids.empty()) - g_debug("marking %zu messages with message-id '%s' as read", - docids.size(), msgid.c_str()); + /* first the main message */ + bool moved = maybe_move(docid, msg.flags(), rename, true/*:view*/); - for (auto&& docid: docids) + /* now any other message with the same message-id */ + for (auto&& rel_docid: docids_for_msgid(store_, msg.message_id())) { + /* ignore main one since we already handled it. */ + if (rel_docid == docid) + continue; if (auto msg{store().find_message(docid)}; msg) - maybe_mark_as_read(docid, msg->flags(), rename); + maybe_move(rel_docid, msg->flags(), rename, false/*:update*/); + } - return true; - -} catch (...) { /* not fatal */ - g_warning("failed to mark <%s> as read", msgid.c_str()); - return false; + return moved; } void -Server::Private::view_handler(const Parameters& params) +Server::Private::view_handler(const Command& cmd) { - const auto mark_as_read{get_bool_or(params, ":mark-as-read")}; + const auto mark_as_read{cmd.boolean_arg(":mark-as-read")}; /* for now, do _not_ rename, as it seems to confuse mbsync */ const auto rename{false}; //const auto rename{get_bool_or(params, ":rename")}; - const auto docids{determine_docids(store(), params)}; + const auto docids{determine_docids(store(), cmd)}; if (docids.empty()) throw Error{Error::Code::Store, "failed to find message for view"}; - const auto docid{docids.at(0)}; auto msg = store().find_message(docid) .or_else([]{throw Error{Error::Code::Store, "failed to find message for view"};}).value(); - if (mark_as_read) { - // maybe mark the main message as read. - maybe_mark_as_read(docid, msg.flags(), rename); - /* maybe mark _all_ messsage with same message-id as read */ - maybe_mark_msgid_as_read(msg.message_id(), rename); - } - - Sexp::List seq; - seq.add_prop(":view", build_message_sexp(msg, docid, {})); - output_sexp(std::move(seq)); + /* if the message is marked-as-read, the response is handled there; + * otherwise, we do so here. */ + if (!mark_as_read || !view_mark_as_read(docid, msg, rename)) + output_sexp(Sexp().put_props(":view", build_message_sexp(msg, docid, {}))); } Server::Server(Store& store, Server::Output output) diff --git a/lib/mu-server.hh b/lib/mu-server.hh index 95c7ffe2..95837216 100644 --- a/lib/mu-server.hh +++ b/lib/mu-server.hh @@ -49,7 +49,7 @@ public: * @param sexp an s-expression * @param flags flags that influence the behavior */ - using Output = std::function; + using Output = std::function; /** * Construct a new server diff --git a/lib/mu-store.cc b/lib/mu-store.cc index d1323e6a..2ae1a1e8 100644 --- a/lib/mu-store.cc +++ b/lib/mu-store.cc @@ -262,11 +262,10 @@ struct Store::Private { Result Store::Private::update_message_unlocked(Message& msg, Store::Id docid) { - msg.update_cached_sexp(); - return xapian_try_result([&]{ writable_db().replace_document(docid, msg.document().xapian_document()); g_debug("updated message @ %s; docid = %u", msg.path().c_str(), docid); + //g_info("%s", msg.sexp().to_string().c_str()); writable_db().set_metadata(ChangedKey, tstamp_to_string(::time({}))); return Ok(std::move(docid)); }); @@ -275,8 +274,6 @@ Store::Private::update_message_unlocked(Message& msg, Store::Id docid) Result Store::Private::update_message_unlocked(Message& msg, const std::string& path_to_replace) { - msg.update_cached_sexp(); - return xapian_try_result([&]{ auto id = writable_db().replace_document( field_from_id(Field::Id::Path).xapian_term(path_to_replace), diff --git a/lib/tests/test-mu-store-query.cc b/lib/tests/test-mu-store-query.cc index c22cc59d..74a49e4a 100644 --- a/lib/tests/test-mu-store-query.cc +++ b/lib/tests/test-mu-store-query.cc @@ -561,6 +561,7 @@ Boo! assert_equal(qr->begin().message()->path(), old_path); g_assert_true(::access(old_path.c_str(), F_OK) == 0); + /* * mark as read, i.e. move to cur/; ensure it really moved. */ @@ -573,11 +574,13 @@ Boo! g_assert_false(::access(old_path.c_str(), F_OK) == 0); g_assert_true(::access(new_path.c_str(), F_OK) == 0); - /* also ensure thath the cached sexp for the message has been updated; + /* also ensure that the cached sexp for the message has been updated; * that's what mu4e uses */ - const auto moved_sexp{moved_msg->to_sexp().to_sexp_string()}; - /* clumsy */ - g_assert_true(moved_sexp.find(new_path) != std::string::npos); + const auto moved_sexp{moved_msg->sexp()}; + //std::cerr << "@@ " << *moved_msg << '\n'; + g_assert_true(moved_sexp.plistp()); + g_assert_true(moved_sexp.has_prop(":path")); + assert_equal(moved_sexp.get_prop(":path").string(), new_path); /* * find new message with query, ensure it's really that new one. diff --git a/lib/tests/test-mu-store.cc b/lib/tests/test-mu-store.cc index 0de724f0..8369a874 100644 --- a/lib/tests/test-mu-store.cc +++ b/lib/tests/test-mu-store.cc @@ -368,7 +368,7 @@ Yes, that would be excellent. const auto oldpath{msg->path()}; assert_equal(msg->subject(), "Re: multi-eq hash tables"); g_assert_true(msg->docid() != 0); - g_debug("%s", msg->to_sexp().to_sexp_string().c_str()); + g_debug("%s", msg->sexp().to_string().c_str()); // Move the message from new->cur std::this_thread::sleep_for(1s); /* ctime should change */ @@ -379,7 +379,7 @@ Yes, that would be excellent. g_assert_true(::access(msg3->path().c_str(), R_OK)==0); g_assert_false(::access(oldpath.c_str(), R_OK)==0); - g_debug("%s", msg3->to_sexp().to_sexp_string().c_str()); + g_debug("%s", msg3->sexp().to_string().c_str()); g_assert_cmpuint(store->size(), ==, 1); } 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/mu-sexp.cc b/lib/utils/mu-sexp.cc index a8e1ff65..4ebfd70c 100644 --- a/lib/utils/mu-sexp.cc +++ b/lib/utils/mu-sexp.cc @@ -1,6 +1,5 @@ /* -** Copyright (C) 2020 Dirk-Jan C. Binnema -** +** Copyright (C) 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 @@ -18,9 +17,11 @@ ** */ + #include "mu-sexp.hh" #include "mu-utils.hh" +#include #include #include @@ -48,36 +49,38 @@ skip_whitespace(const std::string& s, size_t pos) else break; } - return pos; } -static Sexp parse(const std::string& expr, size_t& pos); +static Result parse(const std::string& expr, size_t& pos); -static Sexp +static Result parse_list(const std::string& expr, size_t& pos) { if (expr[pos] != '(') // sanity check. - throw parsing_error(pos, "expected: '(' but got '%c", expr[pos]); + return Err(parsing_error(pos, "expected: '(' but got '%c", expr[pos])); - Sexp::List list; + Sexp lst{}; ++pos; - while (expr[pos] != ')' && pos != expr.size()) - list.add(parse(expr, pos)); + while (expr[pos] != ')' && pos != expr.size()) { + if (auto&& item = parse(expr, pos); item) + lst.add(std::move(*item)); + else + return Err(item.error()); + } if (expr[pos] != ')') - throw parsing_error(pos, "expected: ')' but got '%c'", expr[pos]); + return Err(parsing_error(pos, "expected: ')' but got '%c'", expr[pos])); ++pos; - return Sexp::make_list(std::move(list)); + return Ok(std::move(lst)); } -// parse string -static Sexp +static Result parse_string(const std::string& expr, size_t& pos) { if (expr[pos] != '"') // sanity check. - throw parsing_error(pos, "expected: '\"'' but got '%c", expr[pos]); + return Err(parsing_error(pos, "expected: '\"'' but got '%c", expr[pos])); bool escape{}; std::string str; @@ -101,14 +104,15 @@ parse_string(const std::string& expr, size_t& pos) throw parsing_error(pos, "unterminated string '%s'", str.c_str()); ++pos; - return Sexp::make_string(std::move(str)); + return Ok(Sexp{std::move(str)}); } -static Sexp + +static Result parse_integer(const std::string& expr, size_t& pos) { if (!isdigit(expr[pos]) && expr[pos] != '-') // sanity check. - throw parsing_error(pos, "expected: but got '%c", expr[pos]); + return Err(parsing_error(pos, "expected: but got '%c", expr[pos])); std::string num; // negative number? if (expr[pos] == '-') { @@ -119,32 +123,32 @@ parse_integer(const std::string& expr, size_t& pos) for (; isdigit(expr[pos]); ++pos) num += expr[pos]; - return Sexp::make_number(::atoi(num.c_str())); + return Ok(Sexp{::atoi(num.c_str())}); } -static Sexp +static Result 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]); + return Err(parsing_error(pos, "expected: |: but got '%c", expr[pos])); - std::string symbol(1, expr[pos]); + std::string symb(1, expr[pos]); for (++pos; isalnum(expr[pos]) || expr[pos] == '-'; ++pos) - symbol += expr[pos]; + symb += expr[pos]; - return Sexp::make_symbol(std::move(symbol)); + return Ok(Sexp{Sexp::Symbol{symb}}); } -static Sexp +static Result 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]); + return Err(parsing_error(pos, "expected: character '%c", expr[pos])); const auto kar = expr[pos]; - const auto node = [&]() -> Sexp { + const auto sexp = std::invoke([&]() -> Result { if (kar == '(') return parse_list(expr, pos); else if (kar == '"') @@ -155,55 +159,52 @@ parse(const std::string& expr, size_t& pos) return parse_symbol(expr, pos); else throw parsing_error(pos, "unexpected character '%c", kar); - }(); + }); pos = skip_whitespace(expr, pos); - return node; + return sexp; } -Sexp -Sexp::make_parse(const std::string& expr) +Result +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; + auto res = ::parse(expr, pos); + if (!res) + return res; + else if (pos != expr.size()) + return Err(parsing_error(pos, "trailing data starting with '%c'", expr[pos])); + else + return res; } std::string -Sexp::to_sexp_string() const +Sexp::to_string(Format fopts) const { std::stringstream sstrm; + const auto splitp{any_of(fopts & Format::SplitList)}; + const auto typeinfop{any_of(fopts & Format::TypeInfo)}; - switch (type()) { - case Type::List: { + if (listp()) { sstrm << '('; bool first{true}; - for (auto&& child : list()) { - sstrm << (first ? "" : " ") << child.to_sexp_string(); + for(auto&& elm: list()) { + sstrm << (first ? "" : " ") << elm.to_string(fopts); first = false; } sstrm << ')'; - - if (any_of(formatting_opts & FormattingOptions::SplitList)) + if (splitp) sstrm << '\n'; - break; - } - case Type::String: - sstrm << quote(value()); - break; - case Type::Raw: - sstrm << value(); - break; - case Type::Number: - case Type::Symbol: - case Type::Empty: - default: sstrm << value(); - } + } else if (stringp()) + sstrm << quote(string()); + else if (numberp()) + sstrm << number(); + else if (symbolp()) + sstrm << symbol(); + + if (typeinfop) + sstrm << '<' << Sexp::type_name(type()) << '>'; return sstrm.str(); } @@ -211,26 +212,26 @@ Sexp::to_sexp_string() const // LCOV_EXCL_START std::string -Sexp::to_json_string() const +Sexp::to_json_string(Format fopts) const { std::stringstream sstrm; switch (type()) { case Type::List: { // property-lists become JSON objects - if (is_prop_list()) { + if (plistp()) { sstrm << "{"; auto it{list().begin()}; bool first{true}; while (it != list().end()) { - sstrm << (first ? "" : ",") << quote(it->value()) << ":"; + sstrm << (first ? "" : ",") << quote(it->string()) << ":"; ++it; sstrm << it->to_json_string(); ++it; first = false; } sstrm << "}"; - if (any_of(formatting_opts & FormattingOptions::SplitList)) + if (any_of(fopts & Format::SplitList)) sstrm << '\n'; } else { // other lists become arrays. sstrm << '['; @@ -240,31 +241,254 @@ Sexp::to_json_string() const first = false; } sstrm << ']'; - if (any_of(formatting_opts & FormattingOptions::SplitList)) + if (any_of(fopts & Format::SplitList)) sstrm << '\n'; } break; } case Type::String: - sstrm << quote(value()); + sstrm << quote(string()); break; - case Type::Raw: // FIXME: implement this. - break; - case Type::Symbol: - if (is_nil()) + if (nilp()) sstrm << "false"; - else if (is_t()) + else if (symbol() == "t") sstrm << "true"; else - sstrm << quote(value()); + sstrm << quote(symbol()); break; case Type::Number: - case Type::Empty: - default: sstrm << value(); + sstrm << number(); + break; + default: + break; } return sstrm.str(); } + + +Sexp& +Sexp::del_prop(const std::string& pname) +{ + if (auto kill_it = find_prop(pname, begin(), end()); kill_it != cend()) + list().erase(kill_it, kill_it + 2); + return *this; +} + + +Sexp::const_iterator +Sexp::find_prop(const std::string& s, + Sexp::const_iterator b, Sexp::const_iterator e) const +{ + for (auto&& it = b; it != e && it+1 != e; it += 2) + if (it->symbolp() && it->symbol() == s) + return it; + return e; +} + +Sexp::iterator +Sexp::find_prop(const std::string& s, + Sexp::iterator b, Sexp::iterator e) +{ + for (auto&& it = b; it != e && it+1 != e; it += 2) + if (it->symbolp() && it->symbol() == s) + return it; + return e; +} + + +bool +Sexp::plistp(Sexp::const_iterator b, Sexp::const_iterator e) const +{ + if (b == e) + return true; + else if (b + 1 == e) + return false; + else + return b->symbolp() && plistp(b + 2, e); +} + + // LCOV_EXCL_STOP + +#if BUILD_TESTS + +#include "mu-test-utils.hh" + +static void +test_list() +{ + { + Sexp s; + g_assert_true(s.listp()); + g_assert_true(s.to_string() == "()"); + g_assert_true(s.empty()); + } + + { + Sexp::List items = { + Sexp("hello"), + Sexp(123), + Sexp::Symbol("world") + }; + Sexp s{std::move(items)}; + g_assert_false(s.empty()); + g_assert_cmpuint(s.size(),==,3); + g_assert_true(s.to_string() == "(\"hello\" 123 world)"); + //g_assert_true(s.to_string() == "(\"hello\" 123 world)"); + } + +} + +static void +test_string() +{ + { + Sexp s("hello"); + g_assert_true(s.stringp()); + g_assert_true(s.string()=="hello"); + g_assert_true(s.to_string()=="\"hello\""); + } + + { + // Sexp s(std::string_view("hel\"lo")); + // g_assert_true(s.is_string()); + // g_assert_cmpstr(s.string().c_str(),==,"hel\"lo"); + // g_assert_cmpstr(s.to_string().c_str(),==,"\"hel\\\"lo\""); + } +} + +static void +test_number() +{ + { + Sexp s(123); + g_assert_true(s.numberp()); + g_assert_cmpint(s.number(),==,123); + g_assert_true(s.to_string() == "123"); + } + + { + Sexp s(true); + g_assert_true(s.numberp()); + g_assert_cmpint(s.number(),==,1); + g_assert_true(s.to_string()=="1"); + } +} + +static void +test_symbol() +{ + { + Sexp s{Sexp::Symbol("hello")}; + g_assert_true(s.symbolp()); + g_assert_true(s.symbol()=="hello"); + g_assert_true (s.to_string()=="hello"); + } + + { + Sexp s{"hello"_sym}; + g_assert_true(s.symbolp()); + g_assert_true(s.symbol()=="hello"); + g_assert_true (s.to_string()=="hello"); + } + +} + +static void +test_multi() +{ + Sexp s{"abc", 123, Sexp::Symbol{"def"}}; + g_assert_true(s.to_string() == "(\"abc\" 123 def)"); +} + + +static void +test_add() +{ + { + Sexp s{"abc", 123}; + s.add("def"_sym); + g_assert_true(s.to_string() == "(\"abc\" 123 def)"); + } +} + +static void +test_add_multi() +{ + { + Sexp s{"abc", 123}; + s.add("def"_sym, 456, Sexp{"boo", 2}); + g_assert_true(s.to_string() == "(\"abc\" 123 def 456 (\"boo\" 2))"); + } + + { + Sexp s{"abc", 123}; + Sexp t{"boo", 2}; + s.add("def"_sym, 456, t); + g_assert_true(s.to_string() == "(\"abc\" 123 def 456 (\"boo\" 2))"); + } + +} + +static void +test_plist() +{ + Sexp s; + s.put_props("hello", "world"_sym, "foo", 123, "bar"_sym, "cuux"); + g_assert_true(s.to_string() == R"((hello world foo 123 bar "cuux"))"); + + s.put_props("hello", 12345); + g_assert_true(s.to_string() == R"((foo 123 bar "cuux" hello 12345))"); +} + + +static void +check_parse(const std::string& expr, const std::string& expected) +{ + auto sexp = Sexp::parse(expr); + assert_valid_result(sexp); + assert_equal(to_string(*sexp), expected); +} + +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\""); +} + +int +main(int argc, char* argv[]) +try { + mu_test_init(&argc, &argv); + + g_test_add_func("/sexp/list", test_list); + g_test_add_func("/sexp/string", test_string); + g_test_add_func("/sexp/number", test_number); + g_test_add_func("/sexp/symbol", test_symbol); + g_test_add_func("/sexp/multi", test_multi); + g_test_add_func("/sexp/add", test_add); + g_test_add_func("/sexp/add-multi", test_add_multi); + g_test_add_func("/sexp/plist", test_plist); + g_test_add_func("/sexp/parser", test_parser); + 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-sexp.hh b/lib/utils/mu-sexp.hh index f04b735b..dde4949d 100644 --- a/lib/utils/mu-sexp.hh +++ b/lib/utils/mu-sexp.hh @@ -20,428 +20,255 @@ #ifndef MU_SEXP_HH__ #define MU_SEXP_HH__ -#include -#include -#include +#include "mu-utils.hh" -#include "utils/mu-utils.hh" -#include "utils/mu-error.hh" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include namespace Mu { -/// Simple s-expression parser & list 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)) -/// Parse node +/** + * A structure somewhat similar to a Lisp s-expression and which can be + * constructed from/to an s-expressing string representation. + * + * A sexp is either an atom (String, Number Symbol) or a List + */ struct Sexp { - /// Node type - enum struct Type { Empty, List, String, Number, Symbol, Raw }; - - /** - * Default CTOR - */ - Sexp() : type_{Type::Empty} {} - - // Underlying data type for list; we'd like to use std::dequeu here, - // but that does not compile with libc++ (it does with libstdc++) - using Seq = std::vector; - - /** - * Make a sexp out of an s-expression string. - * - * @param expr a string containing an s-expression - * - * @return the parsed s-expression, or throw Error. - */ - static Sexp make_parse(const std::string& expr); - - /** - * Make a node for a string/integer/symbol/list value - * - * @param val some value - * @param empty_is_nil turn empty string into a 'nil' symbol - * - * @return a node - */ - static Sexp make_string(std::string&& val, bool empty_is_nil=false) - { - if (empty_is_nil && val.empty()) - return make_symbol("nil"); - else - return Sexp{Type::String, std::move(val)}; - } - static Sexp make_string(const std::string& val, bool empty_is_nil=false) - { - if (empty_is_nil && val.empty()) - return make_symbol("nil"); - else - return Sexp{Type::String, std::string(val)}; - } - - static Sexp make_number(int val) { return Sexp{Type::Number, format("%d", val)}; } - static Sexp make_symbol(std::string&& val) { - if (val.empty()) - throw Error(Error::Code::InvalidArgument, - "symbol must be non-empty"); - return Sexp{Type::Symbol, std::move(val)}; - } - static Sexp make_symbol_sv(std::string_view val) { - return make_symbol(std::string{val}); - } - - /** - * Add a raw string sexp. - * - * @param val value - * - * @return A sexp - */ - static Sexp make_raw(std::string&& val) { - return Sexp{Type::Raw, std::string{val}}; - } - static Sexp make_raw(const std::string& val) { - return make_raw(std::string{val}); - } - - - /** - * - * - * The value of this node; invalid for list nodes. - * - * @return - */ - const std::string& value() const { - if (is_list()) - throw Error(Error::Code::InvalidArgument, "no value for list"); - if (is_empty()) - throw Error{Error::Code::InvalidArgument, "no value for empty"}; - return value_; - } - - /** - * The underlying container of this list node; only valid for lists - * - * @return - */ - const Seq& list() const { - if (!is_list()) - throw Error(Error::Code::InvalidArgument, "not a list"); - return seq_; - } - - /** - * Convert a Sexp to its S-expression string representation - * - * @return the string representation - */ - std::string to_sexp_string() const; - - /** - * Convert a Sexp::Node to its JSON string representation - * - * @return the string representation - */ - std::string to_json_string() const; - - /** - * Return the type of this Node. - * - * @return the type - */ - Type type() const { return type_; } - - /// - /// Helper struct to build mutable lists. - /// - struct List { - List () = default; - List (const Seq& seq): seq_{seq} {} - - /** - * Add a sexp to the list - * - * @param sexp a sexp - * @param args rest arguments - * - * @return a ref to this List (for chaining) - */ - List& add() { return *this; } - List& add(Sexp&& sexp) - { - seq_.emplace_back(std::move(sexp)); - return *this; - } - template List& add(Sexp&& sexp, Args... args) - { - seq_.emplace_back(std::move(sexp)); - seq_.emplace_back(std::forward(args)...); - return *this; - } - - /** - * Add a property (i.e., :key sexp ) to the list. Remove any - * prop with the same name - * - * @param name a property-name. Must start with ':', length > 1 - * @param sexp a sexp - * @param args rest arguments - * - * @return a ref to this List (for chaining) - */ - List& add_prop(std::string&& name, Sexp&& sexp) { - remove_prop(name); - if (!is_prop_name(name)) - throw Error{Error::Code::InvalidArgument, - "invalid property name ('%s')", - name.c_str()}; - seq_.emplace_back(make_symbol(std::move(name))); - seq_.emplace_back(std::move(sexp)); - return *this; - } - template - List& add_prop(std::string&& name, Sexp&& sexp, Args... args) { - remove_prop(name); - add_prop(std::move(name), std::move(sexp)); - add_prop(std::forward(args)...); - return *this; - } - - void remove_prop(const std::string& name) { - if (!is_prop_name(name)) - throw Error{Error::Code::InvalidArgument, - "invalid property name ('%s')", name.c_str()}; - auto it = std::find_if(seq_.begin(), seq_.end(), [&](auto&& elm) { - return elm.type() == Sexp::Type::Symbol && - elm.value() == name; - }); - if (it != seq_.cend() && it + 1 != seq_.cend()) { - /* erase propname and value.*/ - seq_.erase(it, it + 2); - } - } - - /** - * Remove all elements from the list. - */ - void clear() { seq_.clear(); } - - /** - * Get the number of elements in the list - * - * @return number - */ - size_t size() const { return seq_.size(); } - - /** - * Is the list empty? - * - * @return true or false - */ - size_t empty() const { return seq_.empty(); } - - private: - friend struct Sexp; - Seq seq_; + /// Types + using List = std::vector; + using String = std::string; + using Number = int64_t; + struct Symbol { // distinguish from String. + Symbol(const std::string& s): name{s} {} + Symbol(std::string&& s): name(std::move(s)) {} + Symbol(const char* str): Symbol(std::string{str}) {} + Symbol(std::string_view sv): Symbol(std::string{sv}) {} + operator const std::string&() const {return name; } + std::string name; }; + enum struct Type { List, String, Number, Symbol }; + using Data = std::variant; /** - * Construct a list sexp from a List + * Get the type of data * - * @param list a list-list - * @param sexp a Sexp - * @param args rest arguments - * - * @return a sexp. + * @return type */ - static Sexp make_list(List&& list) { return Sexp{Type::List, std::move(list.seq_)}; } - template static Sexp make_list(Sexp&& sexp, Args... args) - { - List lst; - lst.add(std::move(sexp)).add(std::forward(args)...); - return make_list(std::move(lst)); + constexpr Type type() const { return static_cast(data.index()); } + + + /** + * Get the name for some type + * + * @param t type + * + * @return name + */ + static constexpr std::string_view type_name(Type t) { + switch(t) { + case Type::String: + return "string"; + case Type::Number: + return "number"; + case Type::Symbol: + return "symbol"; + case Type::List: + return "list"; + default: + return ""; + } + } + + + constexpr bool stringp() const { return std::holds_alternative(data); } + constexpr bool numberp() const { return std::holds_alternative(data); } + constexpr bool listp() const { return std::holds_alternative(data); } + constexpr bool symbolp() const { return std::holds_alternative(data); } + + constexpr bool nilp() const { return symbolp() && symbol() == "nil"; } + static const Sexp& nil() { static const Sexp nilsym(Symbol{"nil"}); return nilsym; } + static const Sexp& t() { static const Sexp tsym(Symbol{"t"}); return tsym; } + + // Get the specific variant type. + const List& list() const { return std::get(data); } + List& list() { return std::get(data); } + const String& string() const { return std::get(data); } + String& string() { return std::get(data); } + const Number& number() const { return std::get(data); } + Number& number() { return std::get(data); } + const String& symbol() const { return std::get(data).name; } + String& symbol() { return std::get(data).name; } + + /// Default ctor + Sexp():data{List{}} {} // default: an empty list. + + // Copy & move ctors + Sexp(const Sexp& other):data{other.data}{} + Sexp(Sexp&& other):data{std::move(other.data)}{} + + // Assignment + Sexp& operator=(const Sexp& rhs) { + if (this != &rhs) + data = rhs.data; + return *this; + } + Sexp& operator=(Sexp&& rhs) { + if (this != &rhs) + data = std::move(rhs.data); + return *this; + } + + /// Type specific ctors + Sexp(const List& lst): data{lst} {} + Sexp(List&& lst): data{std::move(lst)} {} + + Sexp(const String& str): data{str} {} + Sexp(String&& str): data{std::move(str)} {} + Sexp(const char *str): Sexp{std::string{str}} {} + Sexp(std::string_view sv): Sexp{std::string{sv}} {} + + template> > + Sexp(N n):data{static_cast(n)} {} + + Sexp(const Symbol& sym): data{sym} {} + Sexp(Symbol&& sym): data{std::move(sym)} {} + + /// + template + Sexp(S&& s, T&& t, Args&&... args): data{List()} { + auto& l{std::get(data)}; + l.emplace_back(Sexp(std::forward(s))); + l.emplace_back(Sexp(std::forward(t))); + (l.emplace_back(Sexp(std::forward(args))), ...); } /** - * Construct a property list sexp from a List + * Parse sexp from string * - * @param name the property name; must start wtth ':' - * @param sexp a Sexp - * @param args rest arguments (property list) + * @param str a string * - * @return a sexp. + * @return either an Sexp or an error */ - template - static Sexp make_prop_list(std::string&& name, Sexp&& sexp, Args... args) - { - List list; - list.add_prop(std::move(name), std::move(sexp), std::forward(args)...); - return make_list(std::move(list)); + static Result parse(const std::string& str); + + + /// List specific + using iterator = List::iterator; + using const_iterator = List::const_iterator; + iterator begin() { return list().begin(); } + const_iterator begin() const { return list().begin(); } + const_iterator cbegin() const { return list().cbegin(); } + + iterator end() { return list().end(); } + const_iterator end() const { return list().end(); } + const_iterator cend() const { return list().cend(); } + + bool empty() const { return list().empty(); } + size_t size() const { return list().size(); } + void clear() { list().clear(); } + + /// Adding to lists + Sexp& add(const Sexp& s) { list().emplace_back(s); return *this; } + Sexp& add(Sexp&& s) { list().emplace_back(std::move(s)); return *this; } + Sexp& add() { return *this; } + + template + Sexp& add(V1&& v1, V2&& v2, Args... args) { + return add(std::forward(v1)) + .add(std::forward(v2)) + .add(std::forward(args)...); + } + + // Plist (property lists) + bool plistp() const { return listp() && plistp(cbegin(), cend()); } + Sexp& put_props() { return *this; } // Final case for template pack. + template + Sexp& put_props(PropType&& prop, SexpType&& sexp, Args... args) { + auto&& propname{std::string(prop)}; + return del_prop(propname) + .add(Symbol(std::move(propname)), + std::forward(sexp)) + .put_props(std::forward(args)...); } /** - * Construct a properrty list sexp from a List + * Find the property value for some property by name * - * @param funcname function name for the call - * @param name the property name; must start wtth ':' - * @param sexp a Sexp - * @param args rest arguments (property list) + * @param p property name * - * @return a sexp. + * @return the property if found, or the symbol nil otherwise. */ - template - static Sexp make_call(std::string&& funcname, std::string&& name, Sexp&& sexp, Args... args) - { - List list; - list.add(make_symbol(std::move(funcname))); - list.add_prop(std::move(name), std::move(sexp), std::forward(args)...); - return make_list(std::move(list)); - } - - /// Some type helpers - bool is_list() const { return type() == Type::List; } - bool is_string() const { return type() == Type::String; } - bool is_number() const { return type() == Type::Number; } - bool is_symbol() const { return type() == Type::Symbol; } - bool is_empty() const { return type() == Type::Empty; } - - operator bool() const { return !is_empty(); } - - static constexpr auto SymbolNil{"nil"}; - static constexpr auto SymbolT{"t"}; - bool is_nil() const { return is_symbol() && value() == SymbolNil; } - bool is_t() const { return is_symbol() && value() == SymbolT; } - - /** - * Is this a prop-list? A prop list is a list sexp with alternating - * property / sexp - * - * @return - */ - bool is_prop_list() const - { - if (!is_list() || list().size() % 2 != 0) - return false; + const Sexp& get_prop(const std::string& p) const { + if (auto&& it = find_prop(p, cbegin(), cend()); it != cend()) + return *(std::next(it)); else - return is_prop_list(list().begin(), list().end()); + return Sexp::nil(); + } + bool has_prop(const std::string& s) const { + return find_prop(s, cbegin(), cend())!= cend(); } - /** - * Is this a call? A call is a list sexp with a symbol (function name), - * followed by a prop list - * - * @return - */ - bool is_call() const - { - if (!is_list() || list().size() % 2 != 1 || !list().at(0).is_symbol()) - return false; - else - return is_prop_list(list().begin() + 1, list().end()); - } - - enum struct FormattingOptions { + /// Output to string + enum struct Format { Default = 0, /**< Nothing in particular */ SplitList = 1 << 0, /**< Insert newline after list item */ + TypeInfo = 1 << 1, /**< Show type-info */ }; - FormattingOptions formatting_opts{}; /**< Formatting option for the - * string output */ - -private: - Sexp(Type typearg, std::string&& valuearg) : type_{typearg}, value_{std::move(valuearg)} { - if (is_list()) - throw Error{Error::Code::InvalidArgument, "cannot be a list type"}; - if (is_empty()) - throw Error{Error::Code::InvalidArgument, "cannot be an empty type"}; - } - Sexp(Type typearg, Seq&& seq) : type_{Type::List}, seq_{std::move(seq)} { - if (!is_list()) - throw Error{Error::Code::InvalidArgument, "must be a list type"}; - if (is_empty()) - throw Error{Error::Code::InvalidArgument, "cannot be an empty type"}; - } /** - * Is the sexp a valid property name? + * Get a string representation of the sexp * - * @param sexp a Sexp. - * - * @return true or false. + * @return str */ - static bool is_prop_name(const std::string& str) - { - return str.size() > 1 && str.at(0) == ':'; - } - static bool is_prop_name(const Sexp& sexp) - { - return sexp.is_symbol() && is_prop_name(sexp.value()); - } + std::string to_string(Format fopts=Format::Default) const; + std::string to_json_string(Format fopts=Format::Default) const; - static bool is_prop_list(Seq::const_iterator b, Seq::const_iterator e) - { - while (b != e) { - const Sexp& s{*b}; - if (!is_prop_name(s)) - return false; - if (++b == e) - return false; - ++b; - } - return b == e; - } - - Type type_; /**< Type of node */ - std::string value_; /**< String value of node (only for - * non-Type::Lst)*/ - Seq seq_; /**< Children of node (only for - * Type::Lst) */ + Sexp& del_prop(const std::string& pname); +protected: + const_iterator find_prop(const std::string& s, const_iterator b, + const_iterator e) const; + bool plistp(const_iterator b, const_iterator e) const; +private: + iterator find_prop(const std::string& s,iterator b, + iterator e); + Data data; }; -static inline std::ostream& -operator<<(std::ostream& os, Sexp::Type id) -{ - switch (id) { - case Sexp::Type::List: - os << "list"; - break; - case Sexp::Type::String: - os << "string"; - break; - case Sexp::Type::Number: - os << "number"; - break; - case Sexp::Type::Symbol: - os << "symbol"; - break; - case Sexp::Type::Raw: - os << "raw"; - break; - case Sexp::Type::Empty: - os << "empty"; - break; - default: throw std::runtime_error("unknown node type"); - } +MU_ENABLE_BITOPS(Sexp::Format); +/** + * String-literal; allow for ":foo"_sym to be a symbol + */ +static inline Sexp::Symbol +operator"" _sym(const char* str, std::size_t n) +{ + return Sexp::Symbol{str}; +} + +static inline std::ostream& +operator<<(std::ostream& os, const Sexp::Type& stype) +{ + os << Sexp::type_name(stype); return os; } + static inline std::ostream& operator<<(std::ostream& os, const Sexp& sexp) { - os << sexp.to_sexp_string(); + os << sexp.to_string(); return os; } -static inline std::ostream& -operator<<(std::ostream& os, const Sexp::List& sexp) -{ - os << Sexp::make_list(Sexp::List(sexp)); - return os; -} -MU_ENABLE_BITOPS(Sexp::FormattingOptions); - } // namespace Mu #endif /* MU_SEXP_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; -} diff --git a/mu/mu-cmd-find.cc b/mu/mu-cmd-find.cc index 898244fa..5eee98f9 100644 --- a/mu/mu-cmd-find.cc +++ b/mu/mu-cmd-find.cc @@ -372,12 +372,10 @@ static bool output_sexp(const Option& msg, const OutputInfo& info, const MuConfig* opts, GError** err) { if (msg) { - - if (const auto sexp{msg->cached_sexp()}; !sexp.empty()) - fputs(sexp.c_str(), stdout); + if (const auto sexp{msg->sexp()}; !sexp.empty()) + fputs(sexp.to_string().c_str(), stdout); else - fputs(msg->to_sexp().to_sexp_string().c_str(), stdout); - + fputs(msg->sexp().to_string().c_str(), stdout); fputs("\n", stdout); } @@ -401,7 +399,7 @@ output_json(const Option& msg, const OutputInfo& info, const MuConfig* return true; g_print("%s%s\n", - msg->to_sexp().to_json_string().c_str(), + msg->sexp().to_json_string().c_str(), info.last ? "" : ","); return true; diff --git a/mu/mu-cmd-server.cc b/mu/mu-cmd-server.cc index 779b9cb3..7028ee4e 100644 --- a/mu/mu-cmd-server.cc +++ b/mu/mu-cmd-server.cc @@ -31,7 +31,7 @@ #include "mu-server.hh" #include "utils/mu-utils.hh" -#include "utils/mu-command-parser.hh" +#include "utils/mu-command-handler.hh" #include "utils/mu-readline.hh" using namespace Mu; @@ -82,15 +82,15 @@ cookie(size_t n) } static void -output_sexp_stdout(Sexp&& sexp, Server::OutputFlags flags) +output_sexp_stdout(const Sexp& sexp, Server::OutputFlags flags) { /* if requested, insert \n between list elements; note: * is _not_ inherited by children */ + Sexp::Format fopts{}; if (any_of(flags & Server::OutputFlags::SplitList)) - sexp.formatting_opts |= Sexp::FormattingOptions::SplitList; - - const auto str{sexp.to_sexp_string()}; + fopts |= Sexp::Format::SplitList; + const auto str{sexp.to_string(fopts)}; cookie(str.size() + 1); if (G_UNLIKELY(::puts(str.c_str()) < 0)) { g_critical("failed to write output '%s'", str.c_str()); @@ -104,12 +104,8 @@ output_sexp_stdout(Sexp&& sexp, Server::OutputFlags flags) static void report_error(const Mu::Error& err) noexcept { - Sexp::List e; - - e.add_prop(":error", Sexp::make_number(static_cast(err.code()))); - e.add_prop(":message", Sexp::make_string(err.what())); - - output_sexp_stdout(Sexp::make_list(std::move(e)), + output_sexp_stdout(Sexp(":error"_sym, static_cast(err.code()), + ":message"_sym, err.what()), Server::OutputFlags::Flush); } diff --git a/mu/mu-cmd.cc b/mu/mu-cmd.cc index a171389f..1b6bc36a 100644 --- a/mu/mu-cmd.cc +++ b/mu/mu-cmd.cc @@ -51,7 +51,7 @@ using namespace Mu; static Mu::Result view_msg_sexp(const Message& message, const MuConfig* opts) { - ::fputs(message.to_sexp().to_sexp_string().c_str(), stdout); + ::fputs(message.sexp().to_string().c_str(), stdout); ::fputs("\n", stdout); return Ok();