store: ensure updates update message sexp too

And turn all "add" into "replace" so old messages get removed.
Update tests too.
This commit is contained in:
Dirk-Jan C. Binnema 2022-05-05 01:27:08 +03:00
parent c0ae7e6860
commit 6126d7ee62
8 changed files with 329 additions and 170 deletions

View File

@ -20,7 +20,6 @@ lib_mu_message=static_library(
[ [
'mu-message.cc', 'mu-message.cc',
'mu-message-part.cc', 'mu-message-part.cc',
'mu-message-sexp.cc',
'mu-contact.cc', 'mu-contact.cc',
'mu-document.cc', 'mu-document.cc',
'mu-fields.cc', 'mu-fields.cc',

View File

@ -66,16 +66,12 @@ struct Store::Private {
enum struct XapianOpts { ReadOnly, Open, CreateOverwrite }; enum struct XapianOpts { ReadOnly, Open, CreateOverwrite };
Private(const std::string& path, bool readonly) Private(const std::string& path, bool readonly)
: read_only_{readonly}, db_{make_xapian_db(path, : read_only_{readonly}, db_{make_xapian_db(path,
read_only_ ? XapianOpts::ReadOnly read_only_ ? XapianOpts::ReadOnly
: XapianOpts::Open)}, : XapianOpts::Open)},
properties_{make_properties(path)}, properties_{make_properties(path)},
contacts_cache_{db().get_metadata(ContactsKey), contacts_cache_{db().get_metadata(ContactsKey),
properties_.personal_addresses} { properties_.personal_addresses} {
/* we do our own flushing, set Xapian's internal one as the backstop*/
g_setenv("XAPIAN_FLUSH_THRESHOLD",
format("%zu", properties_.batch_size + 100).c_str(), 1);
} }
Private(const std::string& path, Private(const std::string& path,
@ -85,9 +81,20 @@ struct Store::Private {
: read_only_{false}, db_{make_xapian_db(path, XapianOpts::CreateOverwrite)}, : read_only_{false}, db_{make_xapian_db(path, XapianOpts::CreateOverwrite)},
properties_{init_metadata(conf, path, root_maildir, personal_addresses)}, properties_{init_metadata(conf, path, root_maildir, personal_addresses)},
contacts_cache_{"", properties_.personal_addresses} { contacts_cache_{"", properties_.personal_addresses} {
/* we do our own flushing, set Xapian's internal one as the backstop*/
g_setenv("XAPIAN_FLUSH_THRESHOLD", /* add synonym */
format("%zu", properties_.batch_size + 100).c_str(), 1); for (auto&& info: AllMessageFlagInfos) {
constexpr auto field{field_from_id(Field::Id::Flags)};
const auto s1{field.xapian_term(info.name)};
const auto s2{field.xapian_term(info.shortcut)};
writable_db().add_synonym(s1, s2);
}
for (auto&& prio : AllMessagePriorities) {
constexpr auto field{field_from_id(Field::Id::Priority)};
const auto s1{field.xapian_term(to_string(prio))};
const auto s2{field.xapian_term(to_char(prio))};
writable_db().add_synonym(s1, s2);
}
} }
~Private() ~Private()
@ -102,15 +109,23 @@ struct Store::Private {
std::unique_ptr<Xapian::Database> make_xapian_db(const std::string db_path, XapianOpts opts) std::unique_ptr<Xapian::Database> make_xapian_db(const std::string db_path, XapianOpts opts)
try { try {
/* we do our own flushing, set Xapian's internal one as the
* backstop*/
g_setenv("XAPIAN_FLUSH_THRESHOLD",
format("%zu", properties_.batch_size + 100).c_str(), 1);
switch (opts) { switch (opts) {
case XapianOpts::ReadOnly: return std::make_unique<Xapian::Database>(db_path); case XapianOpts::ReadOnly:
return std::make_unique<Xapian::Database>(db_path);
case XapianOpts::Open: case XapianOpts::Open:
return std::make_unique<Xapian::WritableDatabase>(db_path, Xapian::DB_OPEN); return std::make_unique<Xapian::WritableDatabase>(db_path, Xapian::DB_OPEN);
case XapianOpts::CreateOverwrite: case XapianOpts::CreateOverwrite:
return std::make_unique<Xapian::WritableDatabase>( return std::make_unique<Xapian::WritableDatabase>(
db_path, db_path,
Xapian::DB_CREATE_OR_OVERWRITE); Xapian::DB_CREATE_OR_OVERWRITE);
default: throw std::logic_error("invalid xapian options"); default:
throw std::logic_error("invalid xapian options");
} }
} catch (const Xapian::DatabaseError& xde) { } catch (const Xapian::DatabaseError& xde) {
@ -145,8 +160,7 @@ struct Store::Private {
// Opportunistically commit a transaction if the transaction size // Opportunistically commit a transaction if the transaction size
// filled up a batch, or with force. // filled up a batch, or with force.
void transaction_maybe_commit(bool force = false) noexcept void transaction_maybe_commit(bool force = false) noexcept {
{
if (force || transaction_size_ >= properties_.batch_size) { if (force || transaction_size_ >= properties_.batch_size) {
if (contacts_cache_.dirty()) { if (contacts_cache_.dirty()) {
xapian_try([&] { xapian_try([&] {
@ -168,29 +182,7 @@ struct Store::Private {
} }
} }
void add_synonyms() time_t metadata_time_t(const std::string& key) const {
{
for (auto&& info: AllMessageFlagInfos) {
constexpr auto field{field_from_id(Field::Id::Flags)};
const auto s1{field.xapian_term(info.name)};
const auto s2{field.xapian_term(info.shortcut)};
writable_db().clear_synonyms(s1);
writable_db().clear_synonyms(s2);
writable_db().add_synonym(s1, s2);
}
for (auto&& prio : AllMessagePriorities) {
constexpr auto field{field_from_id(Field::Id::Priority)};
const auto s1{field.xapian_term(to_string(prio))};
const auto s2{field.xapian_term(to_char(prio))};
writable_db().clear_synonyms(s1);
writable_db().clear_synonyms(s2);
writable_db().add_synonym(s1, s2);
}
}
time_t metadata_time_t(const std::string& key) const
{
const auto ts = db().get_metadata(key); const auto ts = db().get_metadata(key);
return (time_t)atoll(db().get_metadata(key).c_str()); return (time_t)atoll(db().get_metadata(key).c_str());
} }
@ -214,8 +206,8 @@ struct Store::Private {
Store::Properties init_metadata(const Store::Config& conf, Store::Properties init_metadata(const Store::Config& conf,
const std::string& path, const std::string& path,
const std::string& root_maildir, const std::string& root_maildir,
const StringVec& personal_addresses) const StringVec& personal_addresses) {
{
writable_db().set_metadata(SchemaVersionKey, ExpectedSchemaVersion); writable_db().set_metadata(SchemaVersionKey, ExpectedSchemaVersion);
writable_db().set_metadata(CreatedKey, Mu::format("%" PRId64, (int64_t)::time({}))); writable_db().set_metadata(CreatedKey, Mu::format("%" PRId64, (int64_t)::time({})));
@ -240,6 +232,10 @@ struct Store::Private {
return make_properties(path); return make_properties(path);
} }
Option<Message> find_message_unlocked(Store::Id docid) const;
Result<Store::Id> update_message_unlocked(Message& msg, Store::Id docid);
Result<Store::Id> update_message_unlocked(Message& msg, const std::string& old_path);
/* metadata to write as part of a transaction commit */ /* metadata to write as part of a transaction commit */
std::unordered_map<std::string, std::string> metadata_cache_; std::unordered_map<std::string, std::string> metadata_cache_;
@ -254,6 +250,47 @@ struct Store::Private {
std::mutex lock_; std::mutex lock_;
}; };
Result<Store::Id>
Store::Private::update_message_unlocked(Message& msg, Store::Id docid)
{
msg.update_cached_sexp();
return xapian_try_result([&]{
writable_db().replace_document(docid, msg.document().xapian_document());
g_debug("updated message @ %s; docid = %u", msg.path().c_str(), docid);
return Ok(std::move(docid));
});
}
Result<Store::Id>
Store::Private::update_message_unlocked(Message& msg, const std::string& path_to_replace)
{
msg.update_cached_sexp();
return xapian_try_result([&]{
auto id = writable_db().replace_document(
field_from_id(Field::Id::Path).xapian_term(path_to_replace),
msg.document().xapian_document());
return Ok(std::move(id));
});
}
Option<Message>
Store::Private::find_message_unlocked(Store::Id docid) const
{
return xapian_try([&]()->Option<Message> {
auto res = Message::make_from_document(db().get_document(docid));
if (res)
return Some(std::move(res.value()));
else
return Nothing;
}, Nothing);
}
Store::Store(const std::string& path, bool readonly) Store::Store(const std::string& path, bool readonly)
: priv_{std::make_unique<Private>(path, readonly)} : priv_{std::make_unique<Private>(path, readonly)}
{ {
@ -293,12 +330,6 @@ Store::database() const
return priv_->db(); return priv_->db();
} }
Xapian::WritableDatabase&
Store::writable_database()
{
return priv_->writable_db();
}
Indexer& Indexer&
Store::indexer() Store::indexer()
{ {
@ -337,6 +368,8 @@ Store::add_message(const std::string& path, bool use_transaction)
Result<Store::Id> Result<Store::Id>
Store::add_message(Message& msg, bool use_transaction) Store::add_message(Message& msg, bool use_transaction)
{ {
std::lock_guard guard{priv_->lock_};
const auto mdir{mu_maildir_from_path(msg.path(), const auto mdir{mu_maildir_from_path(msg.path(),
properties().root_maildir)}; properties().root_maildir)};
if (!mdir) if (!mdir)
@ -356,51 +389,28 @@ Store::add_message(Message& msg, bool use_transaction)
if (is_personal) if (is_personal)
msg.set_flags(msg.flags() | Flags::Personal); msg.set_flags(msg.flags() | Flags::Personal);
/* now, we're done with all the fields; generate the sexp string for this if (use_transaction)
* message */ priv_->transaction_inc();
msg.update_cached_sexp();
std::lock_guard guard{priv_->lock_}; auto res = priv_->update_message_unlocked(msg, msg.path());
if (!res)
return Err(res.error());
const auto docid = xapian_try([&]{ if (use_transaction) /* commit if batch is full */
priv_->transaction_maybe_commit();
if (use_transaction) g_debug("added message @ %s; docid = %u", msg.path().c_str(), *res);
priv_->transaction_inc();
const auto docid = priv_->writable_db().add_document( return res;
msg.document().xapian_document());
if (use_transaction) /* commit if batch is full */
priv_->transaction_maybe_commit();
return docid;
}, InvalidId);
if (G_UNLIKELY(docid == InvalidId))
return Err(Error::Code::Message, "failed to add message");
g_debug("added message @ %s; docid = %u", msg.path().c_str(), docid);
g_debug("%s", msg.document().xapian_document().get_description().c_str());
return Ok(static_cast<Store::Id>(docid));
} }
bool Result<Store::Id>
Store::update_message(Message& msg, unsigned docid) Store::update_message(Message& msg, Store::Id docid)
{ {
msg.update_cached_sexp();
std::lock_guard guard{priv_->lock_}; std::lock_guard guard{priv_->lock_};
return xapian_try( return priv_->update_message_unlocked(msg, docid);
[&]{
priv_->writable_db().replace_document(
docid, msg.document().xapian_document());
g_debug("updated message @ %s; docid = %u",
msg.path().c_str(), docid);
return true;
}, false);
} }
bool bool
@ -435,48 +445,52 @@ Store::remove_messages(const std::vector<Store::Id>& ids)
} }
Option<Message>
Store::find_message(Store::Id docid) const
{
std::lock_guard guard{priv_->lock_};
return priv_->find_message_unlocked(docid);
}
Result<Message> Result<Message>
Store::move_message(Store::Id id, Store::move_message(Store::Id id,
Option<const std::string&> target_mdir, Option<const std::string&> target_mdir,
Option<Flags> new_flags, bool change_name) Option<Flags> new_flags, bool change_name)
{ {
auto msg = find_message(id); std::lock_guard guard{priv_->lock_};
auto msg = priv_->find_message_unlocked(id);
if (!msg) if (!msg)
return Err(Error::Code::Store, "cannot find message <%u>", id); return Err(Error::Code::Store, "cannot find message <%u>", id);
const auto old_path = msg->path();
const auto target_flags = new_flags.value_or(msg->flags()); const auto target_flags = new_flags.value_or(msg->flags());
const auto target_maildir = target_mdir.value_or(msg->maildir()); const auto target_maildir = target_mdir.value_or(msg->maildir());
/* 1. first determine the file system path of the target */ /* 1. first determine the file system path of the target */
const auto target_path = const auto target_path =
mu_maildir_determine_target(msg->path(), mu_maildir_determine_target(msg->path(), properties().root_maildir,
properties().root_maildir, target_maildir,target_flags, change_name);
target_maildir,
target_flags,
change_name);
if (!target_path) if (!target_path)
return Err(target_path.error()); return Err(target_path.error());
/* 2. let's move it */ /* 2. let's move it */
const auto move_res = if (const auto res = mu_maildir_move_message(
mu_maildir_move_message(msg->path(), msg->path(), target_path.value(), true/*ignore dups*/); !res)
target_path.value(), return Err(res.error());
true/*ignore dups*/);
if (!move_res)
return Err(move_res.error());
/* 3. file move worked, now update the message with the new info.*/ /* 3. file move worked, now update the message with the new info.*/
const auto update_res = msg->update_after_move(target_path.value(), if (auto&& res = msg->update_after_move(
target_maildir, target_path.value(), target_maildir, target_flags); !res)
target_flags); return Err(res.error());
if (!update_res)
return Err(update_res.error());
/* 4. update message worked; re-store it */ /* 4. update message worked; re-store it */
if (!update_message(*msg, id)) if (auto&& res = priv_->update_message_unlocked(*msg, old_path); !res)
return Err(Error::Code::Store, "failed to update message <%u>", id); return Err(res.error());
/* 5. Profit! */ /* 6. Profit! */
return Ok(std::move(msg.value())); return Ok(std::move(msg.value()));
} }
@ -529,21 +543,6 @@ Store::set_dirstamp(const std::string& path, time_t tstamp)
set_metadata(path, std::string{data.data(), len}); set_metadata(path, std::string{data.data(), len});
} }
Option<Message>
Store::find_message(Store::Id docid) const
{
return xapian_try(
[&]()->Option<Message> {
std::lock_guard guard{priv_->lock_};
auto res = Message::make_from_document(priv_->db().get_document(docid));
if (res)
return Some(std::move(res.value()));
else
return Nothing;
},
Nothing);
}
bool bool
Store::contains_message(const std::string& path) const Store::contains_message(const std::string& path) const
{ {
@ -599,6 +598,7 @@ Store::for_each_term(Field::Id field_id, Store::ForEachTermFunc func) const
const auto prefix{field_from_id(field_id).xapian_term()}; const auto prefix{field_from_id(field_id).xapian_term()};
for (auto it = priv_->db().allterms_begin(prefix); for (auto it = priv_->db().allterms_begin(prefix);
it != priv_->db().allterms_end(prefix); ++it) { it != priv_->db().allterms_end(prefix); ++it) {
++n;
if (!func(*it)) if (!func(*it))
break; break;
} }

View File

@ -67,6 +67,7 @@ public:
* @param maildir maildir to use for this store * @param maildir maildir to use for this store
* @param personal_addresses addresses that should be recognized as * @param personal_addresses addresses that should be recognized as
* 'personal' for identifying personal messages. * 'personal' for identifying personal messages.
* @param config a configuration object
*/ */
Store(const std::string& path, Store(const std::string& path,
const std::string& maildir, const std::string& maildir,
@ -116,14 +117,6 @@ public:
*/ */
const Xapian::Database& database() const; const Xapian::Database& database() const;
/**
* Get the underlying writable Xapian database for this
* store. Throws is this store is not writable.
*
* @return the writable database
*/
Xapian::WritableDatabase& writable_database();
/** /**
* Get the Indexer associated with this store. It is an error to call * Get the Indexer associated with this store. It is an error to call
* this on a read-only store. * this on a read-only store.
@ -205,9 +198,9 @@ public:
* @param msg a message * @param msg a message
* @param id the id for this message * @param id the id for this message
* *
* @return false in case of failure; true otherwise. * @return Ok() or an error.
*/ */
bool update_message(Message& msg, Id id); Result<Store::Id> update_message(Message& msg, Id id);
/** /**
* Remove a message from the store. It will _not_ remove the message * Remove a message from the store. It will _not_ remove the message
@ -384,6 +377,7 @@ public:
const std::unique_ptr<Private>& priv() const { return priv_; } const std::unique_ptr<Private>& priv() const { return priv_; }
private: private:
std::unique_ptr<Private> priv_; std::unique_ptr<Private> priv_;
}; };

View File

@ -21,8 +21,10 @@
#include <glib.h> #include <glib.h>
#include <stdlib.h> #include <stdlib.h>
#include <thread>
#include <unistd.h> #include <unistd.h>
#include <time.h> #include <time.h>
#include <fstream>
#include <locale.h> #include <locale.h>
@ -30,6 +32,7 @@
#include "mu-store.hh" #include "mu-store.hh"
#include "utils/mu-result.hh" #include "utils/mu-result.hh"
#include <utils/mu-utils.hh> #include <utils/mu-utils.hh>
#include "mu-maildir.hh"
using namespace Mu; using namespace Mu;
@ -224,6 +227,93 @@ World!
} }
static void
test_index_move()
{
using namespace std::chrono_literals;
const std::string msg_text =
R"(From: Valentine Michael Smith <mike@example.com>
To: Raul Endymion <raul@example.com>
Cc: emacs-devel@gnu.org
Subject: Re: multi-eq hash tables
Date: Tue, 03 May 2022 20:58:02 +0200
Message-ID: <87h766tzzz.fsf@gnus.org>
MIME-Version: 1.0
Content-Type: text/plain
Precedence: list
List-Id: "Emacs development discussions." <emacs-devel.gnu.org>
List-Post: <mailto:emacs-devel@gnu.org>
Raul Endymion <raul@example.com> writes:
> Maybe we should introduce something like:
>
> (define-hash-table-test shallow-equal
> (lambda (x1 x2) (while (and (consp x1) (consp x2) (eql (car x1) (car x2)))
> (setq x1 (cdr x1)) (setq x2 (cdr x2)))
> (equal x1 x2)))
> ...)
Yes, that would be excellent.
)";
TempDir tempdir2;
{ // create a message file.
const auto res1 = mu_maildir_mkdir(tempdir2.path() + "/Maildir/a");
assert_valid_result(res1);
std::ofstream output{tempdir2.path() + "/Maildir/a/new/msg"};
output.write(msg_text.c_str(), msg_text.size());
output.close();
g_assert_true(output.good());
}
// Index it into a store.
TempDir tempdir;
Store store{tempdir.path(), tempdir2.path() + "/Maildir", {}, {}};
store.indexer().start({});
size_t n{};
while (store.indexer().is_running()) {
std::this_thread::sleep_for(100ms);
g_assert_cmpuint(n++,<=,25);
}
g_assert_true(!store.indexer().is_running());
const auto& prog{store.indexer().progress()};
g_assert_cmpuint(prog.updated,==,1);
g_assert_cmpuint(store.size(), ==, 1);
g_assert_false(store.empty());
// Find the message
auto qr = store.run_query("path:" + tempdir2.path() + "/Maildir/a/new/msg");
assert_valid_result(qr);
g_assert_cmpuint(qr->size(),==,1);
const auto msg = qr->begin().message();
g_assert_true(!!msg);
// Check the message
const auto oldpath{msg->path()};
assert_equal(msg->subject(), "Re: multi-eq hash tables");
g_assert_true(msg->docid() != 0);
g_debug("%s", msg->to_sexp().to_sexp_string().c_str());
// Move the message from new->cur
std::this_thread::sleep_for(1s); /* ctime should change */
const auto msg3 = store.move_message(msg->docid(), {}, Flags::Seen);
assert_valid_result(msg3);
assert_equal(msg3->maildir(), "/a");
assert_equal(msg3->path(), tempdir2.path() + "/Maildir/a/cur/msg:2,S");
g_assert_true(::access(msg3->path().c_str(), R_OK)==0);
g_assert_false(::access(oldpath.c_str(), R_OK)==0);
g_debug("%s", msg3->to_sexp().to_sexp_string().c_str());
g_assert_cmpuint(store.size(), ==, 1);
}
int int
main(int argc, char* argv[]) main(int argc, char* argv[])
{ {
@ -236,6 +326,8 @@ main(int argc, char* argv[])
test_message_mailing_list); test_message_mailing_list);
g_test_add_func("/store/message/attachments", g_test_add_func("/store/message/attachments",
test_message_attachments); test_message_attachments);
g_test_add_func("/store/index/move",
test_index_move);
return g_test_run(); return g_test_run();
} }

View File

@ -190,7 +190,12 @@ Sexp::to_sexp_string() const
sstrm << ')'; sstrm << ')';
break; break;
} }
case Type::String: sstrm << quote(value()); break; case Type::String:
sstrm << quote(value());
break;
case Type::Raw:
sstrm << value();
break;
case Type::Number: case Type::Number:
case Type::Symbol: case Type::Symbol:
case Type::Empty: case Type::Empty:
@ -231,7 +236,12 @@ Sexp::to_json_string() const
} }
break; break;
} }
case Type::String: sstrm << quote(value()); break; case Type::String:
sstrm << quote(value());
break;
case Type::Raw: // FIXME: implement this.
break;
case Type::Symbol: case Type::Symbol:
if (is_nil()) if (is_nil())
sstrm << "false"; sstrm << "false";

View File

@ -1,5 +1,5 @@
/* /*
** Copyright (C) 2021 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
@ -37,7 +37,7 @@ namespace Mu {
/// Parse node /// Parse node
struct Sexp { struct Sexp {
/// Node type /// Node type
enum struct Type { Empty, List, String, Number, Symbol }; enum struct Type { Empty, List, String, Number, Symbol, Raw };
/** /**
* Default CTOR * Default CTOR
@ -81,13 +81,30 @@ struct Sexp {
} }
static Sexp make_number(int val) { return Sexp{Type::Number, format("%d", val)}; } static Sexp make_number(int val) { return Sexp{Type::Number, format("%d", val)}; }
static Sexp make_symbol(std::string&& val) static Sexp make_symbol(std::string&& val) {
{
if (val.empty()) if (val.empty())
throw Error(Error::Code::InvalidArgument, "symbol must be non-empty"); throw Error(Error::Code::InvalidArgument,
"symbol must be non-empty");
return Sexp{Type::Symbol, std::move(val)}; return Sexp{Type::Symbol, std::move(val)};
} }
static Sexp make_symbol_sv(std::string_view val) { return make_symbol(std::string{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});
}
/** /**
* *
@ -96,8 +113,7 @@ struct Sexp {
* *
* @return * @return
*/ */
const std::string& value() const const std::string& value() const {
{
if (is_list()) if (is_list())
throw Error(Error::Code::InvalidArgument, "no value for list"); throw Error(Error::Code::InvalidArgument, "no value for list");
if (is_empty()) if (is_empty())
@ -110,15 +126,14 @@ struct Sexp {
* *
* @return * @return
*/ */
const Seq& list() const const Seq& list() const {
{
if (!is_list()) if (!is_list())
throw Error(Error::Code::InvalidArgument, "not a list"); throw Error(Error::Code::InvalidArgument, "not a list");
return seq_; return seq_;
} }
/** /**
* Convert a Sexp::Node to its S-expression string representation * Convert a Sexp to its S-expression string representation
* *
* @return the string representation * @return the string representation
*/ */
@ -142,6 +157,9 @@ struct Sexp {
/// Helper struct to build mutable lists. /// Helper struct to build mutable lists.
/// ///
struct List { struct List {
List () = default;
List (const Seq& seq): seq_{seq} {}
/** /**
* Add a sexp to the list * Add a sexp to the list
* *
@ -164,7 +182,8 @@ struct Sexp {
} }
/** /**
* Add a property (i.e., :key sexp ) to the list * 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 name a property-name. Must start with ':', length > 1
* @param sexp a sexp * @param sexp a sexp
@ -172,8 +191,8 @@ struct Sexp {
* *
* @return a ref to this List (for chaining) * @return a ref to this List (for chaining)
*/ */
List& add_prop(std::string&& name, Sexp&& sexp) List& add_prop(std::string&& name, Sexp&& sexp) {
{ remove_prop(name);
if (!is_prop_name(name)) if (!is_prop_name(name))
throw Error{Error::Code::InvalidArgument, throw Error{Error::Code::InvalidArgument,
"invalid property name ('%s')", "invalid property name ('%s')",
@ -183,13 +202,27 @@ struct Sexp {
return *this; return *this;
} }
template <typename... Args> template <typename... Args>
List& add_prop(std::string&& name, Sexp&& sexp, Args... 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::move(name), std::move(sexp));
add_prop(std::forward<Args>(args)...); add_prop(std::forward<Args>(args)...);
return *this; 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. * Remove all elements from the list.
*/ */
@ -209,7 +242,7 @@ struct Sexp {
*/ */
size_t empty() const { return seq_.empty(); } size_t empty() const { return seq_.empty(); }
private: private:
friend struct Sexp; friend struct Sexp;
Seq seq_; Seq seq_;
}; };
@ -309,16 +342,14 @@ struct Sexp {
return is_prop_list(list().begin() + 1, list().end()); return is_prop_list(list().begin() + 1, list().end());
} }
private: private:
Sexp(Type typearg, std::string&& valuearg) : type_{typearg}, value_{std::move(valuearg)} Sexp(Type typearg, std::string&& valuearg) : type_{typearg}, value_{std::move(valuearg)} {
{
if (is_list()) if (is_list())
throw Error{Error::Code::InvalidArgument, "cannot be a list type"}; throw Error{Error::Code::InvalidArgument, "cannot be a list type"};
if (is_empty()) if (is_empty())
throw Error{Error::Code::InvalidArgument, "cannot be an empty type"}; throw Error{Error::Code::InvalidArgument, "cannot be an empty type"};
} }
Sexp(Type typearg, Seq&& seq) : type_{Type::List}, seq_{std::move(seq)} Sexp(Type typearg, Seq&& seq) : type_{Type::List}, seq_{std::move(seq)} {
{
if (!is_list()) if (!is_list())
throw Error{Error::Code::InvalidArgument, "must be a list type"}; throw Error{Error::Code::InvalidArgument, "must be a list type"};
if (is_empty()) if (is_empty())
@ -353,10 +384,10 @@ struct Sexp {
return b == e; return b == e;
} }
const Type type_; /**< Type of node */ Type type_; /**< Type of node */
const std::string value_; /**< String value of node (only for std::string value_; /**< String value of node (only for
* non-Type::Lst)*/ * non-Type::Lst)*/
const Seq seq_; /**< Children of node (only for Seq seq_; /**< Children of node (only for
* Type::Lst) */ * Type::Lst) */
}; };

View File

@ -55,7 +55,7 @@ test_parser()
check_parse(R"("foo check_parse(R"("foo
bar")", bar")",
"\"foo\nbar\""); "\"foo\nbar\"");
} }
static void static void
@ -111,20 +111,47 @@ static void
test_props() test_props()
{ {
auto sexp2 = Sexp::make_list(Sexp::make_string("foo"), auto sexp2 = Sexp::make_list(Sexp::make_string("foo"),
Sexp::make_number(123), Sexp::make_number(123),
Sexp::make_symbol("blub")); Sexp::make_symbol("blub"));
auto sexp = Sexp::make_prop_list(":foo", auto sexp = Sexp::make_prop_list(":foo",
Sexp::make_string("bär"), Sexp::make_string("bär"),
":cuux", ":cuux",
Sexp::make_number(123), Sexp::make_number(123),
":flub", ":flub",
Sexp::make_symbol("fnord"), Sexp::make_symbol("fnord"),
":boo", ":boo",
std::move(sexp2)); std::move(sexp2));
assert_equal(sexp.to_sexp_string(), assert_equal(sexp.to_sexp_string(),
"(:foo \"b\303\244r\" :cuux 123 :flub fnord :boo (\"foo\" 123 blub))"); "(: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"))");
}
} }
int int
@ -140,6 +167,7 @@ try {
g_test_add_func("/utils/sexp/parser", test_parser); g_test_add_func("/utils/sexp/parser", test_parser);
g_test_add_func("/utils/sexp/list", test_list); 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", test_prop_list);
g_test_add_func("/utils/sexp/proplist-remove", test_prop_list_remove);
g_test_add_func("/utils/sexp/props", test_props); g_test_add_func("/utils/sexp/props", test_props);
return g_test_run(); return g_test_run();

View File

@ -374,7 +374,12 @@ 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) {
fputs(msg->to_sexp().to_sexp_string().c_str(), stdout);
if (const auto sexp{msg->cached_sexp()}; !sexp.empty())
fputs(sexp.c_str(), stdout);
else
fputs(msg->to_sexp().to_sexp_string().c_str(), stdout);
fputs("\n", stdout); fputs("\n", stdout);
} }
@ -476,7 +481,7 @@ output_query_results(const QueryResults& qres, const MuConfig* opts)
if (!msg) if (!msg)
continue; continue;
if (opts->after != 0 && msg->mtime() < opts->after) if (opts->after != 0 && msg->changed() < opts->after)
continue; continue;
rv = output_func(msg, rv = output_func(msg,