1
0
mirror of https://github.com/djcb/mu.git synced 2024-06-21 06:56:48 +02:00

Merge branch 'wip/djcb/new-sexp'

This commit is contained in:
Dirk-Jan C. Binnema 2022-11-07 18:39:24 +02:00
commit be86963882
24 changed files with 1407 additions and 1621 deletions

View File

@ -19,6 +19,7 @@
#include "mu-document.hh"
#include "mu-message.hh"
#include "utils/mu-sexp.hh"
#include <cstdint>
#include <glib.h>
@ -30,11 +31,37 @@
#include <string>
#include <utils/mu-utils.hh>
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<typename SexpType> void
Document::put_prop(const std::string& pname, SexpType&& val)
{
sexp_.put_props(pname, std::forward<SexpType>(val));
dirty_sexp_ = true;
}
template<typename SexpType> void
Document::put_prop(const Field& field, SexpType&& val)
{
put_prop(std::string(":") + std::string{field.name},
std::forward<SexpType>(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<std::string>& 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<unsigned>(t >> 16)));
dlist.add(Sexp::make_number(static_cast<unsigned>(t & 0xffff)));
dlist.add(Sexp::make_number(0));
return Sexp::make_list(std::move(dlist));
return Sexp().add(static_cast<unsigned>(t >> 16),
static_cast<unsigned>(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<int64_t>(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)
}
}
});
}

View File

@ -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<typename SexpType> void put_prop(const Field& field, SexpType&& val);
template<typename SexpType> 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

View File

@ -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> field_from_number(size_t id)
return field_from_id(static_cast<Field::Id>(id));
}
} // namespace Mu
#endif /* MU_FIELDS_HH__ */

View File

@ -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<void>
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) == '/'))

View File

@ -23,6 +23,8 @@
#include <memory>
#include <string>
#include <vector>
#include <iostream>
#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__ */

View File

@ -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());
}

View File

@ -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<QueryMatch&> qm) const;
Sexp::List move_docid(Store::Id docid, Option<std::string> flagstr,
bool new_name, bool no_view);
Sexp move_docid(Store::Id docid, Option<std::string> 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<bool> keep_going_{};
std::thread index_thread_;
Store& store_;
Server::Output output_;
const CommandHandler command_handler_;
std::atomic<bool> 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<unsigned>(td >> 16),
static_cast<unsigned>(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<QueryMatch&> 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<int>(errcode)));
err.add_prop(":message", Sexp::make_string(msg));
auto err = Sexp().put_props(":error", static_cast<int>(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("<error>");
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<unsigned>(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<size_t>(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<Store::Id>
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<Field>{
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<size_t>(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<std::string> 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<int>(stats.checked),
":updated", static_cast<int>(stats.updated),
":cleaned-up", static_cast<int>(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("<error>")};
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<std::string> flagopt)
return flags.value();
}
Sexp::List
Sexp
Server::Private::move_docid(Store::Id docid,
Option<std::string> 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<std::string>{})};
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)

View File

@ -49,7 +49,7 @@ public:
* @param sexp an s-expression
* @param flags flags that influence the behavior
*/
using Output = std::function<void(Sexp&& sexp, OutputFlags flags)>;
using Output = std::function<void(const Sexp& sexp, OutputFlags flags)>;
/**
* Construct a new server

View File

@ -262,11 +262,10 @@ struct Store::Private {
Result<Store::Id>
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::Id>
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),

View File

@ -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.

View File

@ -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);
}

View File

@ -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')

View File

@ -0,0 +1,248 @@
/*
** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** This program is free software; you can redistribute it and/or modify it
** under the terms of the GNU General Public License as published by the
** Free Software Foundation; either version 3, or (at your option) any
** later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software Foundation,
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**
*/
#include "mu-command-handler.hh"
#include "mu-error.hh"
#include "mu-utils.hh"
#include <iostream>
#include <algorithm>
using namespace Mu;
Option<std::vector<std::string>>
Command::string_vec_arg(const std::string& name) const
{
auto&& val{arg_val(name, Sexp::Type::List)};
if (!val)
return Nothing;
std::vector<std::string> vec;
for (const auto& item : val->list()) {
if (!item.stringp()) {
// g_warning("command: non-string in string-list for %s: %s",
// name.c_str(), to_string().c_str());
return Nothing;
} else
vec.emplace_back(item.string());
}
return vec;
}
static Result<void>
validate(const CommandHandler::CommandInfoMap& cmap,
const CommandHandler::CommandInfo& cmd_info,
const Command& cmd)
{
if (g_test_verbose())
std::cout << cmd.to_string(Sexp::Format::TypeInfo) << '\n';
// all required parameters must be present
for (auto&& arg : cmd_info.args) {
const auto& argname{arg.first};
const auto& arginfo{arg.second};
// calls use keyword-parameters, e.g.
//
// (my-function :bar 1 :cuux "fnorb")
//
// so, we're looking for the odd-numbered parameters.
const auto param_it = cmd.find_arg(argname);
const auto&& param_val = std::next(param_it);
// it's an error when a required parameter is missing.
if (param_it == cmd.cend()) {
if (arginfo.required)
return Err(Error::Code::Command,
"missing required parameter %s in command '%s'",
argname.c_str(), cmd.to_string().c_str());
continue; // not required
}
// the types must match, but the 'nil' symbol is acceptable as "no value"
if (param_val->type() != arginfo.type && !(param_val->nilp()))
return Err(Error::Code::Command,
"parameter %s expects type %s, but got %s in command '%s'",
argname.c_str(),
to_string(arginfo.type).c_str(),
to_string(param_val->type()).c_str(),
cmd.to_string().c_str());
}
// all parameters must be known
for (auto it = cmd.cbegin() + 1; it != cmd.cend() && it + 1 != cmd.cend(); it += 2) {
const auto& cmdargname{it->symbol()};
if (std::none_of(cmd_info.args.cbegin(), cmd_info.args.cend(),
[&](auto&& arg) { return cmdargname == arg.first; }))
return Err(Error::Code::Command,
"unknown parameter '%s 'in command '%s'",
cmdargname.c_str(), cmd.to_string().c_str());
}
return Ok();
}
Result<void>
CommandHandler::invoke(const Command& cmd, bool do_validate) const
{
const auto cmit{cmap_.find(cmd.name())};
if (cmit == cmap_.cend())
return Err(Error::Code::Command,
"unknown command in command '%s'",
cmd.to_string().c_str());
const auto& cmd_info{cmit->second};
if (do_validate) {
if (auto&& res = validate(cmap_, cmd_info, cmd); !res)
return Err(res.error());
}
if (cmd_info.handler)
cmd_info.handler(cmd);
return Ok();
}
#ifdef BUILD_TESTS
#include "mu-test-utils.hh"
static void
test_args()
{
const auto cmd = Command::make_parse(R"((foo :bar 123 :cuux "456" :boo nil :bah true))");
assert_valid_result(cmd);
assert_equal(cmd->name(), "foo");
g_assert_true(cmd->find_arg(":bar") != cmd->cend());
g_assert_true(cmd->find_arg(":bxr") == cmd->cend());
g_assert_cmpint(cmd->number_arg(":bar").value_or(-1), ==, 123);
g_assert_cmpint(cmd->number_arg(":bor").value_or(-1), ==, -1);
assert_equal(cmd->string_arg(":cuux").value_or(""), "456");
assert_equal(cmd->string_arg(":caax").value_or(""), ""); // not present
assert_equal(cmd->string_arg(":bar").value_or("abc"), "abc"); // wrong type
g_assert_false(cmd->boolean_arg(":boo"));
g_assert_true(cmd->boolean_arg(":bah"));
}
using CommandInfoMap = CommandHandler::CommandInfoMap;
using ArgMap = CommandHandler::ArgMap;
using ArgInfo = CommandHandler::ArgInfo;
using CommandInfo = CommandHandler::CommandInfo;
static bool
call(const CommandInfoMap& cmap, const std::string& str) try {
const auto cmd{Command::make_parse(str)};
if (!cmd)
throw Error(Error::Code::Internal, "invalid sexp str");
const auto res{CommandHandler(cmap).invoke(*cmd)};
return !!res;
} catch (const Error& err) {
g_warning("%s", err.what());
return false;
}
static void
test_command()
{
allow_warnings();
CommandInfoMap cmap;
cmap.emplace(
"my-command",
CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}},
{":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}},
"My command,",
{}});
g_assert_true(call(cmap, "(my-command :param1 \"hello\")"));
g_assert_true(call(cmap, "(my-command :param1 \"hello\" :param2 123)"));
g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 123 :param3 xxx)"));
}
static void
test_command2()
{
allow_warnings();
CommandInfoMap cmap;
cmap.emplace("bla",
CommandInfo{ArgMap{
{":foo", ArgInfo{Sexp::Type::Number, false, "foo"}},
{":bar", ArgInfo{Sexp::Type::String, false, "bar"}},
}, "yeah",
[&](const auto& params) {}});
g_assert_true(call(cmap, "(bla :foo nil)"));
g_assert_false(call(cmap, "(bla :foo nil :bla nil)"));
}
static void
test_command_fail()
{
allow_warnings();
CommandInfoMap cmap;
cmap.emplace(
"my-command",
CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}},
{":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}},
"My command,",
{}});
g_assert_false(call(cmap, "(my-command)"));
g_assert_false(call(cmap, "(my-command2)"));
g_assert_false(call(cmap, "(my-command :param1 123 :param2 123)"));
g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 \"123\")"));
}
int
main(int argc, char* argv[]) try {
mu_test_init(&argc, &argv);
g_test_add_func("/utils/command-parser/args", test_args);
g_test_add_func("/utils/command-parser/command", test_command);
g_test_add_func("/utils/command-parser/command2", test_command2);
g_test_add_func("/utils/command-parser/command-fail", test_command_fail);
return g_test_run();
} catch (const std::runtime_error& re) {
std::cerr << re.what() << "\n";
return 1;
}
#endif /*BUILD_TESTS*/

View File

@ -0,0 +1,298 @@
/*
** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** This program is free software; you can redistribute it and/or modify it
** under the terms of the GNU General Public License as published by the
** Free Software Foundation; either version 3, or (at your option) any
** later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software Foundation,
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**
*/
#ifndef MU_COMMAND_HANDLER_HH__
#define MU_COMMAND_HANDLER_HH__
#include <vector>
#include <string>
#include <ostream>
#include <stdexcept>
#include <unordered_map>
#include <functional>
#include <algorithm>
#include "utils/mu-error.hh"
#include "utils/mu-sexp.hh"
#include "utils/mu-option.hh"
namespace Mu {
///
/// Commands are s-expressions with the follow properties:
/// 1) a command is a list with a command-name as its first argument
/// 2) the rest of the parameters are pairs of colon-prefixed symbol and a value of some
/// type (ie. 'keyword arguments')
/// 3) each command is described by its CommandInfo structure, which defines the type
/// 4) calls to the command must include all required parameters
/// 5) all parameters must be of the specified type; however the symbol 'nil' is allowed
/// for specify a non-required parameter to be absent; this is for convenience on the
/// call side.
struct Command: public Sexp {
using iterator = List::iterator;
using const_iterator = List::const_iterator;
static Result<Command> make(Sexp&& sexp) try {
return Ok(Command{std::move(sexp)});
} catch (const Error& e) {
return Err(e);
}
static Result<Command> make_parse(const std::string& cmdstr) try {
if (auto&& sexp{Sexp::parse(cmdstr)}; !sexp)
return Err(sexp.error());
else
return Ok(Command(std::move(*sexp)));
} catch (const Error& e) {
return Err(e);
}
/**
* Get name of the command (first element) in a command exp
*
* @return name
*/
const std::string& name() const {
return cbegin()->symbol();
}
/**
* Find the argument with the given name.
*
* @param arg name
*
* @return iterator point at the argument, or cend
*/
const_iterator find_arg(const std::string& arg) const {
return find_prop(arg, cbegin() + 1, cend());
}
/**
* Get a string argument
*
* @param name of the argument
*
* @return ref to string, or Nothing if not found
*/
Option<const std::string&> string_arg(const std::string& name) const {
if (auto&& val{arg_val(name, Sexp::Type::String)}; !val)
return Nothing;
else
return val->string();
}
/**
* Get a string-vec argument
*
* @param name of the argument
*
* @return ref to string-vec, or Nothing if not found or some error.
*/
Option<std::vector<std::string>> string_vec_arg(const std::string& name) const;
/**
* Get a symbol argument
*
* @param name of the argument
*
* @return ref to symbol name, or Nothing if not found
*/
Option<const std::string&> symbol_arg(const std::string& name) const {
if (auto&& val{arg_val(name, Sexp::Type::String)}; !val)
return Nothing;
else
return val->symbol();
}
/**
* Get a number argument
*
* @param name of the argument
*
* @return number or Nothing if not found
*/
Option<int> number_arg(const std::string& name) const {
if (auto&& val{arg_val(name, Sexp::Type::Number)}; !val)
return Nothing;
else
return static_cast<int>(val->number());
}
/*
* helpers
*/
/**
* Get a boolean argument
*
* @param name of the argument
*
* @return true if there's a non-nil symbol value for the given
* name; false otherwise.
*/
Option<bool> bool_arg(const std::string& name) const {
if (auto&& symb{symbol_arg(name)}; !symb)
return Nothing;
else
return symb.value() == "nil" ? false : true;
}
/**
* Treat any argument as a boolean
*
* @param name name of the argument
*
* @return false if the the argument is absent or the symbol false;
* otherwise true.
*/
bool boolean_arg(const std::string& name) const {
auto&& it{find_arg(name)};
return (it == cend() || std::next(it)->nilp()) ? false : true;
}
private:
explicit Command(Sexp&& s){
*this = std::move(static_cast<Command&&>(s));
if (!listp() || empty() || !cbegin()->symbolp() ||
!plistp(cbegin() + 1, cend()))
throw Error(Error::Code::Command,
"expected command, got '%s'", to_string().c_str());
}
Option<const Sexp&> arg_val(const std::string& name, Sexp::Type type) const {
if (auto&& it{find_arg(name)}; it == cend()) {
//std::cerr << "--> %s name found " << name << '\n';
return Nothing;
} else if (auto&& val{it + 1}; val->type() != type) {
//std::cerr << "--> type " << Sexp::type_name(it->type()) << '\n';
return Nothing;
} else
return *val;
}
};
struct CommandHandler {
/// Information about a function argument
struct ArgInfo {
ArgInfo(Sexp::Type typearg, bool requiredarg, std::string&& docarg)
: type{typearg}, required{requiredarg}, docstring{std::move(docarg)} {}
const Sexp::Type type; /**< Sexp::Type of the argument */
const bool required; /**< Is this argument required? */
const std::string docstring; /**< Documentation */
};
/// The arguments for a function, which maps their names to the information.
using ArgMap = std::unordered_map<std::string, ArgInfo>;
// A handler function
using Handler = std::function<void(const Command&)>;
/// Information about some command
struct CommandInfo {
CommandInfo(ArgMap&& argmaparg, std::string&& docarg, Handler&& handlerarg)
: args{std::move(argmaparg)}, docstring{std::move(docarg)},
handler{std::move(handlerarg)} {}
const ArgMap args;
const std::string docstring;
const Handler handler;
/**
* Get a sorted list of argument names, for display. Required args come
* first, then alphabetical.
*
* @return vec with the sorted names.
*/
std::vector<std::string> sorted_argnames() const {
// sort args -- by required, then alphabetical.
std::vector<std::string> names;
for (auto&& arg : args)
names.emplace_back(arg.first);
std::sort(names.begin(), names.end(), [&](const auto& name1, const auto& name2) {
const auto& arg1{args.find(name1)->second};
const auto& arg2{args.find(name2)->second};
if (arg1.required != arg2.required)
return arg1.required;
else
return name1 < name2;
});
return names;
}
};
/// All commands, mapping their name to information about them.
using CommandInfoMap = std::unordered_map<std::string, CommandInfo>;
CommandHandler(const CommandInfoMap& cmap): cmap_{cmap} {}
CommandHandler(CommandInfoMap&& cmap): cmap_{std::move(cmap)} {}
const CommandInfoMap& info_map() const { return cmap_; }
/**
* Invoke some command
*
* A command uses keyword arguments, e.g. something like: (foo :bar 1
* :cuux "fnorb")
*
* @param cmd a Sexp describing a command call
* @param validate whether to validate before invoking. Useful during
* development.
*
* Return Ok() or some Error
*/
Result<void> invoke(const Command& cmd, bool validate=true) const;
private:
const CommandInfoMap cmap_;
};
static inline std::ostream&
operator<<(std::ostream& os, const CommandHandler::ArgInfo& info)
{
os << info.type << " (" << (info.required ? "required" : "optional") << ")";
return os;
}
static inline std::ostream&
operator<<(std::ostream& os, const CommandHandler::CommandInfo& info)
{
for (auto&& arg : info.args)
os << " " << arg.first << " " << arg.second << '\n'
<< " " << arg.second.docstring << "\n";
return os;
}
static inline std::ostream&
operator<<(std::ostream& os, const CommandHandler::CommandInfoMap& map)
{
for (auto&& c : map)
os << c.first << '\n' << c.second;
return os;
}
} // namespace Mu
#endif /* MU_COMMAND_HANDLER_HH__ */

View File

@ -1,204 +0,0 @@
/*
** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** This program is free software; you can redistribute it and/or modify it
** under the terms of the GNU General Public License as published by the
** Free Software Foundation; either version 3, or (at your option) any
** later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software Foundation,
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**
*/
#include "mu-command-parser.hh"
#include "mu-error.hh"
#include "mu-utils.hh"
#include <iostream>
#include <algorithm>
using namespace Mu;
using namespace Command;
void
Command::invoke(const Command::CommandMap& cmap, const Sexp& call)
{
if (!call.is_call()) {
throw Mu::Error{Error::Code::Command,
"expected call-sexpr but got %s",
call.to_sexp_string().c_str()};
}
const auto& params{call.list()};
const auto cmd_it = cmap.find(params.at(0).value());
if (cmd_it == cmap.end())
throw Mu::Error{Error::Code::Command,
"unknown command in call %s",
call.to_sexp_string().c_str()};
const auto& cinfo{cmd_it->second};
// all required parameters must be present
for (auto&& arg : cinfo.args) {
const auto& argname{arg.first};
const auto& arginfo{arg.second};
// calls used keyword-parameters, e.g.
// (my-function :bar 1 :cuux "fnorb")
// so, we're looking for the odd-numbered parameters.
const auto param_it = [&]() -> Sexp::Seq::const_iterator {
for (size_t i = 1; i < params.size(); i += 2)
if (params.at(i).is_symbol() && params.at(i).value() == argname)
return params.begin() + i + 1;
return params.end();
}();
// it's an error when a required parameter is missing.
if (param_it == params.end()) {
if (arginfo.required)
throw Mu::Error{Error::Code::Command,
"missing required parameter %s in call %s",
argname.c_str(),
call.to_sexp_string().c_str()};
continue; // not required
}
// the types must match, but the 'nil' symbol is acceptable as
// "no value"
if (param_it->type() != arginfo.type && !(param_it->is_nil()))
throw Mu::Error{Error::Code::Command,
"parameter %s expects type %s, but got %s in call %s",
argname.c_str(),
to_string(arginfo.type).c_str(),
to_string(param_it->type()).c_str(),
call.to_sexp_string().c_str()};
}
// all passed parameters must be known
for (size_t i = 1; i < params.size(); i += 2) {
if (std::none_of(cinfo.args.begin(), cinfo.args.end(), [&](auto&& arg) {
return params.at(i).value() == arg.first;
}))
throw Mu::Error{Error::Code::Command,
"unknown parameter %s in call %s",
params.at(i).value().c_str(),
call.to_sexp_string().c_str()};
}
if (cinfo.handler)
cinfo.handler(params);
}
static Sexp::Seq::const_iterator
find_param_node(const Parameters& params, const std::string& argname)
{
if (params.empty())
throw Error(Error::Code::InvalidArgument, "params must not be empty");
if (argname.empty() || argname.at(0) != ':')
throw Error(Error::Code::InvalidArgument,
"property key must start with ':' but got '%s')",
argname.c_str());
for (size_t i = 1; i < params.size(); i += 2) {
if (i + 1 != params.size() && params.at(i).is_symbol() &&
params.at(i).value() == argname)
return params.begin() + i + 1;
}
return params.end();
}
static Error
wrong_type(Sexp::Type expected, Sexp::Type got)
{
return Error(Error::Code::InvalidArgument,
"expected <%s> but got <%s>",
to_string(expected).c_str(),
to_string(got).c_str());
}
Option<std::string>
Command::get_string(const Parameters& params, const std::string& argname)
{
const auto it = find_param_node(params, argname);
if (it == params.end() || it->is_nil())
return Nothing;
else if (!it->is_string())
throw wrong_type(Sexp::Type::String, it->type());
else
return it->value();
}
Option<std::string>
Command::get_symbol(const Parameters& params, const std::string& argname)
{
const auto it = find_param_node(params, argname);
if (it == params.end() || it->is_nil())
return Nothing;
else if (!it->is_symbol())
throw wrong_type(Sexp::Type::Symbol, it->type());
else
return it->value();
}
Option<int>
Command::get_int(const Parameters& params, const std::string& argname)
{
const auto it = find_param_node(params, argname);
if (it == params.end() || it->is_nil())
return Nothing;
else if (!it->is_number())
throw wrong_type(Sexp::Type::Number, it->type());
else
return ::atoi(it->value().c_str());
}
Option<unsigned>
Command::get_unsigned(const Parameters& params, const std::string& argname)
{
if (auto val = get_int(params, argname); val && *val >= 0)
return val;
else
return Nothing;
}
Option<bool>
Command::get_bool(const Parameters& params, const std::string& argname)
{
const auto it = find_param_node(params, argname);
if (it == params.end())
return Nothing;
else if (!it->is_symbol())
throw wrong_type(Sexp::Type::Symbol, it->type());
else
return it->is_nil() ? false : true;
}
std::vector<std::string>
Command::get_string_vec(const Parameters& params, const std::string& argname)
{
const auto it = find_param_node(params, argname);
if (it == params.end() || it->is_nil())
return {};
else if (!it->is_list())
throw wrong_type(Sexp::Type::List, it->type());
std::vector<std::string> vec;
for (const auto& n : it->list()) {
if (!n.is_string())
throw wrong_type(Sexp::Type::String, n.type());
vec.emplace_back(n.value());
}
return vec;
}

View File

@ -1,180 +0,0 @@
/*
** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** This program is free software; you can redistribute it and/or modify it
** under the terms of the GNU General Public License as published by the
** Free Software Foundation; either version 3, or (at your option) any
** later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software Foundation,
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**
*/
#ifndef MU_COMMAND_PARSER_HH__
#define MU_COMMAND_PARSER_HH__
#include <vector>
#include <string>
#include <ostream>
#include <stdexcept>
#include <unordered_map>
#include <functional>
#include <algorithm>
#include "utils/mu-error.hh"
#include "utils/mu-sexp.hh"
#include "utils/mu-option.hh"
namespace Mu {
namespace Command {
///
/// Commands are s-expressions with the follow properties:
/// 1) a command is a list with a command-name as its first argument
/// 2) the rest of the parameters are pairs of colon-prefixed symbol and a value of some
/// type (ie. 'keyword arguments')
/// 3) each command is described by its CommandInfo structure, which defines the type
/// 4) calls to the command must include all required parameters
/// 5) all parameters must be of the specified type; however the symbol 'nil' is allowed
/// for specify a non-required parameter to be absent; this is for convenience on the
/// call side.
/// Information about a function argument
struct ArgInfo {
ArgInfo(Sexp::Type typearg, bool requiredarg, std::string&& docarg)
: type{typearg}, required{requiredarg}, docstring{std::move(docarg)}
{
}
const Sexp::Type type; /**< Sexp::Type of the argument */
const bool required; /**< Is this argument required? */
const std::string docstring; /**< Documentation */
};
/// The arguments for a function, which maps their names to the information.
using ArgMap = std::unordered_map<std::string, ArgInfo>;
// The parameters to a Handler.
using Parameters = Sexp::Seq;
Option<int> get_int(const Parameters& parms, const std::string& argname);
Option<unsigned> get_unsigned(const Parameters& parms, const std::string& argname);
Option<bool> get_bool(const Parameters& parms, const std::string& argname);
Option<std::string> get_string(const Parameters& parms, const std::string& argname);
Option<std::string> get_symbol(const Parameters& parms, const std::string& argname);
std::vector<std::string> get_string_vec(const Parameters& params, const std::string& argname);
/*
* backward compat
*/
static inline int
get_int_or(const Parameters& parms, const std::string& arg, int alt = 0) {
return get_int(parms, arg).value_or(alt);
}
static inline bool
get_bool_or(const Parameters& parms, const std::string& arg, bool alt = false) {
return get_bool(parms, arg).value_or(alt);
}
static inline std::string
get_string_or(const Parameters& parms, const std::string& arg, const std::string& alt = ""){
return get_string(parms, arg).value_or(alt);
}
static inline std::string
get_symbol_or(const Parameters& parms, const std::string& arg, const std::string& alt = "nil") {
return get_symbol(parms, arg).value_or(alt);
}
// A handler function
using Handler = std::function<void(const Parameters&)>;
/// Information about some command
struct CommandInfo {
CommandInfo(ArgMap&& argmaparg, std::string&& docarg, Handler&& handlerarg)
: args{std::move(argmaparg)}, docstring{std::move(docarg)}, handler{
std::move(handlerarg)}
{
}
const ArgMap args;
const std::string docstring;
const Handler handler;
/**
* Get a sorted list of argument names, for display. Required args come
* first, then alphabetical.
*
* @return vec with the sorted names.
*/
std::vector<std::string> sorted_argnames() const
{ // sort args -- by required, then alphabetical.
std::vector<std::string> names;
for (auto&& arg : args)
names.emplace_back(arg.first);
std::sort(names.begin(), names.end(), [&](const auto& name1, const auto& name2) {
const auto& arg1{args.find(name1)->second};
const auto& arg2{args.find(name2)->second};
if (arg1.required != arg2.required)
return arg1.required;
else
return name1 < name2;
});
return names;
}
};
/// All commands, mapping their name to information about them.
using CommandMap = std::unordered_map<std::string, CommandInfo>;
/**
* Validate that the call (a Sexp) specifies a valid call, then invoke it.
*
* A call uses keyword arguments, e.g. something like:
* (foo :bar 1 :cuux "fnorb")
*
* On error, throw Error.
*
* @param cmap map of commands
* @param call node describing a call.
*/
void invoke(const Command::CommandMap& cmap, const Sexp& call);
static inline std::ostream&
operator<<(std::ostream& os, const Command::ArgInfo& info)
{
os << info.type << " (" << (info.required ? "required" : "optional") << ")";
return os;
}
static inline std::ostream&
operator<<(std::ostream& os, const Command::CommandInfo& info)
{
for (auto&& arg : info.args)
os << " " << arg.first << " " << arg.second << '\n'
<< " " << arg.second.docstring << "\n";
return os;
}
static inline std::ostream&
operator<<(std::ostream& os, const Command::CommandMap& map)
{
for (auto&& c : map)
os << c.first << '\n' << c.second;
return os;
}
} // namespace Command
} // namespace Mu
#endif /* MU_COMMAND_PARSER_HH__ */

View File

@ -1,6 +1,5 @@
/*
** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** This program is free software; you can redistribute it and/or modify it
** under the terms of the GNU General Public License as published by the
@ -18,9 +17,11 @@
**
*/
#include "mu-sexp.hh"
#include "mu-utils.hh"
#include <atomic>
#include <sstream>
#include <array>
@ -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<Sexp> parse(const std::string& expr, size_t& pos);
static Sexp
static Result<Sexp>
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<Sexp>
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<Sexp>
parse_integer(const std::string& expr, size_t& pos)
{
if (!isdigit(expr[pos]) && expr[pos] != '-') // sanity check.
throw parsing_error(pos, "expected: <digit> but got '%c", expr[pos]);
return Err(parsing_error(pos, "expected: <digit> 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<Sexp>
parse_symbol(const std::string& expr, size_t& pos)
{
if (!isalpha(expr[pos]) && expr[pos] != ':') // sanity check.
throw parsing_error(pos, "expected: <alpha>|: but got '%c", expr[pos]);
return Err(parsing_error(pos, "expected: <alpha>|: 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<Sexp>
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<Sexp> {
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>
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*/

View File

@ -20,428 +20,255 @@
#ifndef MU_SEXP_HH__
#define MU_SEXP_HH__
#include <string>
#include <vector>
#include <type_traits>
#include "mu-utils.hh"
#include "utils/mu-utils.hh"
#include "utils/mu-error.hh"
#include <stdexcept>
#include <vector>
#include <string>
#include <string_view>
#include <iostream>
#include <variant>
#include <cinttypes>
#include <ostream>
#include <cassert>
#include <utils/mu-result.hh>
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<Sexp>;
/**
* 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 <typename... Args> List& add(Sexp&& sexp, Args... args)
{
seq_.emplace_back(std::move(sexp));
seq_.emplace_back(std::forward<Args>(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 <typename... Args>
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>(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<Sexp>;
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<List, String, Number, Symbol>;
/**
* 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 <typename... Args> static Sexp make_list(Sexp&& sexp, Args... args)
{
List lst;
lst.add(std::move(sexp)).add(std::forward<Args>(args)...);
return make_list(std::move(lst));
constexpr Type type() const { return static_cast<Type>(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 "<error>";
}
}
constexpr bool stringp() const { return std::holds_alternative<String>(data); }
constexpr bool numberp() const { return std::holds_alternative<Number>(data); }
constexpr bool listp() const { return std::holds_alternative<List>(data); }
constexpr bool symbolp() const { return std::holds_alternative<Symbol>(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<List>(data); }
List& list() { return std::get<List>(data); }
const String& string() const { return std::get<String>(data); }
String& string() { return std::get<String>(data); }
const Number& number() const { return std::get<Number>(data); }
Number& number() { return std::get<Number>(data); }
const String& symbol() const { return std::get<Symbol>(data).name; }
String& symbol() { return std::get<Symbol>(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<typename N, typename = std::enable_if_t<std::is_integral_v<N>> >
Sexp(N n):data{static_cast<Number>(n)} {}
Sexp(const Symbol& sym): data{sym} {}
Sexp(Symbol&& sym): data{std::move(sym)} {}
///
template<typename S, typename T, typename... Args>
Sexp(S&& s, T&& t, Args&&... args): data{List()} {
auto& l{std::get<List>(data)};
l.emplace_back(Sexp(std::forward<S>(s)));
l.emplace_back(Sexp(std::forward<T>(t)));
(l.emplace_back(Sexp(std::forward<Args>(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 <typename... Args>
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>(args)...);
return make_list(std::move(list));
static Result<Sexp> 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 <typename V1, typename V2, typename... Args>
Sexp& add(V1&& v1, V2&& v2, Args... args) {
return add(std::forward<V1>(v1))
.add(std::forward<V2>(v2))
.add(std::forward<Args>(args)...);
}
// Plist (property lists)
bool plistp() const { return listp() && plistp(cbegin(), cend()); }
Sexp& put_props() { return *this; } // Final case for template pack.
template <class PropType, class SexpType, typename... Args>
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<SexpType>(sexp))
.put_props(std::forward<Args>(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 <typename... Args>
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>(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__ */

View File

@ -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] ))

View File

@ -1,149 +0,0 @@
/*
** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** This library is free software; you can redistribute it and/or
** modify it under the terms of the GNU Lesser General Public License
** as published by the Free Software Foundation; either version 2.1
** of the License, or (at your option) any later version.
**
** This library is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
** Lesser General Public License for more details.
**
** You should have received a copy of the GNU Lesser General Public
** License along with this library; if not, write to the Free
** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
** 02110-1301, USA.
*/
#include <vector>
#include <glib.h>
#include <iostream>
#include <sstream>
#include "mu-command-parser.hh"
#include "mu-utils.hh"
#include "mu-test-utils.hh"
using namespace Mu;
static void
test_param_getters()
{
const auto sexp{Sexp::make_parse(R"((foo :bar 123 :cuux "456" :boo nil :bah true))")};
if (g_test_verbose())
std::cout << sexp << "\n";
g_assert_cmpint(Command::get_int_or(sexp.list(), ":bar"), ==, 123);
assert_equal(Command::get_string_or(sexp.list(), ":bra", "bla"), "bla");
assert_equal(Command::get_string_or(sexp.list(), ":cuux"), "456");
g_assert_true(Command::get_bool_or(sexp.list(), ":boo") == false);
g_assert_true(Command::get_bool_or(sexp.list(), ":bah") == true);
}
static bool
call(const Command::CommandMap& cmap, const std::string& str)
try {
const auto sexp{Sexp::make_parse(str)};
invoke(cmap, sexp);
return true;
} catch (const Error& err) {
g_warning("%s", err.what());
return false;
}
static void
test_command()
{
using namespace Command;
allow_warnings();
CommandMap cmap;
cmap.emplace(
"my-command",
CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}},
{":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}},
"My command,",
{}});
g_assert_true(call(cmap, "(my-command :param1 \"hello\")"));
g_assert_true(call(cmap, "(my-command :param1 \"hello\" :param2 123)"));
g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 123 :param3 xxx)"));
}
static void
test_command2()
{
using namespace Command;
allow_warnings();
CommandMap cmap;
cmap.emplace("bla",
CommandInfo{ArgMap{
{":foo", ArgInfo{Sexp::Type::Number, false, "foo"}},
{":bar", ArgInfo{Sexp::Type::String, false, "bar"}},
},
"yeah",
[&](const auto& params) {}});
g_assert_true(call(cmap, "(bla :foo nil)"));
g_assert_false(call(cmap, "(bla :foo nil :bla nil)"));
}
static void
test_command_fail()
{
using namespace Command;
allow_warnings();
CommandMap cmap;
cmap.emplace(
"my-command",
CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}},
{":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}},
"My command,",
{}});
g_assert_false(call(cmap, "(my-command)"));
g_assert_false(call(cmap, "(my-command2)"));
g_assert_false(call(cmap, "(my-command :param1 123 :param2 123)"));
g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 \"123\")"));
}
static void
black_hole()
{
}
int
main(int argc, char* argv[]) try {
mu_test_init(&argc, &argv);
g_test_add_func("/utils/command-parser/param-getters", test_param_getters);
g_test_add_func("/utils/command-parser/command", test_command);
g_test_add_func("/utils/command-parser/command2", test_command2);
g_test_add_func("/utils/command-parser/command-fail", test_command_fail);
g_log_set_handler(
NULL,
(GLogLevelFlags)(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION),
(GLogFunc)black_hole,
NULL);
return g_test_run();
} catch (const std::runtime_error& re) {
std::cerr << re.what() << "\n";
return 1;
}

View File

@ -1,190 +0,0 @@
/*
** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** This library is free software; you can redistribute it and/or
** modify it under the terms of the GNU Lesser General Public License
** as published by the Free Software Foundation; either version 2.1
** of the License, or (at your option) any later version.
**
** This library is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
** Lesser General Public License for more details.
**
** You should have received a copy of the GNU Lesser General Public
** License along with this library; if not, write to the Free
** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
** 02110-1301, USA.
*/
#include <vector>
#include <glib.h>
#include <iostream>
#include <sstream>
#include "mu-command-parser.hh"
#include "mu-utils.hh"
#include "mu-test-utils.hh"
using namespace Mu;
static bool
check_parse(const std::string& expr, const std::string& expected)
{
try {
const auto parsed{to_string(Sexp::make_parse(expr))};
assert_equal(parsed, expected);
return true;
} catch (const Error& err) {
g_warning("caught exception parsing '%s': %s", expr.c_str(), err.what());
return false;
}
}
static void
test_parser()
{
check_parse(":foo-123", ":foo-123");
check_parse("foo", "foo");
check_parse(R"(12345)", "12345");
check_parse(R"(-12345)", "-12345");
check_parse(R"((123 bar "cuux"))", "(123 bar \"cuux\")");
check_parse(R"("foo\"bar\"cuux")", "\"foo\\\"bar\\\"cuux\"");
check_parse(R"("foo
bar")",
"\"foo\nbar\"");
}
static void
test_list()
{
const auto nstr{Sexp::make_string("foo")};
g_assert_true(nstr.value() == "foo");
g_assert_true(nstr.type() == Sexp::Type::String);
assert_equal(nstr.to_sexp_string(), "\"foo\"");
const auto nnum{Sexp::make_number(123)};
g_assert_true(nnum.value() == "123");
g_assert_true(nnum.type() == Sexp::Type::Number);
assert_equal(nnum.to_sexp_string(), "123");
const auto nsym{Sexp::make_symbol("blub")};
g_assert_true(nsym.value() == "blub");
g_assert_true(nsym.type() == Sexp::Type::Symbol);
assert_equal(nsym.to_sexp_string(), "blub");
Sexp::List list;
list.add(Sexp::make_string("foo"))
.add(Sexp::make_number(123))
.add(Sexp::make_symbol("blub"));
const auto nlst = Sexp::make_list(std::move(list));
g_assert_true(nlst.list().size() == 3);
g_assert_true(nlst.type() == Sexp::Type::List);
g_assert_true(nlst.list().at(1).value() == "123");
assert_equal(nlst.to_sexp_string(), "(\"foo\" 123 blub)");
}
static void
test_prop_list()
{
Sexp::List l1;
l1.add_prop(":foo", Sexp::make_string("bar"));
Sexp s2{Sexp::make_list(std::move(l1))};
assert_equal(s2.to_sexp_string(), "(:foo \"bar\")");
g_assert_true(s2.is_prop_list());
Sexp::List l2;
const std::string x{"bar"};
l2.add_prop(":foo", Sexp::make_string(x));
l2.add_prop(":bar", Sexp::make_number(77));
Sexp::List l3;
l3.add_prop(":cuux", Sexp::make_list(std::move(l2)));
Sexp s3{Sexp::make_list(std::move(l3))};
assert_equal(s3.to_sexp_string(), "(:cuux (:foo \"bar\" :bar 77))");
}
static void
test_props()
{
auto sexp2 = Sexp::make_list(Sexp::make_string("foo"),
Sexp::make_number(123),
Sexp::make_symbol("blub"));
auto sexp = Sexp::make_prop_list(":foo",
Sexp::make_string("bär"),
":cuux",
Sexp::make_number(123),
":flub",
Sexp::make_symbol("fnord"),
":boo",
std::move(sexp2));
assert_equal(sexp.to_sexp_string(),
"(:foo \"b\303\244r\" :cuux 123 :flub fnord :boo (\"foo\" 123 blub))");
}
static void
test_prop_list_remove()
{
{
Sexp::List lst;
lst.add_prop(":foo", Sexp::make_string("123"))
.add_prop(":bar", Sexp::make_number(123));
assert_equal(Sexp::make_list(std::move(lst)).to_sexp_string(),
R"((:foo "123" :bar 123))");
}
{
Sexp::List lst;
lst.add_prop(":foo", Sexp::make_string("123"))
.add_prop(":bar", Sexp::make_number(123));
assert_equal(Sexp::make_list(Sexp::List{lst}).to_sexp_string(),
R"((:foo "123" :bar 123))");
lst.remove_prop(":bar");
assert_equal(Sexp::make_list(Sexp::List{lst}).to_sexp_string(),
R"((:foo "123"))");
lst.clear();
g_assert_cmpuint(lst.size(), ==, 0);
}
{
Sexp::List lst;
lst.add(Sexp::make_number(123));
Sexp s2{Sexp::make_list(std::move(lst))};
g_assert_false(s2.is_prop_list());
}
}
int
main(int argc, char* argv[])
try {
mu_test_init(&argc, &argv);
if (argc == 2) {
std::cout << Sexp::make_parse(argv[1]) << '\n';
return 0;
}
g_test_add_func("/utils/sexp/parser", test_parser);
g_test_add_func("/utils/sexp/list", test_list);
g_test_add_func("/utils/sexp/proplist", test_prop_list);
g_test_add_func("/utils/sexp/proplist-remove", test_prop_list_remove);
g_test_add_func("/utils/sexp/props", test_props);
return g_test_run();
} catch (const std::runtime_error& re) {
std::cerr << re.what() << "\n";
return 1;
}

View File

@ -372,12 +372,10 @@ static bool
output_sexp(const Option<Message>& 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<Message>& 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;

View File

@ -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<size_t>(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<size_t>(err.code()),
":message"_sym, err.what()),
Server::OutputFlags::Flush);
}

View File

@ -51,7 +51,7 @@ using namespace Mu;
static Mu::Result<void>
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();