mirror of
https://github.com/djcb/mu.git
synced 2024-06-29 07:51:04 +02:00
Merge branch 'wip/djcb/new-sexp'
This commit is contained in:
commit
be86963882
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
#include "mu-document.hh"
|
#include "mu-document.hh"
|
||||||
#include "mu-message.hh"
|
#include "mu-message.hh"
|
||||||
|
#include "utils/mu-sexp.hh"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
|
@ -30,11 +31,37 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utils/mu-utils.hh>
|
#include <utils/mu-utils.hh>
|
||||||
|
|
||||||
|
|
||||||
using namespace Mu;
|
using namespace Mu;
|
||||||
|
|
||||||
constexpr uint8_t SepaChar1 = 0xfe;
|
constexpr uint8_t SepaChar1 = 0xfe;
|
||||||
constexpr uint8_t SepaChar2 = 0xff;
|
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
|
static void
|
||||||
add_search_term(Xapian::Document& doc, const Field& field, const std::string& val)
|
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
|
void
|
||||||
Document::add(Field::Id id, const std::string& val)
|
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())
|
if (field.is_searchable())
|
||||||
add_search_term(xdoc_, field, val);
|
add_search_term(xdoc_, field, val);
|
||||||
|
|
||||||
if (field.include_in_sexp())
|
if (field.include_in_sexp()) {
|
||||||
sexp_list().add_prop(make_prop_name(field),
|
put_prop(field, val);
|
||||||
Sexp::make_string(std::move(val)));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -95,11 +116,10 @@ Document::add(Field::Id id, const std::vector<std::string>& vals)
|
||||||
add_search_term(xdoc_, field, val); });
|
add_search_term(xdoc_, field, val); });
|
||||||
|
|
||||||
if (field.include_in_sexp()) {
|
if (field.include_in_sexp()) {
|
||||||
Sexp::List elms;
|
Sexp elms{};
|
||||||
for(auto&& val: vals)
|
for(auto&& val: vals)
|
||||||
elms.add(Sexp::make_string(val));
|
elms.add(val);
|
||||||
sexp_list().add_prop(make_prop_name(field),
|
put_prop(field, std::move(elms));
|
||||||
Sexp::make_list(std::move(elms)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,19 +133,16 @@ Document::string_vec_value(Field::Id field_id) const noexcept
|
||||||
static Sexp
|
static Sexp
|
||||||
make_contacts_sexp(const Contacts& contacts)
|
make_contacts_sexp(const Contacts& contacts)
|
||||||
{
|
{
|
||||||
Sexp::List clist;
|
Sexp contacts_sexp;
|
||||||
|
|
||||||
seq_for_each(contacts, [&](auto&& c) {
|
seq_for_each(contacts, [&](auto&& c) {
|
||||||
|
Sexp contact(":email"_sym, c.email);
|
||||||
if (!c.name.empty())
|
if (!c.name.empty())
|
||||||
clist.add(Sexp::make_prop_list(
|
contact.add(":name"_sym, c.name);
|
||||||
":name", Sexp::make_string(c.name),
|
contacts_sexp.add(std::move(contact));
|
||||||
":email", Sexp::make_string(c.email)));
|
|
||||||
else
|
|
||||||
clist.add(Sexp::make_prop_list(
|
|
||||||
":email", Sexp::make_string(c.email)));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Sexp::make_list(std::move(clist));
|
return contacts_sexp;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -168,9 +185,7 @@ Document::add(Field::Id id, const Contacts& contacts)
|
||||||
xdoc_.add_value(field.value_no(), join(cvec, SepaChar1));
|
xdoc_.add_value(field.value_no(), join(cvec, SepaChar1));
|
||||||
|
|
||||||
if (field.include_in_sexp())
|
if (field.include_in_sexp())
|
||||||
sexp_list().add_prop(make_prop_name(field),
|
put_prop(field, make_contacts_sexp(contacts));
|
||||||
make_contacts_sexp(contacts));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Contacts
|
Contacts
|
||||||
|
@ -204,22 +219,19 @@ Document::contacts_value(Field::Id id) const noexcept
|
||||||
void
|
void
|
||||||
Document::add_extra_contacts(const std::string& propname, const Contacts& contacts)
|
Document::add_extra_contacts(const std::string& propname, const Contacts& contacts)
|
||||||
{
|
{
|
||||||
if (!contacts.empty())
|
if (!contacts.empty()) {
|
||||||
sexp_list().add_prop(std::string{propname},
|
put_prop(propname, make_contacts_sexp(contacts));
|
||||||
make_contacts_sexp(contacts));
|
dirty_sexp_ = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static Sexp
|
static Sexp
|
||||||
make_emacs_time_sexp(::time_t t)
|
make_emacs_time_sexp(::time_t t)
|
||||||
{
|
{
|
||||||
Sexp::List dlist;
|
return Sexp().add(static_cast<unsigned>(t >> 16),
|
||||||
|
static_cast<unsigned>(t & 0xffff),
|
||||||
dlist.add(Sexp::make_number(static_cast<unsigned>(t >> 16)));
|
0);
|
||||||
dlist.add(Sexp::make_number(static_cast<unsigned>(t & 0xffff)));
|
|
||||||
dlist.add(Sexp::make_number(0));
|
|
||||||
|
|
||||||
return Sexp::make_list(std::move(dlist));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
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' +
|
* we comply, by storing a number a base-16 and prefixing with 'f' +
|
||||||
* length; such that the strings are sorted in the numerical order.
|
* length; such that the strings are sorted in the numerical order.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const auto field{field_from_id(id)};
|
const auto field{field_from_id(id)};
|
||||||
|
|
||||||
if (field.is_value())
|
if (field.is_value())
|
||||||
|
@ -239,11 +250,9 @@ Document::add(Field::Id id, int64_t val)
|
||||||
|
|
||||||
if (field.include_in_sexp()) {
|
if (field.include_in_sexp()) {
|
||||||
if (field.is_time_t())
|
if (field.is_time_t())
|
||||||
sexp_list().add_prop(make_prop_name(field),
|
put_prop(field, make_emacs_time_sexp(val));
|
||||||
make_emacs_time_sexp(val));
|
|
||||||
else
|
else
|
||||||
sexp_list().add_prop(make_prop_name(field),
|
put_prop(field, val);
|
||||||
Sexp::make_number(val));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,8 +274,7 @@ Document::add(Priority prio)
|
||||||
xdoc_.add_boolean_term(field.xapian_term(to_char(prio)));
|
xdoc_.add_boolean_term(field.xapian_term(to_char(prio)));
|
||||||
|
|
||||||
if (field.include_in_sexp())
|
if (field.include_in_sexp())
|
||||||
sexp_list().add_prop(make_prop_name(field),
|
put_prop(field, Sexp::Symbol(priority_name(prio)));
|
||||||
Sexp::make_symbol_sv(priority_name(prio)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Priority
|
Priority
|
||||||
|
@ -281,52 +289,21 @@ Document::add(Flags flags)
|
||||||
{
|
{
|
||||||
constexpr auto field{field_from_id(Field::Id::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)));
|
xdoc_.add_value(field.value_no(), to_lexnum(static_cast<int64_t>(flags)));
|
||||||
flag_infos_for_each([&](auto&& flag_info) {
|
flag_infos_for_each([&](auto&& flag_info) {
|
||||||
auto term=[&](){return field.xapian_term(flag_info.shortcut_lower());};
|
auto term=[&](){return field.xapian_term(flag_info.shortcut_lower());};
|
||||||
if (any_of(flag_info.flag & flags)) {
|
if (any_of(flag_info.flag & flags)) {
|
||||||
xdoc_.add_boolean_term(term());
|
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())
|
if (field.include_in_sexp())
|
||||||
sexp_list().add_prop(make_prop_name(field),
|
put_prop(field, std::move(flaglist));
|
||||||
Sexp::make_list(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
|
Flags
|
||||||
Document::flags_value() const noexcept
|
Document::flags_value() const noexcept
|
||||||
{
|
{
|
||||||
|
@ -364,7 +341,6 @@ Document::remove(Field::Id field_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -52,13 +52,23 @@ public:
|
||||||
*
|
*
|
||||||
* @param doc
|
* @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.
|
* 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
|
* Get the doc-id for this document
|
||||||
|
@ -138,24 +148,12 @@ public:
|
||||||
void remove(Field::Id field_id);
|
void remove(Field::Id field_id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the cached sexp from the sexp_list_
|
* Get the cached s-expression useful for changing
|
||||||
*/
|
|
||||||
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
|
|
||||||
* it (call update_sexp_cache() when done)
|
* 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
|
* 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());
|
return xdoc_.get_value(field_from_id(field_id).value_no());
|
||||||
}, std::string{});
|
}, std::string{});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a vec of string values.
|
* Get a vec of string values.
|
||||||
*
|
*
|
||||||
|
@ -229,9 +228,13 @@ public:
|
||||||
Flags flags_value() const noexcept;
|
Flags flags_value() const noexcept;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Xapian::Document xdoc_;
|
template<typename SexpType> void put_prop(const Field& field, SexpType&& val);
|
||||||
Sexp::List sexp_list_;
|
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
|
} // namepace Mu
|
||||||
|
|
|
@ -186,8 +186,8 @@ struct Field {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
constexpr char xapian_prefix() const
|
constexpr char xapian_prefix() const {
|
||||||
{ /* xapian uses uppercase shortcuts; toupper is not constexpr */
|
/* xapian uses uppercase shortcuts; toupper is not constexpr */
|
||||||
return shortcut == 0 ? 0 : shortcut - ('a' - 'A');
|
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));
|
return field_from_id(static_cast<Field::Id>(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} // namespace Mu
|
} // namespace Mu
|
||||||
#endif /* MU_FIELDS_HH__ */
|
#endif /* MU_FIELDS_HH__ */
|
||||||
|
|
|
@ -182,23 +182,16 @@ Message::docid() const
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const Mu::Sexp::List&
|
const Mu::Sexp&
|
||||||
Message::to_sexp_list() const
|
Message::sexp() const
|
||||||
{
|
{
|
||||||
return priv_->doc.sexp_list();
|
return priv_->doc.sexp();
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Message::update_cached_sexp()
|
|
||||||
{
|
|
||||||
priv_->doc.update_cached_sexp();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<void>
|
Result<void>
|
||||||
Message::set_maildir(const std::string& maildir)
|
Message::set_maildir(const std::string& maildir)
|
||||||
{
|
{
|
||||||
/* sanity check a little bit */
|
/* sanity check a little bit */
|
||||||
|
|
||||||
if (maildir.empty() ||
|
if (maildir.empty() ||
|
||||||
maildir.at(0) != '/' ||
|
maildir.at(0) != '/' ||
|
||||||
(maildir.size() > 1 && maildir.at(maildir.length()-1) == '/'))
|
(maildir.size() > 1 && maildir.at(maildir.length()-1) == '/'))
|
||||||
|
|
|
@ -23,6 +23,8 @@
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
#include "mu-contact.hh"
|
#include "mu-contact.hh"
|
||||||
#include "mu-priority.hh"
|
#include "mu-priority.hh"
|
||||||
#include "mu-flags.hh"
|
#include "mu-flags.hh"
|
||||||
|
@ -336,37 +338,17 @@ public:
|
||||||
.string_vec_value(Field::Id::Tags);
|
.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
|
* Convert to Sexp
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the s-expression for this message. Stays valid as long
|
* Get the s-expression for this message. Stays valid as long as this
|
||||||
* as this message is.
|
* message is.
|
||||||
*
|
*
|
||||||
* @return a Mu::Sexp::List representing the message.
|
* @return an Sexp representing the message.
|
||||||
*/
|
*/
|
||||||
const Mu::Sexp::List& to_sexp_list() const;
|
const Sexp& sexp() 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();
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* And some non-const message, for updating an existing
|
* And some non-const message, for updating an existing
|
||||||
|
@ -477,5 +459,15 @@ private:
|
||||||
}; // Message
|
}; // Message
|
||||||
MU_ENABLE_BITOPS(Message::Options);
|
MU_ENABLE_BITOPS(Message::Options);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static inline std::ostream&
|
||||||
|
operator<<(std::ostream& os, const Message& msg)
|
||||||
|
{
|
||||||
|
os << msg.sexp();
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
} // Mu
|
} // Mu
|
||||||
#endif /* MU_MESSAGE_HH__ */
|
#endif /* MU_MESSAGE_HH__ */
|
||||||
|
|
|
@ -481,8 +481,7 @@ Content-Type: message/rfc822
|
||||||
)";
|
)";
|
||||||
auto message{Message::make_from_text(msgtext)};
|
auto message{Message::make_from_text(msgtext)};
|
||||||
g_assert_true(!!message);
|
g_assert_true(!!message);
|
||||||
|
//g_assert_true(message->sexp().empty());
|
||||||
g_assert_true(message->cached_sexp().empty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
492
lib/mu-server.cc
492
lib/mu-server.cc
|
@ -42,16 +42,16 @@
|
||||||
|
|
||||||
#include "utils/mu-utils.hh"
|
#include "utils/mu-utils.hh"
|
||||||
#include "utils/mu-option.hh"
|
#include "utils/mu-option.hh"
|
||||||
#include "utils/mu-command-parser.hh"
|
#include "utils/mu-command-handler.hh"
|
||||||
#include "utils/mu-readline.hh"
|
#include "utils/mu-readline.hh"
|
||||||
|
|
||||||
using namespace Mu;
|
using namespace Mu;
|
||||||
using namespace Command;
|
|
||||||
|
|
||||||
/// @brief object to manage the server-context for all commands.
|
/// @brief object to manage the server-context for all commands.
|
||||||
struct Server::Private {
|
struct Server::Private {
|
||||||
Private(Store& store, Output output)
|
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}
|
keep_going_{true}
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
@ -63,14 +63,14 @@ struct Server::Private {
|
||||||
//
|
//
|
||||||
// construction helpers
|
// construction helpers
|
||||||
//
|
//
|
||||||
CommandMap make_command_map();
|
CommandHandler::CommandInfoMap make_command_map();
|
||||||
|
|
||||||
//
|
//
|
||||||
// acccessors
|
// acccessors
|
||||||
Store& store() { return store_; }
|
Store& store() { return store_; }
|
||||||
const Store& store() const { return store_; }
|
const Store& store() const { return store_; }
|
||||||
Indexer& indexer() { return store().indexer(); }
|
Indexer& indexer() { return store().indexer(); }
|
||||||
const CommandMap& command_map() const { return command_map_; }
|
//CommandMap& command_map() const { return command_map_; }
|
||||||
|
|
||||||
//
|
//
|
||||||
// invoke
|
// invoke
|
||||||
|
@ -80,32 +80,29 @@ struct Server::Private {
|
||||||
//
|
//
|
||||||
// output
|
// output
|
||||||
//
|
//
|
||||||
void output_sexp(Sexp&& sexp,Server::OutputFlags flags = {}) const {
|
void output_sexp(const Sexp& sexp, Server::OutputFlags flags = {}) const {
|
||||||
if (output_)
|
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;
|
size_t output_results(const QueryResults& qres, size_t batch_size) const;
|
||||||
|
|
||||||
//
|
//
|
||||||
// handlers for various commands.
|
// handlers for various commands.
|
||||||
//
|
//
|
||||||
void add_handler(const Parameters& params);
|
void add_handler(const Command& cmd);
|
||||||
void compose_handler(const Parameters& params);
|
void compose_handler(const Command& cmd);
|
||||||
void contacts_handler(const Parameters& params);
|
void contacts_handler(const Command& cmd);
|
||||||
void find_handler(const Parameters& params);
|
void find_handler(const Command& cmd);
|
||||||
void help_handler(const Parameters& params);
|
void help_handler(const Command& cmd);
|
||||||
void index_handler(const Parameters& params);
|
void index_handler(const Command& cmd);
|
||||||
void move_handler(const Parameters& params);
|
void move_handler(const Command& cmd);
|
||||||
void mkdir_handler(const Parameters& params);
|
void mkdir_handler(const Command& cmd);
|
||||||
void ping_handler(const Parameters& params);
|
void ping_handler(const Command& cmd);
|
||||||
void quit_handler(const Parameters& params);
|
void quit_handler(const Command& cmd);
|
||||||
void remove_handler(const Parameters& params);
|
void remove_handler(const Command& cmd);
|
||||||
void sent_handler(const Parameters& params);
|
void sent_handler(const Command& cmd);
|
||||||
void view_handler(const Parameters& params);
|
void view_handler(const Command& cmd);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// helpers
|
// helpers
|
||||||
|
@ -113,22 +110,21 @@ private:
|
||||||
Store::Id docid,
|
Store::Id docid,
|
||||||
const Option<QueryMatch&> qm) const;
|
const Option<QueryMatch&> qm) const;
|
||||||
|
|
||||||
Sexp::List move_docid(Store::Id docid, Option<std::string> flagstr,
|
Sexp move_docid(Store::Id docid, Option<std::string> flagstr,
|
||||||
bool new_name, bool no_view);
|
bool new_name, bool no_view);
|
||||||
|
|
||||||
Sexp::List perform_move(Store::Id docid,
|
Sexp perform_move(Store::Id docid,
|
||||||
const Message& msg,
|
const Message& msg,
|
||||||
const std::string& maildirarg,
|
const std::string& maildirarg,
|
||||||
Flags flags,
|
Flags flags,
|
||||||
bool new_name,
|
bool new_name,
|
||||||
bool no_view);
|
bool no_view);
|
||||||
|
|
||||||
bool maybe_mark_as_read(Store::Id docid, Flags old_flags, bool rename);
|
bool view_mark_as_read(Store::Id docid, const Message& msg, bool rename);
|
||||||
bool maybe_mark_msgid_as_read(const std::string& msgid, bool rename);
|
|
||||||
|
|
||||||
Store& store_;
|
Store& store_;
|
||||||
Server::Output output_;
|
Server::Output output_;
|
||||||
const CommandMap command_map_;
|
const CommandHandler command_handler_;
|
||||||
std::atomic<bool> keep_going_{};
|
std::atomic<bool> keep_going_{};
|
||||||
std::thread index_thread_;
|
std::thread index_thread_;
|
||||||
};
|
};
|
||||||
|
@ -136,64 +132,60 @@ private:
|
||||||
static Sexp
|
static Sexp
|
||||||
build_metadata(const QueryMatch& qmatch)
|
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())};
|
const auto td{::atoi(qmatch.thread_date.c_str())};
|
||||||
dlist.add(Sexp::make_number((unsigned)(td >> 16)));
|
auto mdata = Sexp().put_props(":path", qmatch.thread_path,
|
||||||
dlist.add(Sexp::make_number((unsigned)(td & 0xffff)));
|
":level", qmatch.thread_level,
|
||||||
dlist.add(Sexp::make_number(0));
|
":date", qmatch.thread_date,
|
||||||
mdata.add_prop(":date-tstamp", Sexp::make_list(std::move(dlist)));
|
":data-tstamp", Sexp().add(static_cast<unsigned>(td >> 16),
|
||||||
|
static_cast<unsigned>(td & 0xffff),
|
||||||
|
0));
|
||||||
if (qmatch.has_flag(QueryMatch::Flags::Root))
|
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))
|
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))
|
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))
|
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))
|
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))
|
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))
|
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))
|
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
|
* A message here consists of a message s-expression with optionally a :docid
|
||||||
* optionally a :meta expression added.
|
* and/or :meta expression added.
|
||||||
*/
|
*/
|
||||||
Sexp
|
Sexp
|
||||||
Server::Private::build_message_sexp(const Message& msg,
|
Server::Private::build_message_sexp(const Message& msg,
|
||||||
Store::Id docid,
|
Store::Id docid,
|
||||||
const Option<QueryMatch&> qm) const
|
const Option<QueryMatch&> qm) const
|
||||||
{
|
{
|
||||||
auto sexp_list = msg.to_sexp_list();
|
Sexp sexp{msg.sexp()}; // copy
|
||||||
if (docid != 0)
|
if (docid != 0)
|
||||||
sexp_list.add_prop(":docid", Sexp::make_number(docid));
|
sexp.put_props(":docid", docid);
|
||||||
if (qm)
|
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()
|
Server::Private::make_command_map()
|
||||||
{
|
{
|
||||||
CommandMap cmap;
|
CommandHandler::CommandInfoMap cmap;
|
||||||
|
|
||||||
|
using CommandInfo = CommandHandler::CommandInfo;
|
||||||
|
using ArgMap = CommandHandler::ArgMap;
|
||||||
|
using ArgInfo = CommandHandler::ArgInfo;
|
||||||
|
using Type = Sexp::Type;
|
||||||
using Type = Sexp::Type;
|
using Type = Sexp::Type;
|
||||||
|
|
||||||
cmap.emplace(
|
cmap.emplace(
|
||||||
|
@ -352,12 +344,10 @@ make_error(Error::Code errcode, const char* frm, ...)
|
||||||
g_vasprintf(&msg, frm, ap);
|
g_vasprintf(&msg, frm, ap);
|
||||||
va_end(ap);
|
va_end(ap);
|
||||||
|
|
||||||
Sexp::List err;
|
auto err = Sexp().put_props(":error", static_cast<int>(errcode),
|
||||||
err.add_prop(":error", Sexp::make_number(static_cast<int>(errcode)));
|
":message", msg);
|
||||||
err.add_prop(":message", Sexp::make_string(msg));
|
|
||||||
g_free(msg);
|
g_free(msg);
|
||||||
|
return err;
|
||||||
return Sexp::make_list(std::move(err));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
@ -365,10 +355,14 @@ Server::Private::invoke(const std::string& expr) noexcept
|
||||||
{
|
{
|
||||||
if (!keep_going_)
|
if (!keep_going_)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
auto call{Sexp::Sexp::make_parse(expr)};
|
auto cmd{Command::make_parse(std::string{expr})};
|
||||||
Command::invoke(command_map(), call);
|
if (!cmd)
|
||||||
|
throw cmd.error();
|
||||||
|
|
||||||
|
auto res = command_handler_.invoke(*cmd);
|
||||||
|
if (!res)
|
||||||
|
throw res.error();
|
||||||
|
|
||||||
} catch (const Mu::Error& me) {
|
} catch (const Mu::Error& me) {
|
||||||
output_sexp(make_error(me.code(), "%s", me.what()));
|
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)
|
* information about the newly added message (details: see code below)
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
Server::Private::add_handler(const Parameters& params)
|
Server::Private::add_handler(const Command& cmd)
|
||||||
{
|
{
|
||||||
auto path{get_string_or(params, ":path")};
|
auto path{cmd.string_arg(":path")};
|
||||||
const auto docid_res{store().add_message(path)};
|
const auto docid_res{store().add_message(*path)};
|
||||||
|
|
||||||
if (!docid_res)
|
if (!docid_res)
|
||||||
throw docid_res.error();
|
throw docid_res.error();
|
||||||
|
|
||||||
const auto docid{docid_res.value()};
|
const auto docid{docid_res.value()};
|
||||||
|
output_sexp(Sexp().put_props(":info", "add"_sym,
|
||||||
Sexp::List expr;
|
":path", *path,
|
||||||
expr.add_prop(":info", Sexp::make_symbol("add"));
|
":docid", docid));
|
||||||
expr.add_prop(":path", Sexp::make_string(path));
|
|
||||||
expr.add_prop(":docid", Sexp::make_number(docid));
|
|
||||||
|
|
||||||
output_sexp(Sexp::make_list(std::move(expr)));
|
|
||||||
|
|
||||||
auto msg_res{store().find_message(docid)};
|
auto msg_res{store().find_message(docid)};
|
||||||
if (!msg_res)
|
if (!msg_res)
|
||||||
throw Error(Error::Code::Store,
|
throw Error(Error::Code::Store,
|
||||||
"failed to get message at %s (docid=%u): %s",
|
"failed to get message at %s (docid=%u): %s",
|
||||||
path.c_str(), docid);
|
path->c_str(), docid);
|
||||||
|
|
||||||
Sexp::List update;
|
output_sexp(Sexp().put_props(":update",
|
||||||
update.add_prop(":update", build_message_sexp(msg_res.value(), docid, {}));
|
build_message_sexp(msg_res.value(), docid, {})));
|
||||||
output_sexp(Sexp::make_list(std::move(update)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 'compose' produces the un-changed *original* message sexp (ie., the message
|
/* '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)
|
if (!res)
|
||||||
throw res.error();
|
throw res.error();
|
||||||
|
|
||||||
Sexp::List pi;
|
Sexp pi;
|
||||||
|
|
||||||
if (auto cdescr = part.content_description(); cdescr)
|
if (auto cdescr = part.content_description(); cdescr)
|
||||||
pi.add_prop(":description", Sexp::make_string(*cdescr));
|
pi.put_props(":description", *cdescr);
|
||||||
else if (cooked_name)
|
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.put_props(":file-name", fname,
|
||||||
pi.add_prop(":mime-type", Sexp::make_string(
|
":mime-type",
|
||||||
part.mime_type().value_or("application/octet-stream")));
|
part.mime_type().value_or("application/octet-stream"));
|
||||||
|
|
||||||
return Some(Sexp::make_list(std::move(pi)));
|
return Some(std::move(pi));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
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;
|
auto comp_lst = Sexp().put_props(":compose", Sexp::Symbol(ctype));
|
||||||
comp_lst.add_prop(":compose", Sexp::make_symbol(std::string(ctype)));
|
|
||||||
|
|
||||||
|
|
||||||
if (ctype == "reply" || ctype == "forward" ||
|
if (ctype == "reply" || ctype == "forward" ||
|
||||||
ctype == "edit" || ctype == "resend") {
|
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)};
|
auto msg{store().find_message(docid)};
|
||||||
if (!msg)
|
if (!msg)
|
||||||
throw Error{Error::Code::Store, "failed to get message %u", docid};
|
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") {
|
if (ctype == "forward") {
|
||||||
// when forwarding, attach any attachment in the orig
|
// when forwarding, attach any attachment in the orig
|
||||||
size_t index{};
|
size_t index{};
|
||||||
Sexp::List attseq;
|
Sexp attseq;
|
||||||
for (auto&& part: msg->parts()) {
|
for (auto&& part: msg->parts()) {
|
||||||
if (auto attsexp = maybe_add_attachment(
|
if (auto attsexp = maybe_add_attachment(
|
||||||
*msg, part, index); attsexp) {
|
*msg, part, index); attsexp) {
|
||||||
|
@ -498,10 +485,8 @@ Server::Private::compose_handler(const Parameters& params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!attseq.empty()) {
|
if (!attseq.empty()) {
|
||||||
comp_lst.add_prop(":include",
|
comp_lst.put_props(":include", std::move(attseq),
|
||||||
Sexp::make_list(std::move(attseq)));
|
":cache-path", *msg->cache_path());
|
||||||
comp_lst.add_prop(":cache-path",
|
|
||||||
Sexp::make_string(*msg->cache_path()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -509,16 +494,16 @@ Server::Private::compose_handler(const Parameters& params)
|
||||||
throw Error(Error::Code::InvalidArgument, "invalid compose type '%s'",
|
throw Error(Error::Code::InvalidArgument, "invalid compose type '%s'",
|
||||||
ctype.c_str());
|
ctype.c_str());
|
||||||
|
|
||||||
output_sexp(std::move(comp_lst));
|
output_sexp(comp_lst);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
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 personal = cmd.boolean_arg(":personal");
|
||||||
const auto afterstr = get_string_or(params, ":after");
|
const auto afterstr = cmd.string_arg(":after").value_or("");
|
||||||
const auto tstampstr = get_string_or(params, ":tstamp");
|
const auto tstampstr = cmd.string_arg(":tstamp").value_or("");
|
||||||
const auto maxnum = get_int_or(params, ":maxnum", 0 /*unlimited*/);
|
const auto maxnum = cmd.number_arg(":maxnum").value_or(0 /*unlimited*/);
|
||||||
|
|
||||||
const auto after{afterstr.empty() ? 0 :
|
const auto after{afterstr.empty() ? 0 :
|
||||||
parse_date_time(afterstr, true).value_or(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));
|
static_cast<size_t>(tstamp));
|
||||||
|
|
||||||
auto n{0};
|
auto n{0};
|
||||||
Sexp::List contacts;
|
Sexp contacts;
|
||||||
store().contacts_cache().for_each([&](const Contact& ci) {
|
store().contacts_cache().for_each([&](const Contact& ci) {
|
||||||
|
|
||||||
/* since the last time we got some contacts */
|
/* since the last time we got some contacts */
|
||||||
|
@ -545,19 +530,17 @@ Server::Private::contacts_handler(const Parameters& params)
|
||||||
|
|
||||||
n++;
|
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;
|
return maxnum == 0 || n < maxnum;
|
||||||
});
|
});
|
||||||
|
|
||||||
Sexp::List seq;
|
Sexp seq;
|
||||||
seq.add_prop(":contacts", Sexp::make_list(std::move(contacts)));
|
seq.put_props(":contacts", contacts,
|
||||||
seq.add_prop(":tstamp",
|
":tstamp", format("%" G_GINT64_FORMAT, g_get_monotonic_time()));
|
||||||
Sexp::make_string(format("%" G_GINT64_FORMAT,
|
|
||||||
g_get_monotonic_time())));
|
|
||||||
|
|
||||||
/* dump the contacts cache as a giant sexp */
|
/* dump the contacts cache as a giant sexp */
|
||||||
g_debug("sending %d of %zu contact(s)", n, store().contacts_cache().size());
|
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 */
|
/* 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>
|
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)};
|
auto docid{cmd.number_arg(":docid").value_or(0)};
|
||||||
const auto msgid{get_string_or(params, ":msgid")};
|
const auto msgid{cmd.string_arg(":msgid").value_or("")};
|
||||||
|
|
||||||
if ((docid == 0) == msgid.empty())
|
if ((docid == 0) == msgid.empty())
|
||||||
throw Error(Error::Code::InvalidArgument,
|
throw Error(Error::Code::InvalidArgument,
|
||||||
|
@ -632,12 +615,12 @@ size_t
|
||||||
Server::Private::output_results(const QueryResults& qres, size_t batch_size) const
|
Server::Private::output_results(const QueryResults& qres, size_t batch_size) const
|
||||||
{
|
{
|
||||||
size_t n{};
|
size_t n{};
|
||||||
Sexp::List headers;
|
Sexp headers;
|
||||||
|
|
||||||
const auto output_batch = [&](Sexp::List&& hdrs) {
|
const auto output_batch = [&](Sexp&& hdrs) {
|
||||||
Sexp::List batch;
|
Sexp batch;
|
||||||
batch.add_prop(":headers", Sexp::make_list(std::move(hdrs)));
|
batch.put_props(":headers", std::move(hdrs));
|
||||||
output_sexp(std::move(batch));
|
output_sexp(batch);
|
||||||
};
|
};
|
||||||
|
|
||||||
for (auto&& mi : qres) {
|
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.
|
// construct sexp for a single header.
|
||||||
auto qm{mi.query_match()};
|
auto qm{mi.query_match()};
|
||||||
auto msgsexp{build_message_sexp(*msg, mi.doc_id(), qm)};
|
auto msgsexp{build_message_sexp(*msg, mi.doc_id(), qm)};
|
||||||
msgsexp.formatting_opts |= Sexp::FormattingOptions::SplitList;
|
|
||||||
headers.add(std::move(msgsexp));
|
headers.add(std::move(msgsexp));
|
||||||
// we output up-to-batch-size lists of messages. It's much
|
// we output up-to-batch-size lists of messages. It's much
|
||||||
// faster (on the emacs side) to handle such batches than single
|
// 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
|
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 q{cmd.string_arg(":query").value_or("")};
|
||||||
const auto threads{get_bool_or(params, ":threads", false)};
|
const auto threads{cmd.boolean_arg(":threads")};
|
||||||
// perhaps let mu4e set this as frame-lines of the appropriate frame.
|
// 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 batch_size{cmd.number_arg(":batch-size").value_or(110)};
|
||||||
const auto sortfieldstr{get_symbol_or(params, ":sortfield", "")};
|
const auto sortfieldstr{cmd.symbol_arg(":sortfield").value_or("")};
|
||||||
const auto descending{get_bool_or(params, ":descending", false)};
|
const auto descending{cmd.boolean_arg(":descending")};
|
||||||
const auto maxnum{get_int_or(params, ":maxnum", -1 /*unlimited*/)};
|
const auto maxnum{cmd.number_arg(":maxnum").value_or(-1) /*unlimited*/};
|
||||||
const auto skip_dups{get_bool_or(params, ":skip-dups", false)};
|
const auto skip_dups{cmd.boolean_arg(":skip-dups")};
|
||||||
const auto include_related{get_bool_or(params, ":include-related", false)};
|
const auto include_related{cmd.boolean_arg(":include-related")};
|
||||||
|
|
||||||
auto sort_field = std::invoke([&]()->Option<Field>{
|
auto sort_field = std::invoke([&]()->Option<Field>{
|
||||||
if (sortfieldstr.size() < 2)
|
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
|
/* before sending new results, send an 'erase' message, so the frontend
|
||||||
* knows it should erase the headers buffer. this will ensure that the
|
* knows it should erase the headers buffer. this will ensure that the
|
||||||
* output of two finds will not be mixed. */
|
* output of two finds will not be mixed. */
|
||||||
{
|
output_sexp(Sexp().put_props(":erase", Sexp::t()));
|
||||||
Sexp::List lst;
|
|
||||||
lst.add_prop(":erase", Sexp::make_symbol("t"));
|
|
||||||
output_sexp(std::move(lst));
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto foundnum{output_results(*qres, static_cast<size_t>(batch_size))};
|
const auto foundnum{output_results(*qres, static_cast<size_t>(batch_size))};
|
||||||
|
output_sexp(Sexp().put_props(":found", foundnum));
|
||||||
{
|
|
||||||
Sexp::List lst;
|
|
||||||
lst.add_prop(":found", Sexp::make_number(foundnum));
|
|
||||||
output_sexp(std::move(lst));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
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 command{cmd.symbol_arg(":command").value_or("")};
|
||||||
const auto full{get_bool_or(params, ":full", !command.empty())};
|
const auto full{cmd.bool_arg(":full").value_or(!command.empty())};
|
||||||
|
auto&& info_map{command_handler_.info_map()};
|
||||||
|
|
||||||
if (command.empty()) {
|
if (command.empty()) {
|
||||||
std::cout << ";; Commands are s-expressions of the form\n"
|
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;
|
std::vector<std::string> names;
|
||||||
for (auto&& name_cmd : command_map())
|
for (auto&& name_cmd: info_map)
|
||||||
names.emplace_back(name_cmd.first);
|
names.emplace_back(name_cmd.first);
|
||||||
|
|
||||||
std::sort(names.begin(), names.end());
|
std::sort(names.begin(), names.end());
|
||||||
|
|
||||||
for (auto&& name : names) {
|
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)
|
if (!command.empty() && name != command)
|
||||||
continue;
|
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)
|
get_stats(const Indexer::Progress& stats, const std::string& state)
|
||||||
{
|
{
|
||||||
Sexp::List lst;
|
Sexp sexp;
|
||||||
|
|
||||||
lst.add_prop(":info", Sexp::make_symbol("index"));
|
sexp.put_props(
|
||||||
lst.add_prop(":status", Sexp::make_symbol(std::string{state}));
|
":info", "index"_sym,
|
||||||
lst.add_prop(":checked", Sexp::make_number(stats.checked));
|
":status", Sexp::Symbol(state),
|
||||||
lst.add_prop(":updated", Sexp::make_number(stats.updated));
|
":checked", static_cast<int>(stats.checked),
|
||||||
lst.add_prop(":cleaned-up", Sexp::make_number(stats.removed));
|
":updated", static_cast<int>(stats.updated),
|
||||||
|
":cleaned-up", static_cast<int>(stats.removed));
|
||||||
|
|
||||||
return lst;
|
return sexp;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Server::Private::index_handler(const Parameters& params)
|
Server::Private::index_handler(const Command& cmd)
|
||||||
{
|
{
|
||||||
Mu::Indexer::Config conf{};
|
Mu::Indexer::Config conf{};
|
||||||
conf.cleanup = get_bool_or(params, ":cleanup");
|
conf.cleanup = cmd.boolean_arg(":cleanup");
|
||||||
conf.lazy_check = get_bool_or(params, ":lazy-check");
|
conf.lazy_check = cmd.boolean_arg(":lazy-check");
|
||||||
// ignore .noupdate with an empty store.
|
// ignore .noupdate with an empty store.
|
||||||
conf.ignore_noupdate = store().empty();
|
conf.ignore_noupdate = store().empty();
|
||||||
|
|
||||||
|
@ -813,20 +788,17 @@ Server::Private::index_handler(const Parameters& params)
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Server::Private::mkdir_handler(const Parameters& params)
|
Server::Private::mkdir_handler(const Command& cmd)
|
||||||
{
|
{
|
||||||
const auto path{get_string_or(params, ":path")};
|
const auto path{cmd.string_arg(":path").value_or("<error>")};
|
||||||
if (auto&& res = maildir_mkdir(path, 0755, FALSE); !res)
|
if (auto&& res = maildir_mkdir(path, 0755, false); !res)
|
||||||
throw res.error();
|
throw res.error();
|
||||||
|
|
||||||
Sexp::List lst;
|
output_sexp(Sexp().put_props(":info", "mkdir",
|
||||||
lst.add_prop(":info", Sexp::make_string("mkdir"));
|
":message", format("%s has been created", path.c_str())));
|
||||||
lst.add_prop(":message", Sexp::make_string(format("%s has been created", path.c_str())));
|
|
||||||
|
|
||||||
output_sexp(std::move(lst));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Sexp::List
|
Sexp
|
||||||
Server::Private::perform_move(Store::Id docid,
|
Server::Private::perform_move(Store::Id docid,
|
||||||
const Message& msg,
|
const Message& msg,
|
||||||
const std::string& maildirarg,
|
const std::string& maildirarg,
|
||||||
|
@ -846,14 +818,14 @@ Server::Private::perform_move(Store::Id docid,
|
||||||
if (!new_msg)
|
if (!new_msg)
|
||||||
throw new_msg.error();
|
throw new_msg.error();
|
||||||
|
|
||||||
Sexp::List seq;
|
Sexp seq;
|
||||||
seq.add_prop(":update", build_message_sexp(new_msg.value(), docid, {}));
|
seq.put_props(":update", build_message_sexp(new_msg.value(), docid, {}));
|
||||||
/* note, the :move t thing is a hint to the frontend that it
|
/* note, the :move t thing is a hint to the frontend that it
|
||||||
* could remove the particular header */
|
* could remove the particular header */
|
||||||
if (different_mdir)
|
if (different_mdir)
|
||||||
seq.add_prop(":move", Sexp::make_symbol("t"));
|
seq.put_props(":move", Sexp::t());
|
||||||
if (!no_view)
|
if (!no_view)
|
||||||
seq.add_prop(":maybe-view", Sexp::make_symbol("t"));
|
seq.put_props(":maybe-view", Sexp::t());
|
||||||
|
|
||||||
return seq;
|
return seq;
|
||||||
}
|
}
|
||||||
|
@ -876,7 +848,7 @@ calculate_message_flags(const Message& msg, Option<std::string> flagopt)
|
||||||
return flags.value();
|
return flags.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
Sexp::List
|
Sexp
|
||||||
Server::Private::move_docid(Store::Id docid,
|
Server::Private::move_docid(Store::Id docid,
|
||||||
Option<std::string> flagopt,
|
Option<std::string> flagopt,
|
||||||
bool new_name,
|
bool new_name,
|
||||||
|
@ -891,6 +863,7 @@ Server::Private::move_docid(Store::Id docid,
|
||||||
|
|
||||||
const auto flags = calculate_message_flags(msg.value(), flagopt);
|
const auto flags = calculate_message_flags(msg.value(), flagopt);
|
||||||
auto lst = perform_move(docid, *msg, "", flags, new_name, no_view);
|
auto lst = perform_move(docid, *msg, "", flags, new_name, no_view);
|
||||||
|
|
||||||
return lst;
|
return lst;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -904,13 +877,13 @@ Server::Private::move_docid(Store::Id docid,
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
Server::Private::move_handler(const Parameters& params)
|
Server::Private::move_handler(const Command& cmd)
|
||||||
{
|
{
|
||||||
auto maildir{get_string_or(params, ":maildir")};
|
auto maildir{cmd.string_arg(":maildir").value_or("")};
|
||||||
const auto flagopt{get_string(params, ":flags")};
|
const auto flagopt{cmd.string_arg(":flags").value_or("")};
|
||||||
const auto rename{get_bool_or(params, ":rename")};
|
const auto rename{cmd.boolean_arg(":rename")};
|
||||||
const auto no_view{get_bool_or(params, ":noupdate")};
|
const auto no_view{cmd.boolean_arg(":noupdate")};
|
||||||
const auto docids{determine_docids(store_, params)};
|
const auto docids{determine_docids(store_, cmd)};
|
||||||
|
|
||||||
if (docids.size() > 1) {
|
if (docids.size() > 1) {
|
||||||
if (!maildir.empty()) // ie. duplicate message-ids.
|
if (!maildir.empty()) // ie. duplicate message-ids.
|
||||||
|
@ -939,57 +912,50 @@ Server::Private::move_handler(const Parameters& params)
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Server::Private::ping_handler(const Parameters& params)
|
Server::Private::ping_handler(const Command& cmd)
|
||||||
{
|
{
|
||||||
const auto storecount{store().size()};
|
const auto storecount{store().size()};
|
||||||
if (storecount == (unsigned)-1)
|
if (storecount == (unsigned)-1)
|
||||||
throw Error{Error::Code::Store, "failed to read store"};
|
throw Error{Error::Code::Store, "failed to read store"};
|
||||||
|
|
||||||
const auto queries{get_string_vec(params, ":queries")};
|
const auto queries{cmd.string_vec_arg(":queries")
|
||||||
Sexp::List qresults;
|
.value_or(std::vector<std::string>{})};
|
||||||
|
Sexp qresults;
|
||||||
for (auto&& q : queries) {
|
for (auto&& q : queries) {
|
||||||
const auto count{store_.count_query(q)};
|
const auto count{store_.count_query(q)};
|
||||||
const auto unreadq{format("flag:unread AND (%s)", q.c_str())};
|
const auto unreadq{format("flag:unread AND (%s)", q.c_str())};
|
||||||
const auto unread{store_.count_query(unreadq)};
|
const auto unread{store_.count_query(unreadq)};
|
||||||
|
qresults.add(Sexp().put_props(":query", q,
|
||||||
Sexp::List lst;
|
":count", count,
|
||||||
lst.add_prop(":query", Sexp::make_string(q));
|
":unread", unread));
|
||||||
lst.add_prop(":count", Sexp::make_number(count));
|
|
||||||
lst.add_prop(":unread", Sexp::make_number(unread));
|
|
||||||
|
|
||||||
qresults.add(Sexp::make_list(std::move(lst)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Sexp::List addrs;
|
Sexp addrs;
|
||||||
for (auto&& addr : store().properties().personal_addresses)
|
for (auto&& addr : store().properties().personal_addresses)
|
||||||
addrs.add(Sexp::make_string(addr));
|
addrs.add(addr);
|
||||||
|
|
||||||
Sexp::List lst;
|
auto lst = Sexp().put_props(":pong", "mu");
|
||||||
lst.add_prop(":pong", Sexp::make_string("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;
|
output_sexp(lst.put_props(":props", std::move(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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Server::Private::quit_handler(const Parameters& params)
|
Server::Private::quit_handler(const Command& cmd)
|
||||||
{
|
{
|
||||||
keep_going_ = false;
|
keep_going_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
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)};
|
const auto path{path_from_docid(store(), docid)};
|
||||||
|
|
||||||
if (::unlink(path.c_str()) != 0 && errno != ENOENT)
|
if (::unlink(path.c_str()) != 0 && errno != ENOENT)
|
||||||
|
@ -1000,99 +966,83 @@ Server::Private::remove_handler(const Parameters& params)
|
||||||
|
|
||||||
if (!store().remove_message(path))
|
if (!store().remove_message(path))
|
||||||
g_warning("failed to remove message @ %s (%d) from store", path.c_str(), docid);
|
g_warning("failed to remove message @ %s (%d) from store", path.c_str(), docid);
|
||||||
// act as if it worked.
|
output_sexp(Sexp().put_props(":remove", docid)); // act as if it worked.
|
||||||
|
|
||||||
Sexp::List lst;
|
|
||||||
lst.add_prop(":remove", Sexp::make_number(docid));
|
|
||||||
|
|
||||||
output_sexp(std::move(lst));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
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);
|
const auto docid = store().add_message(path);
|
||||||
if (!docid)
|
if (!docid)
|
||||||
throw Error{Error::Code::Store, "failed to add path: %s",
|
throw Error{Error::Code::Store, "failed to add path: %s",
|
||||||
docid.error().what()};
|
docid.error().what()};
|
||||||
|
output_sexp(Sexp().put_props(
|
||||||
Sexp::List lst;
|
":sent", Sexp::t(),
|
||||||
lst.add_prop(":sent", Sexp::make_symbol("t"));
|
":path", path,
|
||||||
lst.add_prop(":path", Sexp::make_string(path));
|
":docid", docid.value()));
|
||||||
lst.add_prop(":docid", Sexp::make_number(docid.value()));
|
|
||||||
|
|
||||||
output_sexp(std::move(lst));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
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)};
|
/* move some message if the flags changes; and send either a :view (main message
|
||||||
if (!newflags || oldflags == *newflags)
|
* or :update (the rest))*/
|
||||||
return false; // nothing to do.
|
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);
|
const auto newflags{flags_from_delta_expr("+S-u-N", old_flags)};
|
||||||
if (!msg)
|
if (!newflags || old_flags == *newflags)
|
||||||
throw msg.error();
|
|
||||||
|
|
||||||
/* send an update */
|
|
||||||
Sexp::List update;
|
|
||||||
update.add_prop(":update", build_message_sexp(*msg, docid, {}));
|
|
||||||
output_sexp(Sexp::make_list(std::move(update)));
|
|
||||||
|
|
||||||
g_debug("marked message %d as read => %s", docid, msg->path().c_str());
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
|
|
||||||
for (auto&& docid: docids)
|
|
||||||
if (auto msg{store().find_message(docid)}; msg)
|
|
||||||
maybe_mark_as_read(docid, msg->flags(), rename);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (...) { /* not fatal */
|
|
||||||
g_warning("failed to mark <%s> as read", msgid.c_str());
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
auto updated_msg = store().move_message(msg_docid, {}, newflags, do_rename);
|
||||||
|
if (!updated_msg)
|
||||||
|
throw updated_msg.error();
|
||||||
|
|
||||||
|
output_sexp(Sexp().put_props(do_view ? ":view" : ":update",
|
||||||
|
build_message_sexp(*updated_msg, docid, {})));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* now get _al_ the message-ids for the given message-id,
|
||||||
|
* since, we want to apply the read-status to _all_. */
|
||||||
|
|
||||||
|
/* first the main message */
|
||||||
|
bool moved = maybe_move(docid, msg.flags(), rename, true/*:view*/);
|
||||||
|
|
||||||
|
/* 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_move(rel_docid, msg->flags(), rename, false/*:update*/);
|
||||||
|
}
|
||||||
|
|
||||||
|
return moved;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
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 */
|
/* for now, do _not_ rename, as it seems to confuse mbsync */
|
||||||
const auto rename{false};
|
const auto rename{false};
|
||||||
//const auto rename{get_bool_or(params, ":rename")};
|
//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())
|
if (docids.empty())
|
||||||
throw Error{Error::Code::Store, "failed to find message for view"};
|
throw Error{Error::Code::Store, "failed to find message for view"};
|
||||||
|
|
||||||
const auto docid{docids.at(0)};
|
const auto docid{docids.at(0)};
|
||||||
auto msg = store().find_message(docid)
|
auto msg = store().find_message(docid)
|
||||||
.or_else([]{throw Error{Error::Code::Store,
|
.or_else([]{throw Error{Error::Code::Store,
|
||||||
"failed to find message for view"};}).value();
|
"failed to find message for view"};}).value();
|
||||||
|
|
||||||
if (mark_as_read) {
|
/* if the message is marked-as-read, the response is handled there;
|
||||||
// maybe mark the main message as read.
|
* otherwise, we do so here. */
|
||||||
maybe_mark_as_read(docid, msg.flags(), rename);
|
if (!mark_as_read || !view_mark_as_read(docid, msg, rename))
|
||||||
/* maybe mark _all_ messsage with same message-id as read */
|
output_sexp(Sexp().put_props(":view", build_message_sexp(msg, docid, {})));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Server::Server(Store& store, Server::Output output)
|
Server::Server(Store& store, Server::Output output)
|
||||||
|
|
|
@ -49,7 +49,7 @@ public:
|
||||||
* @param sexp an s-expression
|
* @param sexp an s-expression
|
||||||
* @param flags flags that influence the behavior
|
* @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
|
* Construct a new server
|
||||||
|
|
|
@ -262,11 +262,10 @@ struct Store::Private {
|
||||||
Result<Store::Id>
|
Result<Store::Id>
|
||||||
Store::Private::update_message_unlocked(Message& msg, Store::Id docid)
|
Store::Private::update_message_unlocked(Message& msg, Store::Id docid)
|
||||||
{
|
{
|
||||||
msg.update_cached_sexp();
|
|
||||||
|
|
||||||
return xapian_try_result([&]{
|
return xapian_try_result([&]{
|
||||||
writable_db().replace_document(docid, msg.document().xapian_document());
|
writable_db().replace_document(docid, msg.document().xapian_document());
|
||||||
g_debug("updated message @ %s; docid = %u", msg.path().c_str(), docid);
|
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({})));
|
writable_db().set_metadata(ChangedKey, tstamp_to_string(::time({})));
|
||||||
return Ok(std::move(docid));
|
return Ok(std::move(docid));
|
||||||
});
|
});
|
||||||
|
@ -275,8 +274,6 @@ Store::Private::update_message_unlocked(Message& msg, Store::Id docid)
|
||||||
Result<Store::Id>
|
Result<Store::Id>
|
||||||
Store::Private::update_message_unlocked(Message& msg, const std::string& path_to_replace)
|
Store::Private::update_message_unlocked(Message& msg, const std::string& path_to_replace)
|
||||||
{
|
{
|
||||||
msg.update_cached_sexp();
|
|
||||||
|
|
||||||
return xapian_try_result([&]{
|
return xapian_try_result([&]{
|
||||||
auto id = writable_db().replace_document(
|
auto id = writable_db().replace_document(
|
||||||
field_from_id(Field::Id::Path).xapian_term(path_to_replace),
|
field_from_id(Field::Id::Path).xapian_term(path_to_replace),
|
||||||
|
|
|
@ -561,6 +561,7 @@ Boo!
|
||||||
assert_equal(qr->begin().message()->path(), old_path);
|
assert_equal(qr->begin().message()->path(), old_path);
|
||||||
g_assert_true(::access(old_path.c_str(), F_OK) == 0);
|
g_assert_true(::access(old_path.c_str(), F_OK) == 0);
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* mark as read, i.e. move to cur/; ensure it really moved.
|
* 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_false(::access(old_path.c_str(), F_OK) == 0);
|
||||||
g_assert_true(::access(new_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 */
|
* that's what mu4e uses */
|
||||||
const auto moved_sexp{moved_msg->to_sexp().to_sexp_string()};
|
const auto moved_sexp{moved_msg->sexp()};
|
||||||
/* clumsy */
|
//std::cerr << "@@ " << *moved_msg << '\n';
|
||||||
g_assert_true(moved_sexp.find(new_path) != std::string::npos);
|
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.
|
* find new message with query, ensure it's really that new one.
|
||||||
|
|
|
@ -368,7 +368,7 @@ Yes, that would be excellent.
|
||||||
const auto oldpath{msg->path()};
|
const auto oldpath{msg->path()};
|
||||||
assert_equal(msg->subject(), "Re: multi-eq hash tables");
|
assert_equal(msg->subject(), "Re: multi-eq hash tables");
|
||||||
g_assert_true(msg->docid() != 0);
|
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
|
// Move the message from new->cur
|
||||||
std::this_thread::sleep_for(1s); /* ctime should change */
|
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_true(::access(msg3->path().c_str(), R_OK)==0);
|
||||||
g_assert_false(::access(oldpath.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);
|
g_assert_cmpuint(store->size(), ==, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,14 +16,13 @@
|
||||||
|
|
||||||
|
|
||||||
lib_mu_utils=static_library('mu-utils', [
|
lib_mu_utils=static_library('mu-utils', [
|
||||||
'mu-command-parser.cc',
|
'mu-command-handler.cc',
|
||||||
'mu-logger.cc',
|
'mu-logger.cc',
|
||||||
'mu-option.cc',
|
'mu-option.cc',
|
||||||
'mu-readline.cc',
|
'mu-readline.cc',
|
||||||
'mu-sexp.cc',
|
'mu-sexp.cc',
|
||||||
'mu-test-utils.cc',
|
'mu-test-utils.cc',
|
||||||
'mu-util.c',
|
'mu-util.c',
|
||||||
'mu-util.h',
|
|
||||||
'mu-utils.cc'],
|
'mu-utils.cc'],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
glib_dep,
|
glib_dep,
|
||||||
|
@ -39,4 +38,19 @@ lib_mu_utils_dep = declare_dependency(
|
||||||
include_directories: include_directories(['.', '..'])
|
include_directories: include_directories(['.', '..'])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#
|
||||||
|
# tests
|
||||||
|
#
|
||||||
|
test('test-sexp',
|
||||||
|
executable('test-sexp', 'mu-sexp.cc',
|
||||||
|
install: false,
|
||||||
|
cpp_args: ['-DBUILD_TESTS'],
|
||||||
|
dependencies: [glib_dep, lib_mu_utils_dep]))
|
||||||
|
|
||||||
|
test('test-command-handler',
|
||||||
|
executable('test-command-handler', 'mu-command-handler.cc',
|
||||||
|
install: false,
|
||||||
|
cpp_args: ['-DBUILD_TESTS'],
|
||||||
|
dependencies: [glib_dep, lib_mu_utils_dep]))
|
||||||
|
|
||||||
subdir('tests')
|
subdir('tests')
|
||||||
|
|
248
lib/utils/mu-command-handler.cc
Normal file
248
lib/utils/mu-command-handler.cc
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
/*
|
||||||
|
** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||||
|
**
|
||||||
|
** This program is free software; you can redistribute it and/or modify it
|
||||||
|
** under the terms of the GNU General Public License as published by the
|
||||||
|
** Free Software Foundation; either version 3, or (at your option) any
|
||||||
|
** later version.
|
||||||
|
**
|
||||||
|
** This program is distributed in the hope that it will be useful,
|
||||||
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
** GNU General Public License for more details.
|
||||||
|
**
|
||||||
|
** You should have received a copy of the GNU General Public License
|
||||||
|
** along with this program; if not, write to the Free Software Foundation,
|
||||||
|
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
**
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "mu-command-handler.hh"
|
||||||
|
#include "mu-error.hh"
|
||||||
|
#include "mu-utils.hh"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
using namespace Mu;
|
||||||
|
|
||||||
|
Option<std::vector<std::string>>
|
||||||
|
Command::string_vec_arg(const std::string& name) const
|
||||||
|
{
|
||||||
|
auto&& val{arg_val(name, Sexp::Type::List)};
|
||||||
|
if (!val)
|
||||||
|
return Nothing;
|
||||||
|
|
||||||
|
std::vector<std::string> vec;
|
||||||
|
for (const auto& item : val->list()) {
|
||||||
|
if (!item.stringp()) {
|
||||||
|
// g_warning("command: non-string in string-list for %s: %s",
|
||||||
|
// name.c_str(), to_string().c_str());
|
||||||
|
return Nothing;
|
||||||
|
} else
|
||||||
|
vec.emplace_back(item.string());
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Result<void>
|
||||||
|
validate(const CommandHandler::CommandInfoMap& cmap,
|
||||||
|
const CommandHandler::CommandInfo& cmd_info,
|
||||||
|
const Command& cmd)
|
||||||
|
{
|
||||||
|
if (g_test_verbose())
|
||||||
|
std::cout << cmd.to_string(Sexp::Format::TypeInfo) << '\n';
|
||||||
|
|
||||||
|
// all required parameters must be present
|
||||||
|
for (auto&& arg : cmd_info.args) {
|
||||||
|
|
||||||
|
const auto& argname{arg.first};
|
||||||
|
const auto& arginfo{arg.second};
|
||||||
|
|
||||||
|
// calls use keyword-parameters, e.g.
|
||||||
|
//
|
||||||
|
// (my-function :bar 1 :cuux "fnorb")
|
||||||
|
//
|
||||||
|
// so, we're looking for the odd-numbered parameters.
|
||||||
|
const auto param_it = cmd.find_arg(argname);
|
||||||
|
const auto&& param_val = std::next(param_it);
|
||||||
|
// it's an error when a required parameter is missing.
|
||||||
|
if (param_it == cmd.cend()) {
|
||||||
|
if (arginfo.required)
|
||||||
|
return Err(Error::Code::Command,
|
||||||
|
"missing required parameter %s in command '%s'",
|
||||||
|
argname.c_str(), cmd.to_string().c_str());
|
||||||
|
continue; // not required
|
||||||
|
}
|
||||||
|
|
||||||
|
// the types must match, but the 'nil' symbol is acceptable as "no value"
|
||||||
|
if (param_val->type() != arginfo.type && !(param_val->nilp()))
|
||||||
|
return Err(Error::Code::Command,
|
||||||
|
"parameter %s expects type %s, but got %s in command '%s'",
|
||||||
|
argname.c_str(),
|
||||||
|
to_string(arginfo.type).c_str(),
|
||||||
|
to_string(param_val->type()).c_str(),
|
||||||
|
cmd.to_string().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// all parameters must be known
|
||||||
|
for (auto it = cmd.cbegin() + 1; it != cmd.cend() && it + 1 != cmd.cend(); it += 2) {
|
||||||
|
const auto& cmdargname{it->symbol()};
|
||||||
|
if (std::none_of(cmd_info.args.cbegin(), cmd_info.args.cend(),
|
||||||
|
[&](auto&& arg) { return cmdargname == arg.first; }))
|
||||||
|
return Err(Error::Code::Command,
|
||||||
|
"unknown parameter '%s 'in command '%s'",
|
||||||
|
cmdargname.c_str(), cmd.to_string().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void>
|
||||||
|
CommandHandler::invoke(const Command& cmd, bool do_validate) const
|
||||||
|
{
|
||||||
|
const auto cmit{cmap_.find(cmd.name())};
|
||||||
|
if (cmit == cmap_.cend())
|
||||||
|
return Err(Error::Code::Command,
|
||||||
|
"unknown command in command '%s'",
|
||||||
|
cmd.to_string().c_str());
|
||||||
|
|
||||||
|
const auto& cmd_info{cmit->second};
|
||||||
|
if (do_validate) {
|
||||||
|
if (auto&& res = validate(cmap_, cmd_info, cmd); !res)
|
||||||
|
return Err(res.error());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd_info.handler)
|
||||||
|
cmd_info.handler(cmd);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef BUILD_TESTS
|
||||||
|
|
||||||
|
#include "mu-test-utils.hh"
|
||||||
|
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_args()
|
||||||
|
{
|
||||||
|
const auto cmd = Command::make_parse(R"((foo :bar 123 :cuux "456" :boo nil :bah true))");
|
||||||
|
assert_valid_result(cmd);
|
||||||
|
|
||||||
|
assert_equal(cmd->name(), "foo");
|
||||||
|
g_assert_true(cmd->find_arg(":bar") != cmd->cend());
|
||||||
|
g_assert_true(cmd->find_arg(":bxr") == cmd->cend());
|
||||||
|
|
||||||
|
g_assert_cmpint(cmd->number_arg(":bar").value_or(-1), ==, 123);
|
||||||
|
g_assert_cmpint(cmd->number_arg(":bor").value_or(-1), ==, -1);
|
||||||
|
|
||||||
|
assert_equal(cmd->string_arg(":cuux").value_or(""), "456");
|
||||||
|
assert_equal(cmd->string_arg(":caax").value_or(""), ""); // not present
|
||||||
|
assert_equal(cmd->string_arg(":bar").value_or("abc"), "abc"); // wrong type
|
||||||
|
|
||||||
|
g_assert_false(cmd->boolean_arg(":boo"));
|
||||||
|
|
||||||
|
|
||||||
|
g_assert_true(cmd->boolean_arg(":bah"));
|
||||||
|
}
|
||||||
|
|
||||||
|
using CommandInfoMap = CommandHandler::CommandInfoMap;
|
||||||
|
using ArgMap = CommandHandler::ArgMap;
|
||||||
|
using ArgInfo = CommandHandler::ArgInfo;
|
||||||
|
using CommandInfo = CommandHandler::CommandInfo;
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
call(const CommandInfoMap& cmap, const std::string& str) try {
|
||||||
|
|
||||||
|
const auto cmd{Command::make_parse(str)};
|
||||||
|
if (!cmd)
|
||||||
|
throw Error(Error::Code::Internal, "invalid sexp str");
|
||||||
|
|
||||||
|
const auto res{CommandHandler(cmap).invoke(*cmd)};
|
||||||
|
return !!res;
|
||||||
|
|
||||||
|
} catch (const Error& err) {
|
||||||
|
g_warning("%s", err.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_command()
|
||||||
|
{
|
||||||
|
allow_warnings();
|
||||||
|
|
||||||
|
CommandInfoMap cmap;
|
||||||
|
cmap.emplace(
|
||||||
|
"my-command",
|
||||||
|
CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}},
|
||||||
|
{":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}},
|
||||||
|
"My command,",
|
||||||
|
{}});
|
||||||
|
|
||||||
|
g_assert_true(call(cmap, "(my-command :param1 \"hello\")"));
|
||||||
|
g_assert_true(call(cmap, "(my-command :param1 \"hello\" :param2 123)"));
|
||||||
|
|
||||||
|
g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 123 :param3 xxx)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_command2()
|
||||||
|
{
|
||||||
|
allow_warnings();
|
||||||
|
|
||||||
|
CommandInfoMap cmap;
|
||||||
|
cmap.emplace("bla",
|
||||||
|
CommandInfo{ArgMap{
|
||||||
|
{":foo", ArgInfo{Sexp::Type::Number, false, "foo"}},
|
||||||
|
{":bar", ArgInfo{Sexp::Type::String, false, "bar"}},
|
||||||
|
}, "yeah",
|
||||||
|
[&](const auto& params) {}});
|
||||||
|
|
||||||
|
g_assert_true(call(cmap, "(bla :foo nil)"));
|
||||||
|
g_assert_false(call(cmap, "(bla :foo nil :bla nil)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_command_fail()
|
||||||
|
{
|
||||||
|
allow_warnings();
|
||||||
|
|
||||||
|
CommandInfoMap cmap;
|
||||||
|
|
||||||
|
cmap.emplace(
|
||||||
|
"my-command",
|
||||||
|
CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}},
|
||||||
|
{":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}},
|
||||||
|
"My command,",
|
||||||
|
{}});
|
||||||
|
|
||||||
|
g_assert_false(call(cmap, "(my-command)"));
|
||||||
|
g_assert_false(call(cmap, "(my-command2)"));
|
||||||
|
g_assert_false(call(cmap, "(my-command :param1 123 :param2 123)"));
|
||||||
|
g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 \"123\")"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
main(int argc, char* argv[]) try {
|
||||||
|
|
||||||
|
mu_test_init(&argc, &argv);
|
||||||
|
|
||||||
|
g_test_add_func("/utils/command-parser/args", test_args);
|
||||||
|
g_test_add_func("/utils/command-parser/command", test_command);
|
||||||
|
g_test_add_func("/utils/command-parser/command2", test_command2);
|
||||||
|
g_test_add_func("/utils/command-parser/command-fail", test_command_fail);
|
||||||
|
|
||||||
|
return g_test_run();
|
||||||
|
|
||||||
|
} catch (const std::runtime_error& re) {
|
||||||
|
std::cerr << re.what() << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /*BUILD_TESTS*/
|
298
lib/utils/mu-command-handler.hh
Normal file
298
lib/utils/mu-command-handler.hh
Normal file
|
@ -0,0 +1,298 @@
|
||||||
|
/*
|
||||||
|
** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||||
|
**
|
||||||
|
** This program is free software; you can redistribute it and/or modify it
|
||||||
|
** under the terms of the GNU General Public License as published by the
|
||||||
|
** Free Software Foundation; either version 3, or (at your option) any
|
||||||
|
** later version.
|
||||||
|
**
|
||||||
|
** This program is distributed in the hope that it will be useful,
|
||||||
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
** GNU General Public License for more details.
|
||||||
|
**
|
||||||
|
** You should have received a copy of the GNU General Public License
|
||||||
|
** along with this program; if not, write to the Free Software Foundation,
|
||||||
|
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
**
|
||||||
|
*/
|
||||||
|
#ifndef MU_COMMAND_HANDLER_HH__
|
||||||
|
#define MU_COMMAND_HANDLER_HH__
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <ostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <functional>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "utils/mu-error.hh"
|
||||||
|
#include "utils/mu-sexp.hh"
|
||||||
|
#include "utils/mu-option.hh"
|
||||||
|
|
||||||
|
namespace Mu {
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Commands are s-expressions with the follow properties:
|
||||||
|
|
||||||
|
/// 1) a command is a list with a command-name as its first argument
|
||||||
|
/// 2) the rest of the parameters are pairs of colon-prefixed symbol and a value of some
|
||||||
|
/// type (ie. 'keyword arguments')
|
||||||
|
/// 3) each command is described by its CommandInfo structure, which defines the type
|
||||||
|
/// 4) calls to the command must include all required parameters
|
||||||
|
/// 5) all parameters must be of the specified type; however the symbol 'nil' is allowed
|
||||||
|
/// for specify a non-required parameter to be absent; this is for convenience on the
|
||||||
|
/// call side.
|
||||||
|
|
||||||
|
struct Command: public Sexp {
|
||||||
|
|
||||||
|
using iterator = List::iterator;
|
||||||
|
using const_iterator = List::const_iterator;
|
||||||
|
|
||||||
|
static Result<Command> make(Sexp&& sexp) try {
|
||||||
|
return Ok(Command{std::move(sexp)});
|
||||||
|
} catch (const Error& e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Result<Command> make_parse(const std::string& cmdstr) try {
|
||||||
|
if (auto&& sexp{Sexp::parse(cmdstr)}; !sexp)
|
||||||
|
return Err(sexp.error());
|
||||||
|
else
|
||||||
|
return Ok(Command(std::move(*sexp)));
|
||||||
|
} catch (const Error& e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get name of the command (first element) in a command exp
|
||||||
|
*
|
||||||
|
* @return name
|
||||||
|
*/
|
||||||
|
const std::string& name() const {
|
||||||
|
return cbegin()->symbol();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the argument with the given name.
|
||||||
|
*
|
||||||
|
* @param arg name
|
||||||
|
*
|
||||||
|
* @return iterator point at the argument, or cend
|
||||||
|
*/
|
||||||
|
const_iterator find_arg(const std::string& arg) const {
|
||||||
|
return find_prop(arg, cbegin() + 1, cend());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string argument
|
||||||
|
*
|
||||||
|
* @param name of the argument
|
||||||
|
*
|
||||||
|
* @return ref to string, or Nothing if not found
|
||||||
|
*/
|
||||||
|
Option<const std::string&> string_arg(const std::string& name) const {
|
||||||
|
if (auto&& val{arg_val(name, Sexp::Type::String)}; !val)
|
||||||
|
return Nothing;
|
||||||
|
else
|
||||||
|
return val->string();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string-vec argument
|
||||||
|
*
|
||||||
|
* @param name of the argument
|
||||||
|
*
|
||||||
|
* @return ref to string-vec, or Nothing if not found or some error.
|
||||||
|
*/
|
||||||
|
Option<std::vector<std::string>> string_vec_arg(const std::string& name) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a symbol argument
|
||||||
|
*
|
||||||
|
* @param name of the argument
|
||||||
|
*
|
||||||
|
* @return ref to symbol name, or Nothing if not found
|
||||||
|
*/
|
||||||
|
Option<const std::string&> symbol_arg(const std::string& name) const {
|
||||||
|
if (auto&& val{arg_val(name, Sexp::Type::String)}; !val)
|
||||||
|
return Nothing;
|
||||||
|
else
|
||||||
|
return val->symbol();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a number argument
|
||||||
|
*
|
||||||
|
* @param name of the argument
|
||||||
|
*
|
||||||
|
* @return number or Nothing if not found
|
||||||
|
*/
|
||||||
|
Option<int> number_arg(const std::string& name) const {
|
||||||
|
if (auto&& val{arg_val(name, Sexp::Type::Number)}; !val)
|
||||||
|
return Nothing;
|
||||||
|
else
|
||||||
|
return static_cast<int>(val->number());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* helpers
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a boolean argument
|
||||||
|
*
|
||||||
|
* @param name of the argument
|
||||||
|
*
|
||||||
|
* @return true if there's a non-nil symbol value for the given
|
||||||
|
* name; false otherwise.
|
||||||
|
*/
|
||||||
|
Option<bool> bool_arg(const std::string& name) const {
|
||||||
|
if (auto&& symb{symbol_arg(name)}; !symb)
|
||||||
|
return Nothing;
|
||||||
|
else
|
||||||
|
return symb.value() == "nil" ? false : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Treat any argument as a boolean
|
||||||
|
*
|
||||||
|
* @param name name of the argument
|
||||||
|
*
|
||||||
|
* @return false if the the argument is absent or the symbol false;
|
||||||
|
* otherwise true.
|
||||||
|
*/
|
||||||
|
bool boolean_arg(const std::string& name) const {
|
||||||
|
auto&& it{find_arg(name)};
|
||||||
|
return (it == cend() || std::next(it)->nilp()) ? false : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit Command(Sexp&& s){
|
||||||
|
*this = std::move(static_cast<Command&&>(s));
|
||||||
|
if (!listp() || empty() || !cbegin()->symbolp() ||
|
||||||
|
!plistp(cbegin() + 1, cend()))
|
||||||
|
throw Error(Error::Code::Command,
|
||||||
|
"expected command, got '%s'", to_string().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Option<const Sexp&> arg_val(const std::string& name, Sexp::Type type) const {
|
||||||
|
if (auto&& it{find_arg(name)}; it == cend()) {
|
||||||
|
//std::cerr << "--> %s name found " << name << '\n';
|
||||||
|
return Nothing;
|
||||||
|
} else if (auto&& val{it + 1}; val->type() != type) {
|
||||||
|
//std::cerr << "--> type " << Sexp::type_name(it->type()) << '\n';
|
||||||
|
return Nothing;
|
||||||
|
} else
|
||||||
|
return *val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CommandHandler {
|
||||||
|
|
||||||
|
/// Information about a function argument
|
||||||
|
struct ArgInfo {
|
||||||
|
ArgInfo(Sexp::Type typearg, bool requiredarg, std::string&& docarg)
|
||||||
|
: type{typearg}, required{requiredarg}, docstring{std::move(docarg)} {}
|
||||||
|
const Sexp::Type type; /**< Sexp::Type of the argument */
|
||||||
|
const bool required; /**< Is this argument required? */
|
||||||
|
const std::string docstring; /**< Documentation */
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The arguments for a function, which maps their names to the information.
|
||||||
|
using ArgMap = std::unordered_map<std::string, ArgInfo>;
|
||||||
|
|
||||||
|
// A handler function
|
||||||
|
using Handler = std::function<void(const Command&)>;
|
||||||
|
|
||||||
|
/// Information about some command
|
||||||
|
struct CommandInfo {
|
||||||
|
CommandInfo(ArgMap&& argmaparg, std::string&& docarg, Handler&& handlerarg)
|
||||||
|
: args{std::move(argmaparg)}, docstring{std::move(docarg)},
|
||||||
|
handler{std::move(handlerarg)} {}
|
||||||
|
const ArgMap args;
|
||||||
|
const std::string docstring;
|
||||||
|
const Handler handler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a sorted list of argument names, for display. Required args come
|
||||||
|
* first, then alphabetical.
|
||||||
|
*
|
||||||
|
* @return vec with the sorted names.
|
||||||
|
*/
|
||||||
|
std::vector<std::string> sorted_argnames() const {
|
||||||
|
// sort args -- by required, then alphabetical.
|
||||||
|
std::vector<std::string> names;
|
||||||
|
for (auto&& arg : args)
|
||||||
|
names.emplace_back(arg.first);
|
||||||
|
std::sort(names.begin(), names.end(), [&](const auto& name1, const auto& name2) {
|
||||||
|
const auto& arg1{args.find(name1)->second};
|
||||||
|
const auto& arg2{args.find(name2)->second};
|
||||||
|
if (arg1.required != arg2.required)
|
||||||
|
return arg1.required;
|
||||||
|
else
|
||||||
|
return name1 < name2;
|
||||||
|
});
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/// All commands, mapping their name to information about them.
|
||||||
|
using CommandInfoMap = std::unordered_map<std::string, CommandInfo>;
|
||||||
|
|
||||||
|
CommandHandler(const CommandInfoMap& cmap): cmap_{cmap} {}
|
||||||
|
CommandHandler(CommandInfoMap&& cmap): cmap_{std::move(cmap)} {}
|
||||||
|
|
||||||
|
const CommandInfoMap& info_map() const { return cmap_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoke some command
|
||||||
|
*
|
||||||
|
* A command uses keyword arguments, e.g. something like: (foo :bar 1
|
||||||
|
* :cuux "fnorb")
|
||||||
|
*
|
||||||
|
* @param cmd a Sexp describing a command call
|
||||||
|
* @param validate whether to validate before invoking. Useful during
|
||||||
|
* development.
|
||||||
|
*
|
||||||
|
* Return Ok() or some Error
|
||||||
|
*/
|
||||||
|
Result<void> invoke(const Command& cmd, bool validate=true) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const CommandInfoMap cmap_;
|
||||||
|
};
|
||||||
|
|
||||||
|
static inline std::ostream&
|
||||||
|
operator<<(std::ostream& os, const CommandHandler::ArgInfo& info)
|
||||||
|
{
|
||||||
|
os << info.type << " (" << (info.required ? "required" : "optional") << ")";
|
||||||
|
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline std::ostream&
|
||||||
|
operator<<(std::ostream& os, const CommandHandler::CommandInfo& info)
|
||||||
|
{
|
||||||
|
for (auto&& arg : info.args)
|
||||||
|
os << " " << arg.first << " " << arg.second << '\n'
|
||||||
|
<< " " << arg.second.docstring << "\n";
|
||||||
|
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline std::ostream&
|
||||||
|
operator<<(std::ostream& os, const CommandHandler::CommandInfoMap& map)
|
||||||
|
{
|
||||||
|
for (auto&& c : map)
|
||||||
|
os << c.first << '\n' << c.second;
|
||||||
|
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Mu
|
||||||
|
|
||||||
|
#endif /* MU_COMMAND_HANDLER_HH__ */
|
|
@ -1,204 +0,0 @@
|
||||||
/*
|
|
||||||
** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
|
||||||
**
|
|
||||||
** This program is free software; you can redistribute it and/or modify it
|
|
||||||
** under the terms of the GNU General Public License as published by the
|
|
||||||
** Free Software Foundation; either version 3, or (at your option) any
|
|
||||||
** later version.
|
|
||||||
**
|
|
||||||
** This program is distributed in the hope that it will be useful,
|
|
||||||
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
** GNU General Public License for more details.
|
|
||||||
**
|
|
||||||
** You should have received a copy of the GNU General Public License
|
|
||||||
** along with this program; if not, write to the Free Software Foundation,
|
|
||||||
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
**
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "mu-command-parser.hh"
|
|
||||||
#include "mu-error.hh"
|
|
||||||
#include "mu-utils.hh"
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
using namespace Mu;
|
|
||||||
using namespace Command;
|
|
||||||
|
|
||||||
void
|
|
||||||
Command::invoke(const Command::CommandMap& cmap, const Sexp& call)
|
|
||||||
{
|
|
||||||
if (!call.is_call()) {
|
|
||||||
throw Mu::Error{Error::Code::Command,
|
|
||||||
"expected call-sexpr but got %s",
|
|
||||||
call.to_sexp_string().c_str()};
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto& params{call.list()};
|
|
||||||
const auto cmd_it = cmap.find(params.at(0).value());
|
|
||||||
if (cmd_it == cmap.end())
|
|
||||||
throw Mu::Error{Error::Code::Command,
|
|
||||||
"unknown command in call %s",
|
|
||||||
call.to_sexp_string().c_str()};
|
|
||||||
|
|
||||||
const auto& cinfo{cmd_it->second};
|
|
||||||
|
|
||||||
// all required parameters must be present
|
|
||||||
for (auto&& arg : cinfo.args) {
|
|
||||||
const auto& argname{arg.first};
|
|
||||||
const auto& arginfo{arg.second};
|
|
||||||
|
|
||||||
// calls used keyword-parameters, e.g.
|
|
||||||
// (my-function :bar 1 :cuux "fnorb")
|
|
||||||
// so, we're looking for the odd-numbered parameters.
|
|
||||||
const auto param_it = [&]() -> Sexp::Seq::const_iterator {
|
|
||||||
for (size_t i = 1; i < params.size(); i += 2)
|
|
||||||
if (params.at(i).is_symbol() && params.at(i).value() == argname)
|
|
||||||
return params.begin() + i + 1;
|
|
||||||
|
|
||||||
return params.end();
|
|
||||||
}();
|
|
||||||
|
|
||||||
// it's an error when a required parameter is missing.
|
|
||||||
if (param_it == params.end()) {
|
|
||||||
if (arginfo.required)
|
|
||||||
throw Mu::Error{Error::Code::Command,
|
|
||||||
"missing required parameter %s in call %s",
|
|
||||||
argname.c_str(),
|
|
||||||
call.to_sexp_string().c_str()};
|
|
||||||
continue; // not required
|
|
||||||
}
|
|
||||||
|
|
||||||
// the types must match, but the 'nil' symbol is acceptable as
|
|
||||||
// "no value"
|
|
||||||
if (param_it->type() != arginfo.type && !(param_it->is_nil()))
|
|
||||||
throw Mu::Error{Error::Code::Command,
|
|
||||||
"parameter %s expects type %s, but got %s in call %s",
|
|
||||||
argname.c_str(),
|
|
||||||
to_string(arginfo.type).c_str(),
|
|
||||||
to_string(param_it->type()).c_str(),
|
|
||||||
call.to_sexp_string().c_str()};
|
|
||||||
}
|
|
||||||
|
|
||||||
// all passed parameters must be known
|
|
||||||
for (size_t i = 1; i < params.size(); i += 2) {
|
|
||||||
if (std::none_of(cinfo.args.begin(), cinfo.args.end(), [&](auto&& arg) {
|
|
||||||
return params.at(i).value() == arg.first;
|
|
||||||
}))
|
|
||||||
throw Mu::Error{Error::Code::Command,
|
|
||||||
"unknown parameter %s in call %s",
|
|
||||||
params.at(i).value().c_str(),
|
|
||||||
call.to_sexp_string().c_str()};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cinfo.handler)
|
|
||||||
cinfo.handler(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Sexp::Seq::const_iterator
|
|
||||||
find_param_node(const Parameters& params, const std::string& argname)
|
|
||||||
{
|
|
||||||
if (params.empty())
|
|
||||||
throw Error(Error::Code::InvalidArgument, "params must not be empty");
|
|
||||||
|
|
||||||
if (argname.empty() || argname.at(0) != ':')
|
|
||||||
throw Error(Error::Code::InvalidArgument,
|
|
||||||
"property key must start with ':' but got '%s')",
|
|
||||||
argname.c_str());
|
|
||||||
|
|
||||||
for (size_t i = 1; i < params.size(); i += 2) {
|
|
||||||
if (i + 1 != params.size() && params.at(i).is_symbol() &&
|
|
||||||
params.at(i).value() == argname)
|
|
||||||
return params.begin() + i + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return params.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
static Error
|
|
||||||
wrong_type(Sexp::Type expected, Sexp::Type got)
|
|
||||||
{
|
|
||||||
return Error(Error::Code::InvalidArgument,
|
|
||||||
"expected <%s> but got <%s>",
|
|
||||||
to_string(expected).c_str(),
|
|
||||||
to_string(got).c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
Option<std::string>
|
|
||||||
Command::get_string(const Parameters& params, const std::string& argname)
|
|
||||||
{
|
|
||||||
const auto it = find_param_node(params, argname);
|
|
||||||
if (it == params.end() || it->is_nil())
|
|
||||||
return Nothing;
|
|
||||||
else if (!it->is_string())
|
|
||||||
throw wrong_type(Sexp::Type::String, it->type());
|
|
||||||
else
|
|
||||||
return it->value();
|
|
||||||
}
|
|
||||||
|
|
||||||
Option<std::string>
|
|
||||||
Command::get_symbol(const Parameters& params, const std::string& argname)
|
|
||||||
{
|
|
||||||
const auto it = find_param_node(params, argname);
|
|
||||||
if (it == params.end() || it->is_nil())
|
|
||||||
return Nothing;
|
|
||||||
else if (!it->is_symbol())
|
|
||||||
throw wrong_type(Sexp::Type::Symbol, it->type());
|
|
||||||
else
|
|
||||||
return it->value();
|
|
||||||
}
|
|
||||||
|
|
||||||
Option<int>
|
|
||||||
Command::get_int(const Parameters& params, const std::string& argname)
|
|
||||||
{
|
|
||||||
const auto it = find_param_node(params, argname);
|
|
||||||
if (it == params.end() || it->is_nil())
|
|
||||||
return Nothing;
|
|
||||||
else if (!it->is_number())
|
|
||||||
throw wrong_type(Sexp::Type::Number, it->type());
|
|
||||||
else
|
|
||||||
return ::atoi(it->value().c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
Option<unsigned>
|
|
||||||
Command::get_unsigned(const Parameters& params, const std::string& argname)
|
|
||||||
{
|
|
||||||
if (auto val = get_int(params, argname); val && *val >= 0)
|
|
||||||
return val;
|
|
||||||
else
|
|
||||||
return Nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Option<bool>
|
|
||||||
Command::get_bool(const Parameters& params, const std::string& argname)
|
|
||||||
{
|
|
||||||
const auto it = find_param_node(params, argname);
|
|
||||||
if (it == params.end())
|
|
||||||
return Nothing;
|
|
||||||
else if (!it->is_symbol())
|
|
||||||
throw wrong_type(Sexp::Type::Symbol, it->type());
|
|
||||||
else
|
|
||||||
return it->is_nil() ? false : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string>
|
|
||||||
Command::get_string_vec(const Parameters& params, const std::string& argname)
|
|
||||||
{
|
|
||||||
const auto it = find_param_node(params, argname);
|
|
||||||
if (it == params.end() || it->is_nil())
|
|
||||||
return {};
|
|
||||||
else if (!it->is_list())
|
|
||||||
throw wrong_type(Sexp::Type::List, it->type());
|
|
||||||
|
|
||||||
std::vector<std::string> vec;
|
|
||||||
for (const auto& n : it->list()) {
|
|
||||||
if (!n.is_string())
|
|
||||||
throw wrong_type(Sexp::Type::String, n.type());
|
|
||||||
vec.emplace_back(n.value());
|
|
||||||
}
|
|
||||||
|
|
||||||
return vec;
|
|
||||||
}
|
|
|
@ -1,180 +0,0 @@
|
||||||
/*
|
|
||||||
** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
|
||||||
**
|
|
||||||
** This program is free software; you can redistribute it and/or modify it
|
|
||||||
** under the terms of the GNU General Public License as published by the
|
|
||||||
** Free Software Foundation; either version 3, or (at your option) any
|
|
||||||
** later version.
|
|
||||||
**
|
|
||||||
** This program is distributed in the hope that it will be useful,
|
|
||||||
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
** GNU General Public License for more details.
|
|
||||||
**
|
|
||||||
** You should have received a copy of the GNU General Public License
|
|
||||||
** along with this program; if not, write to the Free Software Foundation,
|
|
||||||
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
**
|
|
||||||
*/
|
|
||||||
#ifndef MU_COMMAND_PARSER_HH__
|
|
||||||
#define MU_COMMAND_PARSER_HH__
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
#include <string>
|
|
||||||
#include <ostream>
|
|
||||||
#include <stdexcept>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <functional>
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
#include "utils/mu-error.hh"
|
|
||||||
#include "utils/mu-sexp.hh"
|
|
||||||
#include "utils/mu-option.hh"
|
|
||||||
|
|
||||||
namespace Mu {
|
|
||||||
namespace Command {
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Commands are s-expressions with the follow properties:
|
|
||||||
|
|
||||||
/// 1) a command is a list with a command-name as its first argument
|
|
||||||
/// 2) the rest of the parameters are pairs of colon-prefixed symbol and a value of some
|
|
||||||
/// type (ie. 'keyword arguments')
|
|
||||||
/// 3) each command is described by its CommandInfo structure, which defines the type
|
|
||||||
/// 4) calls to the command must include all required parameters
|
|
||||||
/// 5) all parameters must be of the specified type; however the symbol 'nil' is allowed
|
|
||||||
/// for specify a non-required parameter to be absent; this is for convenience on the
|
|
||||||
/// call side.
|
|
||||||
|
|
||||||
/// Information about a function argument
|
|
||||||
struct ArgInfo {
|
|
||||||
ArgInfo(Sexp::Type typearg, bool requiredarg, std::string&& docarg)
|
|
||||||
: type{typearg}, required{requiredarg}, docstring{std::move(docarg)}
|
|
||||||
{
|
|
||||||
}
|
|
||||||
const Sexp::Type type; /**< Sexp::Type of the argument */
|
|
||||||
const bool required; /**< Is this argument required? */
|
|
||||||
const std::string docstring; /**< Documentation */
|
|
||||||
};
|
|
||||||
|
|
||||||
/// The arguments for a function, which maps their names to the information.
|
|
||||||
using ArgMap = std::unordered_map<std::string, ArgInfo>;
|
|
||||||
// The parameters to a Handler.
|
|
||||||
using Parameters = Sexp::Seq;
|
|
||||||
|
|
||||||
Option<int> get_int(const Parameters& parms, const std::string& argname);
|
|
||||||
Option<unsigned> get_unsigned(const Parameters& parms, const std::string& argname);
|
|
||||||
Option<bool> get_bool(const Parameters& parms, const std::string& argname);
|
|
||||||
Option<std::string> get_string(const Parameters& parms, const std::string& argname);
|
|
||||||
Option<std::string> get_symbol(const Parameters& parms, const std::string& argname);
|
|
||||||
|
|
||||||
std::vector<std::string> get_string_vec(const Parameters& params, const std::string& argname);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* backward compat
|
|
||||||
*/
|
|
||||||
static inline int
|
|
||||||
get_int_or(const Parameters& parms, const std::string& arg, int alt = 0) {
|
|
||||||
return get_int(parms, arg).value_or(alt);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline bool
|
|
||||||
get_bool_or(const Parameters& parms, const std::string& arg, bool alt = false) {
|
|
||||||
return get_bool(parms, arg).value_or(alt);
|
|
||||||
}
|
|
||||||
static inline std::string
|
|
||||||
get_string_or(const Parameters& parms, const std::string& arg, const std::string& alt = ""){
|
|
||||||
return get_string(parms, arg).value_or(alt);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline std::string
|
|
||||||
get_symbol_or(const Parameters& parms, const std::string& arg, const std::string& alt = "nil") {
|
|
||||||
return get_symbol(parms, arg).value_or(alt);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// A handler function
|
|
||||||
using Handler = std::function<void(const Parameters&)>;
|
|
||||||
|
|
||||||
/// Information about some command
|
|
||||||
struct CommandInfo {
|
|
||||||
CommandInfo(ArgMap&& argmaparg, std::string&& docarg, Handler&& handlerarg)
|
|
||||||
: args{std::move(argmaparg)}, docstring{std::move(docarg)}, handler{
|
|
||||||
std::move(handlerarg)}
|
|
||||||
{
|
|
||||||
}
|
|
||||||
const ArgMap args;
|
|
||||||
const std::string docstring;
|
|
||||||
const Handler handler;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a sorted list of argument names, for display. Required args come
|
|
||||||
* first, then alphabetical.
|
|
||||||
*
|
|
||||||
* @return vec with the sorted names.
|
|
||||||
*/
|
|
||||||
std::vector<std::string> sorted_argnames() const
|
|
||||||
{ // sort args -- by required, then alphabetical.
|
|
||||||
std::vector<std::string> names;
|
|
||||||
for (auto&& arg : args)
|
|
||||||
names.emplace_back(arg.first);
|
|
||||||
std::sort(names.begin(), names.end(), [&](const auto& name1, const auto& name2) {
|
|
||||||
const auto& arg1{args.find(name1)->second};
|
|
||||||
const auto& arg2{args.find(name2)->second};
|
|
||||||
if (arg1.required != arg2.required)
|
|
||||||
return arg1.required;
|
|
||||||
else
|
|
||||||
return name1 < name2;
|
|
||||||
});
|
|
||||||
return names;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/// All commands, mapping their name to information about them.
|
|
||||||
using CommandMap = std::unordered_map<std::string, CommandInfo>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate that the call (a Sexp) specifies a valid call, then invoke it.
|
|
||||||
*
|
|
||||||
* A call uses keyword arguments, e.g. something like:
|
|
||||||
* (foo :bar 1 :cuux "fnorb")
|
|
||||||
*
|
|
||||||
* On error, throw Error.
|
|
||||||
*
|
|
||||||
* @param cmap map of commands
|
|
||||||
* @param call node describing a call.
|
|
||||||
*/
|
|
||||||
void invoke(const Command::CommandMap& cmap, const Sexp& call);
|
|
||||||
|
|
||||||
static inline std::ostream&
|
|
||||||
operator<<(std::ostream& os, const Command::ArgInfo& info)
|
|
||||||
{
|
|
||||||
os << info.type << " (" << (info.required ? "required" : "optional") << ")";
|
|
||||||
|
|
||||||
return os;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline std::ostream&
|
|
||||||
operator<<(std::ostream& os, const Command::CommandInfo& info)
|
|
||||||
{
|
|
||||||
for (auto&& arg : info.args)
|
|
||||||
os << " " << arg.first << " " << arg.second << '\n'
|
|
||||||
<< " " << arg.second.docstring << "\n";
|
|
||||||
|
|
||||||
return os;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline std::ostream&
|
|
||||||
operator<<(std::ostream& os, const Command::CommandMap& map)
|
|
||||||
{
|
|
||||||
for (auto&& c : map)
|
|
||||||
os << c.first << '\n' << c.second;
|
|
||||||
|
|
||||||
return os;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Command
|
|
||||||
} // namespace Mu
|
|
||||||
|
|
||||||
#endif /* MU_COMMAND_PARSER_HH__ */
|
|
|
@ -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
|
** 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
|
** under the terms of the GNU General Public License as published by the
|
||||||
|
@ -18,9 +17,11 @@
|
||||||
**
|
**
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
#include "mu-sexp.hh"
|
#include "mu-sexp.hh"
|
||||||
#include "mu-utils.hh"
|
#include "mu-utils.hh"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
|
||||||
|
@ -48,36 +49,38 @@ skip_whitespace(const std::string& s, size_t pos)
|
||||||
else
|
else
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return pos;
|
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)
|
parse_list(const std::string& expr, size_t& pos)
|
||||||
{
|
{
|
||||||
if (expr[pos] != '(') // sanity check.
|
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;
|
++pos;
|
||||||
while (expr[pos] != ')' && pos != expr.size())
|
while (expr[pos] != ')' && pos != expr.size()) {
|
||||||
list.add(parse(expr, pos));
|
if (auto&& item = parse(expr, pos); item)
|
||||||
|
lst.add(std::move(*item));
|
||||||
|
else
|
||||||
|
return Err(item.error());
|
||||||
|
}
|
||||||
|
|
||||||
if (expr[pos] != ')')
|
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;
|
++pos;
|
||||||
return Sexp::make_list(std::move(list));
|
return Ok(std::move(lst));
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse string
|
static Result<Sexp>
|
||||||
static Sexp
|
|
||||||
parse_string(const std::string& expr, size_t& pos)
|
parse_string(const std::string& expr, size_t& pos)
|
||||||
{
|
{
|
||||||
if (expr[pos] != '"') // sanity check.
|
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{};
|
bool escape{};
|
||||||
std::string str;
|
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());
|
throw parsing_error(pos, "unterminated string '%s'", str.c_str());
|
||||||
|
|
||||||
++pos;
|
++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)
|
parse_integer(const std::string& expr, size_t& pos)
|
||||||
{
|
{
|
||||||
if (!isdigit(expr[pos]) && expr[pos] != '-') // sanity check.
|
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?
|
std::string num; // negative number?
|
||||||
if (expr[pos] == '-') {
|
if (expr[pos] == '-') {
|
||||||
|
@ -119,32 +123,32 @@ parse_integer(const std::string& expr, size_t& pos)
|
||||||
for (; isdigit(expr[pos]); ++pos)
|
for (; isdigit(expr[pos]); ++pos)
|
||||||
num += expr[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)
|
parse_symbol(const std::string& expr, size_t& pos)
|
||||||
{
|
{
|
||||||
if (!isalpha(expr[pos]) && expr[pos] != ':') // sanity check.
|
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)
|
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)
|
parse(const std::string& expr, size_t& pos)
|
||||||
{
|
{
|
||||||
pos = skip_whitespace(expr, pos);
|
pos = skip_whitespace(expr, pos);
|
||||||
|
|
||||||
if (pos == expr.size())
|
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 kar = expr[pos];
|
||||||
const auto node = [&]() -> Sexp {
|
const auto sexp = std::invoke([&]() -> Result<Sexp> {
|
||||||
if (kar == '(')
|
if (kar == '(')
|
||||||
return parse_list(expr, pos);
|
return parse_list(expr, pos);
|
||||||
else if (kar == '"')
|
else if (kar == '"')
|
||||||
|
@ -155,55 +159,52 @@ parse(const std::string& expr, size_t& pos)
|
||||||
return parse_symbol(expr, pos);
|
return parse_symbol(expr, pos);
|
||||||
else
|
else
|
||||||
throw parsing_error(pos, "unexpected character '%c", kar);
|
throw parsing_error(pos, "unexpected character '%c", kar);
|
||||||
}();
|
});
|
||||||
|
|
||||||
pos = skip_whitespace(expr, pos);
|
pos = skip_whitespace(expr, pos);
|
||||||
|
|
||||||
return node;
|
return sexp;
|
||||||
}
|
}
|
||||||
|
|
||||||
Sexp
|
Result<Sexp>
|
||||||
Sexp::make_parse(const std::string& expr)
|
Sexp::parse(const std::string& expr)
|
||||||
{
|
{
|
||||||
size_t pos{};
|
size_t pos{};
|
||||||
auto node{::parse(expr, pos)};
|
auto res = ::parse(expr, pos);
|
||||||
|
if (!res)
|
||||||
if (pos != expr.size())
|
return res;
|
||||||
throw parsing_error(pos, "trailing data starting with '%c'", expr[pos]);
|
else if (pos != expr.size())
|
||||||
|
return Err(parsing_error(pos, "trailing data starting with '%c'", expr[pos]));
|
||||||
return node;
|
else
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string
|
std::string
|
||||||
Sexp::to_sexp_string() const
|
Sexp::to_string(Format fopts) const
|
||||||
{
|
{
|
||||||
std::stringstream sstrm;
|
std::stringstream sstrm;
|
||||||
|
const auto splitp{any_of(fopts & Format::SplitList)};
|
||||||
|
const auto typeinfop{any_of(fopts & Format::TypeInfo)};
|
||||||
|
|
||||||
switch (type()) {
|
if (listp()) {
|
||||||
case Type::List: {
|
|
||||||
sstrm << '(';
|
sstrm << '(';
|
||||||
bool first{true};
|
bool first{true};
|
||||||
for (auto&& child : list()) {
|
for(auto&& elm: list()) {
|
||||||
sstrm << (first ? "" : " ") << child.to_sexp_string();
|
sstrm << (first ? "" : " ") << elm.to_string(fopts);
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
sstrm << ')';
|
sstrm << ')';
|
||||||
|
if (splitp)
|
||||||
if (any_of(formatting_opts & FormattingOptions::SplitList))
|
|
||||||
sstrm << '\n';
|
sstrm << '\n';
|
||||||
break;
|
} else if (stringp())
|
||||||
}
|
sstrm << quote(string());
|
||||||
case Type::String:
|
else if (numberp())
|
||||||
sstrm << quote(value());
|
sstrm << number();
|
||||||
break;
|
else if (symbolp())
|
||||||
case Type::Raw:
|
sstrm << symbol();
|
||||||
sstrm << value();
|
|
||||||
break;
|
if (typeinfop)
|
||||||
case Type::Number:
|
sstrm << '<' << Sexp::type_name(type()) << '>';
|
||||||
case Type::Symbol:
|
|
||||||
case Type::Empty:
|
|
||||||
default: sstrm << value();
|
|
||||||
}
|
|
||||||
|
|
||||||
return sstrm.str();
|
return sstrm.str();
|
||||||
}
|
}
|
||||||
|
@ -211,26 +212,26 @@ Sexp::to_sexp_string() const
|
||||||
// LCOV_EXCL_START
|
// LCOV_EXCL_START
|
||||||
|
|
||||||
std::string
|
std::string
|
||||||
Sexp::to_json_string() const
|
Sexp::to_json_string(Format fopts) const
|
||||||
{
|
{
|
||||||
std::stringstream sstrm;
|
std::stringstream sstrm;
|
||||||
|
|
||||||
switch (type()) {
|
switch (type()) {
|
||||||
case Type::List: {
|
case Type::List: {
|
||||||
// property-lists become JSON objects
|
// property-lists become JSON objects
|
||||||
if (is_prop_list()) {
|
if (plistp()) {
|
||||||
sstrm << "{";
|
sstrm << "{";
|
||||||
auto it{list().begin()};
|
auto it{list().begin()};
|
||||||
bool first{true};
|
bool first{true};
|
||||||
while (it != list().end()) {
|
while (it != list().end()) {
|
||||||
sstrm << (first ? "" : ",") << quote(it->value()) << ":";
|
sstrm << (first ? "" : ",") << quote(it->string()) << ":";
|
||||||
++it;
|
++it;
|
||||||
sstrm << it->to_json_string();
|
sstrm << it->to_json_string();
|
||||||
++it;
|
++it;
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
sstrm << "}";
|
sstrm << "}";
|
||||||
if (any_of(formatting_opts & FormattingOptions::SplitList))
|
if (any_of(fopts & Format::SplitList))
|
||||||
sstrm << '\n';
|
sstrm << '\n';
|
||||||
} else { // other lists become arrays.
|
} else { // other lists become arrays.
|
||||||
sstrm << '[';
|
sstrm << '[';
|
||||||
|
@ -240,31 +241,254 @@ Sexp::to_json_string() const
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
sstrm << ']';
|
sstrm << ']';
|
||||||
if (any_of(formatting_opts & FormattingOptions::SplitList))
|
if (any_of(fopts & Format::SplitList))
|
||||||
sstrm << '\n';
|
sstrm << '\n';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Type::String:
|
case Type::String:
|
||||||
sstrm << quote(value());
|
sstrm << quote(string());
|
||||||
break;
|
break;
|
||||||
case Type::Raw: // FIXME: implement this.
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Type::Symbol:
|
case Type::Symbol:
|
||||||
if (is_nil())
|
if (nilp())
|
||||||
sstrm << "false";
|
sstrm << "false";
|
||||||
else if (is_t())
|
else if (symbol() == "t")
|
||||||
sstrm << "true";
|
sstrm << "true";
|
||||||
else
|
else
|
||||||
sstrm << quote(value());
|
sstrm << quote(symbol());
|
||||||
break;
|
break;
|
||||||
case Type::Number:
|
case Type::Number:
|
||||||
case Type::Empty:
|
sstrm << number();
|
||||||
default: sstrm << value();
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sstrm.str();
|
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
|
// 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*/
|
||||||
|
|
|
@ -20,428 +20,255 @@
|
||||||
#ifndef MU_SEXP_HH__
|
#ifndef MU_SEXP_HH__
|
||||||
#define MU_SEXP_HH__
|
#define MU_SEXP_HH__
|
||||||
|
|
||||||
#include <string>
|
#include "mu-utils.hh"
|
||||||
#include <vector>
|
|
||||||
#include <type_traits>
|
|
||||||
|
|
||||||
#include "utils/mu-utils.hh"
|
#include <stdexcept>
|
||||||
#include "utils/mu-error.hh"
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <iostream>
|
||||||
|
#include <variant>
|
||||||
|
#include <cinttypes>
|
||||||
|
#include <ostream>
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
#include <utils/mu-result.hh>
|
||||||
|
|
||||||
namespace Mu {
|
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 {
|
struct Sexp {
|
||||||
/// Node type
|
/// Types
|
||||||
enum struct Type { Empty, List, String, Number, Symbol, Raw };
|
using List = std::vector<Sexp>;
|
||||||
|
using String = std::string;
|
||||||
/**
|
using Number = int64_t;
|
||||||
* Default CTOR
|
struct Symbol { // distinguish from String.
|
||||||
*/
|
Symbol(const std::string& s): name{s} {}
|
||||||
Sexp() : type_{Type::Empty} {}
|
Symbol(std::string&& s): name(std::move(s)) {}
|
||||||
|
Symbol(const char* str): Symbol(std::string{str}) {}
|
||||||
// Underlying data type for list; we'd like to use std::dequeu here,
|
Symbol(std::string_view sv): Symbol(std::string{sv}) {}
|
||||||
// but that does not compile with libc++ (it does with libstdc++)
|
operator const std::string&() const {return name; }
|
||||||
using Seq = std::vector<Sexp>;
|
std::string name;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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_;
|
|
||||||
};
|
};
|
||||||
|
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
|
* @return type
|
||||||
* @param sexp a Sexp
|
|
||||||
* @param args rest arguments
|
|
||||||
*
|
|
||||||
* @return a sexp.
|
|
||||||
*/
|
*/
|
||||||
static Sexp make_list(List&& list) { return Sexp{Type::List, std::move(list.seq_)}; }
|
constexpr Type type() const { return static_cast<Type>(data.index()); }
|
||||||
template <typename... Args> static Sexp make_list(Sexp&& sexp, Args... args)
|
|
||||||
{
|
|
||||||
List lst;
|
/**
|
||||||
lst.add(std::move(sexp)).add(std::forward<Args>(args)...);
|
* Get the name for some type
|
||||||
return make_list(std::move(lst));
|
*
|
||||||
|
* @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 str a string
|
||||||
* @param sexp a Sexp
|
|
||||||
* @param args rest arguments (property list)
|
|
||||||
*
|
*
|
||||||
* @return a sexp.
|
* @return either an Sexp or an error
|
||||||
*/
|
*/
|
||||||
template <typename... Args>
|
static Result<Sexp> parse(const std::string& str);
|
||||||
static Sexp make_prop_list(std::string&& name, Sexp&& sexp, Args... args)
|
|
||||||
{
|
|
||||||
List list;
|
/// List specific
|
||||||
list.add_prop(std::move(name), std::move(sexp), std::forward<Args>(args)...);
|
using iterator = List::iterator;
|
||||||
return make_list(std::move(list));
|
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 p property name
|
||||||
* @param name the property name; must start wtth ':'
|
|
||||||
* @param sexp a Sexp
|
|
||||||
* @param args rest arguments (property list)
|
|
||||||
*
|
*
|
||||||
* @return a sexp.
|
* @return the property if found, or the symbol nil otherwise.
|
||||||
*/
|
*/
|
||||||
template <typename... Args>
|
const Sexp& get_prop(const std::string& p) const {
|
||||||
static Sexp make_call(std::string&& funcname, std::string&& name, Sexp&& sexp, Args... args)
|
if (auto&& it = find_prop(p, cbegin(), cend()); it != cend())
|
||||||
{
|
return *(std::next(it));
|
||||||
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;
|
|
||||||
else
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Output to string
|
||||||
* Is this a call? A call is a list sexp with a symbol (function name),
|
enum struct Format {
|
||||||
* 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 {
|
|
||||||
Default = 0, /**< Nothing in particular */
|
Default = 0, /**< Nothing in particular */
|
||||||
SplitList = 1 << 0, /**< Insert newline after list item */
|
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 str
|
||||||
*
|
|
||||||
* @return true or false.
|
|
||||||
*/
|
*/
|
||||||
static bool is_prop_name(const std::string& str)
|
std::string to_string(Format fopts=Format::Default) const;
|
||||||
{
|
std::string to_json_string(Format fopts=Format::Default) const;
|
||||||
return str.size() > 1 && str.at(0) == ':';
|
|
||||||
}
|
|
||||||
static bool is_prop_name(const Sexp& sexp)
|
|
||||||
{
|
|
||||||
return sexp.is_symbol() && is_prop_name(sexp.value());
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool is_prop_list(Seq::const_iterator b, Seq::const_iterator e)
|
Sexp& del_prop(const std::string& pname);
|
||||||
{
|
protected:
|
||||||
while (b != e) {
|
const_iterator find_prop(const std::string& s, const_iterator b,
|
||||||
const Sexp& s{*b};
|
const_iterator e) const;
|
||||||
if (!is_prop_name(s))
|
bool plistp(const_iterator b, const_iterator e) const;
|
||||||
return false;
|
private:
|
||||||
if (++b == e)
|
iterator find_prop(const std::string& s,iterator b,
|
||||||
return false;
|
iterator e);
|
||||||
++b;
|
Data data;
|
||||||
}
|
|
||||||
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) */
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static inline std::ostream&
|
MU_ENABLE_BITOPS(Sexp::Format);
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
return os;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static inline std::ostream&
|
static inline std::ostream&
|
||||||
operator<<(std::ostream& os, const Sexp& sexp)
|
operator<<(std::ostream& os, const Sexp& sexp)
|
||||||
{
|
{
|
||||||
os << sexp.to_sexp_string();
|
os << sexp.to_string();
|
||||||
return os;
|
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
|
} // namespace Mu
|
||||||
|
|
||||||
#endif /* MU_SEXP_HH__ */
|
#endif /* MU_SEXP_HH__ */
|
||||||
|
|
|
@ -18,11 +18,6 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
# tests
|
# tests
|
||||||
#
|
#
|
||||||
test('test-command-parser',
|
|
||||||
executable('test-command-parser',
|
|
||||||
'test-command-parser.cc',
|
|
||||||
install: false,
|
|
||||||
dependencies: [glib_dep, lib_mu_utils_dep]))
|
|
||||||
test('test-mu-util',
|
test('test-mu-util',
|
||||||
executable('test-mu-util',
|
executable('test-mu-util',
|
||||||
'test-mu-util.c',
|
'test-mu-util.c',
|
||||||
|
@ -38,8 +33,3 @@ test('test-mu-utils',
|
||||||
'test-utils.cc',
|
'test-utils.cc',
|
||||||
install: false,
|
install: false,
|
||||||
dependencies: [glib_dep, lib_mu_utils_dep]))
|
dependencies: [glib_dep, lib_mu_utils_dep]))
|
||||||
test('test-sexp',
|
|
||||||
executable('test-sexp',
|
|
||||||
'test-sexp.cc',
|
|
||||||
install: false,
|
|
||||||
dependencies: [glib_dep, lib_mu_utils_dep] ))
|
|
||||||
|
|
|
@ -1,149 +0,0 @@
|
||||||
/*
|
|
||||||
** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
|
||||||
**
|
|
||||||
** This library is free software; you can redistribute it and/or
|
|
||||||
** modify it under the terms of the GNU Lesser General Public License
|
|
||||||
** as published by the Free Software Foundation; either version 2.1
|
|
||||||
** of the License, or (at your option) any later version.
|
|
||||||
**
|
|
||||||
** This library is distributed in the hope that it will be useful,
|
|
||||||
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
** Lesser General Public License for more details.
|
|
||||||
**
|
|
||||||
** You should have received a copy of the GNU Lesser General Public
|
|
||||||
** License along with this library; if not, write to the Free
|
|
||||||
** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
|
|
||||||
** 02110-1301, USA.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
#include <glib.h>
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
#include "mu-command-parser.hh"
|
|
||||||
#include "mu-utils.hh"
|
|
||||||
#include "mu-test-utils.hh"
|
|
||||||
|
|
||||||
using namespace Mu;
|
|
||||||
|
|
||||||
static void
|
|
||||||
test_param_getters()
|
|
||||||
{
|
|
||||||
const auto sexp{Sexp::make_parse(R"((foo :bar 123 :cuux "456" :boo nil :bah true))")};
|
|
||||||
|
|
||||||
if (g_test_verbose())
|
|
||||||
std::cout << sexp << "\n";
|
|
||||||
|
|
||||||
g_assert_cmpint(Command::get_int_or(sexp.list(), ":bar"), ==, 123);
|
|
||||||
assert_equal(Command::get_string_or(sexp.list(), ":bra", "bla"), "bla");
|
|
||||||
assert_equal(Command::get_string_or(sexp.list(), ":cuux"), "456");
|
|
||||||
|
|
||||||
g_assert_true(Command::get_bool_or(sexp.list(), ":boo") == false);
|
|
||||||
g_assert_true(Command::get_bool_or(sexp.list(), ":bah") == true);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
call(const Command::CommandMap& cmap, const std::string& str)
|
|
||||||
try {
|
|
||||||
const auto sexp{Sexp::make_parse(str)};
|
|
||||||
invoke(cmap, sexp);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (const Error& err) {
|
|
||||||
g_warning("%s", err.what());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
test_command()
|
|
||||||
{
|
|
||||||
using namespace Command;
|
|
||||||
allow_warnings();
|
|
||||||
|
|
||||||
CommandMap cmap;
|
|
||||||
|
|
||||||
cmap.emplace(
|
|
||||||
"my-command",
|
|
||||||
CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}},
|
|
||||||
{":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}},
|
|
||||||
"My command,",
|
|
||||||
{}});
|
|
||||||
|
|
||||||
g_assert_true(call(cmap, "(my-command :param1 \"hello\")"));
|
|
||||||
g_assert_true(call(cmap, "(my-command :param1 \"hello\" :param2 123)"));
|
|
||||||
|
|
||||||
g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 123 :param3 xxx)"));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
test_command2()
|
|
||||||
{
|
|
||||||
using namespace Command;
|
|
||||||
allow_warnings();
|
|
||||||
|
|
||||||
CommandMap cmap;
|
|
||||||
cmap.emplace("bla",
|
|
||||||
CommandInfo{ArgMap{
|
|
||||||
{":foo", ArgInfo{Sexp::Type::Number, false, "foo"}},
|
|
||||||
{":bar", ArgInfo{Sexp::Type::String, false, "bar"}},
|
|
||||||
},
|
|
||||||
"yeah",
|
|
||||||
[&](const auto& params) {}});
|
|
||||||
|
|
||||||
g_assert_true(call(cmap, "(bla :foo nil)"));
|
|
||||||
g_assert_false(call(cmap, "(bla :foo nil :bla nil)"));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
test_command_fail()
|
|
||||||
{
|
|
||||||
using namespace Command;
|
|
||||||
|
|
||||||
allow_warnings();
|
|
||||||
|
|
||||||
CommandMap cmap;
|
|
||||||
|
|
||||||
cmap.emplace(
|
|
||||||
"my-command",
|
|
||||||
CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}},
|
|
||||||
{":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}},
|
|
||||||
"My command,",
|
|
||||||
{}});
|
|
||||||
|
|
||||||
g_assert_false(call(cmap, "(my-command)"));
|
|
||||||
g_assert_false(call(cmap, "(my-command2)"));
|
|
||||||
g_assert_false(call(cmap, "(my-command :param1 123 :param2 123)"));
|
|
||||||
g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 \"123\")"));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
black_hole()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
int
|
|
||||||
main(int argc, char* argv[]) try {
|
|
||||||
|
|
||||||
mu_test_init(&argc, &argv);
|
|
||||||
|
|
||||||
g_test_add_func("/utils/command-parser/param-getters", test_param_getters);
|
|
||||||
g_test_add_func("/utils/command-parser/command", test_command);
|
|
||||||
g_test_add_func("/utils/command-parser/command2", test_command2);
|
|
||||||
g_test_add_func("/utils/command-parser/command-fail", test_command_fail);
|
|
||||||
|
|
||||||
g_log_set_handler(
|
|
||||||
NULL,
|
|
||||||
(GLogLevelFlags)(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION),
|
|
||||||
(GLogFunc)black_hole,
|
|
||||||
NULL);
|
|
||||||
|
|
||||||
return g_test_run();
|
|
||||||
|
|
||||||
} catch (const std::runtime_error& re) {
|
|
||||||
std::cerr << re.what() << "\n";
|
|
||||||
return 1;
|
|
||||||
}
|
|
|
@ -1,190 +0,0 @@
|
||||||
/*
|
|
||||||
** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
|
||||||
**
|
|
||||||
** This library is free software; you can redistribute it and/or
|
|
||||||
** modify it under the terms of the GNU Lesser General Public License
|
|
||||||
** as published by the Free Software Foundation; either version 2.1
|
|
||||||
** of the License, or (at your option) any later version.
|
|
||||||
**
|
|
||||||
** This library is distributed in the hope that it will be useful,
|
|
||||||
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
** Lesser General Public License for more details.
|
|
||||||
**
|
|
||||||
** You should have received a copy of the GNU Lesser General Public
|
|
||||||
** License along with this library; if not, write to the Free
|
|
||||||
** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
|
|
||||||
** 02110-1301, USA.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
#include <glib.h>
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
#include "mu-command-parser.hh"
|
|
||||||
#include "mu-utils.hh"
|
|
||||||
#include "mu-test-utils.hh"
|
|
||||||
|
|
||||||
using namespace Mu;
|
|
||||||
|
|
||||||
static bool
|
|
||||||
check_parse(const std::string& expr, const std::string& expected)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
const auto parsed{to_string(Sexp::make_parse(expr))};
|
|
||||||
assert_equal(parsed, expected);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (const Error& err) {
|
|
||||||
g_warning("caught exception parsing '%s': %s", expr.c_str(), err.what());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
test_parser()
|
|
||||||
{
|
|
||||||
check_parse(":foo-123", ":foo-123");
|
|
||||||
check_parse("foo", "foo");
|
|
||||||
check_parse(R"(12345)", "12345");
|
|
||||||
check_parse(R"(-12345)", "-12345");
|
|
||||||
check_parse(R"((123 bar "cuux"))", "(123 bar \"cuux\")");
|
|
||||||
|
|
||||||
check_parse(R"("foo\"bar\"cuux")", "\"foo\\\"bar\\\"cuux\"");
|
|
||||||
|
|
||||||
check_parse(R"("foo
|
|
||||||
bar")",
|
|
||||||
"\"foo\nbar\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
test_list()
|
|
||||||
{
|
|
||||||
const auto nstr{Sexp::make_string("foo")};
|
|
||||||
g_assert_true(nstr.value() == "foo");
|
|
||||||
g_assert_true(nstr.type() == Sexp::Type::String);
|
|
||||||
assert_equal(nstr.to_sexp_string(), "\"foo\"");
|
|
||||||
|
|
||||||
const auto nnum{Sexp::make_number(123)};
|
|
||||||
g_assert_true(nnum.value() == "123");
|
|
||||||
g_assert_true(nnum.type() == Sexp::Type::Number);
|
|
||||||
assert_equal(nnum.to_sexp_string(), "123");
|
|
||||||
|
|
||||||
const auto nsym{Sexp::make_symbol("blub")};
|
|
||||||
g_assert_true(nsym.value() == "blub");
|
|
||||||
g_assert_true(nsym.type() == Sexp::Type::Symbol);
|
|
||||||
assert_equal(nsym.to_sexp_string(), "blub");
|
|
||||||
|
|
||||||
Sexp::List list;
|
|
||||||
list.add(Sexp::make_string("foo"))
|
|
||||||
.add(Sexp::make_number(123))
|
|
||||||
.add(Sexp::make_symbol("blub"));
|
|
||||||
|
|
||||||
const auto nlst = Sexp::make_list(std::move(list));
|
|
||||||
g_assert_true(nlst.list().size() == 3);
|
|
||||||
g_assert_true(nlst.type() == Sexp::Type::List);
|
|
||||||
g_assert_true(nlst.list().at(1).value() == "123");
|
|
||||||
|
|
||||||
assert_equal(nlst.to_sexp_string(), "(\"foo\" 123 blub)");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
test_prop_list()
|
|
||||||
{
|
|
||||||
Sexp::List l1;
|
|
||||||
l1.add_prop(":foo", Sexp::make_string("bar"));
|
|
||||||
Sexp s2{Sexp::make_list(std::move(l1))};
|
|
||||||
assert_equal(s2.to_sexp_string(), "(:foo \"bar\")");
|
|
||||||
g_assert_true(s2.is_prop_list());
|
|
||||||
|
|
||||||
Sexp::List l2;
|
|
||||||
const std::string x{"bar"};
|
|
||||||
l2.add_prop(":foo", Sexp::make_string(x));
|
|
||||||
l2.add_prop(":bar", Sexp::make_number(77));
|
|
||||||
Sexp::List l3;
|
|
||||||
l3.add_prop(":cuux", Sexp::make_list(std::move(l2)));
|
|
||||||
Sexp s3{Sexp::make_list(std::move(l3))};
|
|
||||||
assert_equal(s3.to_sexp_string(), "(:cuux (:foo \"bar\" :bar 77))");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
test_props()
|
|
||||||
{
|
|
||||||
auto sexp2 = Sexp::make_list(Sexp::make_string("foo"),
|
|
||||||
Sexp::make_number(123),
|
|
||||||
Sexp::make_symbol("blub"));
|
|
||||||
|
|
||||||
auto sexp = Sexp::make_prop_list(":foo",
|
|
||||||
Sexp::make_string("bär"),
|
|
||||||
":cuux",
|
|
||||||
Sexp::make_number(123),
|
|
||||||
":flub",
|
|
||||||
Sexp::make_symbol("fnord"),
|
|
||||||
":boo",
|
|
||||||
std::move(sexp2));
|
|
||||||
|
|
||||||
assert_equal(sexp.to_sexp_string(),
|
|
||||||
"(:foo \"b\303\244r\" :cuux 123 :flub fnord :boo (\"foo\" 123 blub))");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
test_prop_list_remove()
|
|
||||||
{
|
|
||||||
{
|
|
||||||
Sexp::List lst;
|
|
||||||
lst.add_prop(":foo", Sexp::make_string("123"))
|
|
||||||
.add_prop(":bar", Sexp::make_number(123));
|
|
||||||
|
|
||||||
assert_equal(Sexp::make_list(std::move(lst)).to_sexp_string(),
|
|
||||||
R"((:foo "123" :bar 123))");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
Sexp::List lst;
|
|
||||||
lst.add_prop(":foo", Sexp::make_string("123"))
|
|
||||||
.add_prop(":bar", Sexp::make_number(123));
|
|
||||||
|
|
||||||
assert_equal(Sexp::make_list(Sexp::List{lst}).to_sexp_string(),
|
|
||||||
R"((:foo "123" :bar 123))");
|
|
||||||
|
|
||||||
lst.remove_prop(":bar");
|
|
||||||
|
|
||||||
assert_equal(Sexp::make_list(Sexp::List{lst}).to_sexp_string(),
|
|
||||||
R"((:foo "123"))");
|
|
||||||
|
|
||||||
lst.clear();
|
|
||||||
g_assert_cmpuint(lst.size(), ==, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
Sexp::List lst;
|
|
||||||
lst.add(Sexp::make_number(123));
|
|
||||||
Sexp s2{Sexp::make_list(std::move(lst))};
|
|
||||||
g_assert_false(s2.is_prop_list());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int
|
|
||||||
main(int argc, char* argv[])
|
|
||||||
try {
|
|
||||||
mu_test_init(&argc, &argv);
|
|
||||||
|
|
||||||
if (argc == 2) {
|
|
||||||
std::cout << Sexp::make_parse(argv[1]) << '\n';
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_test_add_func("/utils/sexp/parser", test_parser);
|
|
||||||
g_test_add_func("/utils/sexp/list", test_list);
|
|
||||||
g_test_add_func("/utils/sexp/proplist", test_prop_list);
|
|
||||||
g_test_add_func("/utils/sexp/proplist-remove", test_prop_list_remove);
|
|
||||||
g_test_add_func("/utils/sexp/props", test_props);
|
|
||||||
|
|
||||||
return g_test_run();
|
|
||||||
|
|
||||||
} catch (const std::runtime_error& re) {
|
|
||||||
std::cerr << re.what() << "\n";
|
|
||||||
return 1;
|
|
||||||
}
|
|
|
@ -372,12 +372,10 @@ static bool
|
||||||
output_sexp(const Option<Message>& msg, const OutputInfo& info, const MuConfig* opts, GError** err)
|
output_sexp(const Option<Message>& msg, const OutputInfo& info, const MuConfig* opts, GError** err)
|
||||||
{
|
{
|
||||||
if (msg) {
|
if (msg) {
|
||||||
|
if (const auto sexp{msg->sexp()}; !sexp.empty())
|
||||||
if (const auto sexp{msg->cached_sexp()}; !sexp.empty())
|
fputs(sexp.to_string().c_str(), stdout);
|
||||||
fputs(sexp.c_str(), stdout);
|
|
||||||
else
|
else
|
||||||
fputs(msg->to_sexp().to_sexp_string().c_str(), stdout);
|
fputs(msg->sexp().to_string().c_str(), stdout);
|
||||||
|
|
||||||
fputs("\n", stdout);
|
fputs("\n", stdout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -401,7 +399,7 @@ output_json(const Option<Message>& msg, const OutputInfo& info, const MuConfig*
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
g_print("%s%s\n",
|
g_print("%s%s\n",
|
||||||
msg->to_sexp().to_json_string().c_str(),
|
msg->sexp().to_json_string().c_str(),
|
||||||
info.last ? "" : ",");
|
info.last ? "" : ",");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
#include "mu-server.hh"
|
#include "mu-server.hh"
|
||||||
|
|
||||||
#include "utils/mu-utils.hh"
|
#include "utils/mu-utils.hh"
|
||||||
#include "utils/mu-command-parser.hh"
|
#include "utils/mu-command-handler.hh"
|
||||||
#include "utils/mu-readline.hh"
|
#include "utils/mu-readline.hh"
|
||||||
|
|
||||||
using namespace Mu;
|
using namespace Mu;
|
||||||
|
@ -82,15 +82,15 @@ cookie(size_t n)
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
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:
|
/* if requested, insert \n between list elements; note:
|
||||||
* is _not_ inherited by children */
|
* is _not_ inherited by children */
|
||||||
|
Sexp::Format fopts{};
|
||||||
if (any_of(flags & Server::OutputFlags::SplitList))
|
if (any_of(flags & Server::OutputFlags::SplitList))
|
||||||
sexp.formatting_opts |= Sexp::FormattingOptions::SplitList;
|
fopts |= Sexp::Format::SplitList;
|
||||||
|
|
||||||
const auto str{sexp.to_sexp_string()};
|
|
||||||
|
|
||||||
|
const auto str{sexp.to_string(fopts)};
|
||||||
cookie(str.size() + 1);
|
cookie(str.size() + 1);
|
||||||
if (G_UNLIKELY(::puts(str.c_str()) < 0)) {
|
if (G_UNLIKELY(::puts(str.c_str()) < 0)) {
|
||||||
g_critical("failed to write output '%s'", str.c_str());
|
g_critical("failed to write output '%s'", str.c_str());
|
||||||
|
@ -104,12 +104,8 @@ output_sexp_stdout(Sexp&& sexp, Server::OutputFlags flags)
|
||||||
static void
|
static void
|
||||||
report_error(const Mu::Error& err) noexcept
|
report_error(const Mu::Error& err) noexcept
|
||||||
{
|
{
|
||||||
Sexp::List e;
|
output_sexp_stdout(Sexp(":error"_sym, static_cast<size_t>(err.code()),
|
||||||
|
":message"_sym, err.what()),
|
||||||
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)),
|
|
||||||
Server::OutputFlags::Flush);
|
Server::OutputFlags::Flush);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ using namespace Mu;
|
||||||
static Mu::Result<void>
|
static Mu::Result<void>
|
||||||
view_msg_sexp(const Message& message, const MuConfig* opts)
|
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);
|
::fputs("\n", stdout);
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
|
|
Loading…
Reference in New Issue
Block a user