mirror of https://github.com/djcb/mu.git
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:
parent
c0ae7e6860
commit
6126d7ee62
|
@ -20,7 +20,6 @@ lib_mu_message=static_library(
|
|||
[
|
||||
'mu-message.cc',
|
||||
'mu-message-part.cc',
|
||||
'mu-message-sexp.cc',
|
||||
'mu-contact.cc',
|
||||
'mu-document.cc',
|
||||
'mu-fields.cc',
|
||||
|
|
236
lib/mu-store.cc
236
lib/mu-store.cc
|
@ -66,16 +66,12 @@ struct Store::Private {
|
|||
enum struct XapianOpts { ReadOnly, Open, CreateOverwrite };
|
||||
|
||||
Private(const std::string& path, bool readonly)
|
||||
: read_only_{readonly}, db_{make_xapian_db(path,
|
||||
read_only_ ? XapianOpts::ReadOnly
|
||||
: XapianOpts::Open)},
|
||||
properties_{make_properties(path)},
|
||||
contacts_cache_{db().get_metadata(ContactsKey),
|
||||
: read_only_{readonly}, db_{make_xapian_db(path,
|
||||
read_only_ ? XapianOpts::ReadOnly
|
||||
: XapianOpts::Open)},
|
||||
properties_{make_properties(path)},
|
||||
contacts_cache_{db().get_metadata(ContactsKey),
|
||||
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,
|
||||
|
@ -85,9 +81,20 @@ struct Store::Private {
|
|||
: read_only_{false}, db_{make_xapian_db(path, XapianOpts::CreateOverwrite)},
|
||||
properties_{init_metadata(conf, path, root_maildir, 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",
|
||||
format("%zu", properties_.batch_size + 100).c_str(), 1);
|
||||
|
||||
/* add synonym */
|
||||
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()
|
||||
|
@ -102,15 +109,23 @@ struct Store::Private {
|
|||
|
||||
std::unique_ptr<Xapian::Database> make_xapian_db(const std::string db_path, XapianOpts opts)
|
||||
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) {
|
||||
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:
|
||||
return std::make_unique<Xapian::WritableDatabase>(db_path, Xapian::DB_OPEN);
|
||||
case XapianOpts::CreateOverwrite:
|
||||
return std::make_unique<Xapian::WritableDatabase>(
|
||||
db_path,
|
||||
db_path,
|
||||
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) {
|
||||
|
@ -145,8 +160,7 @@ struct Store::Private {
|
|||
|
||||
// Opportunistically commit a transaction if the transaction size
|
||||
// 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 (contacts_cache_.dirty()) {
|
||||
xapian_try([&] {
|
||||
|
@ -168,29 +182,7 @@ struct Store::Private {
|
|||
}
|
||||
}
|
||||
|
||||
void add_synonyms()
|
||||
{
|
||||
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
|
||||
{
|
||||
time_t metadata_time_t(const std::string& key) const {
|
||||
const auto ts = db().get_metadata(key);
|
||||
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,
|
||||
const std::string& path,
|
||||
const std::string& root_maildir,
|
||||
const StringVec& personal_addresses)
|
||||
{
|
||||
const StringVec& personal_addresses) {
|
||||
|
||||
writable_db().set_metadata(SchemaVersionKey, ExpectedSchemaVersion);
|
||||
writable_db().set_metadata(CreatedKey, Mu::format("%" PRId64, (int64_t)::time({})));
|
||||
|
||||
|
@ -240,6 +232,10 @@ struct Store::Private {
|
|||
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 */
|
||||
std::unordered_map<std::string, std::string> metadata_cache_;
|
||||
|
||||
|
@ -254,6 +250,47 @@ struct Store::Private {
|
|||
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)
|
||||
: priv_{std::make_unique<Private>(path, readonly)}
|
||||
{
|
||||
|
@ -293,12 +330,6 @@ Store::database() const
|
|||
return priv_->db();
|
||||
}
|
||||
|
||||
Xapian::WritableDatabase&
|
||||
Store::writable_database()
|
||||
{
|
||||
return priv_->writable_db();
|
||||
}
|
||||
|
||||
Indexer&
|
||||
Store::indexer()
|
||||
{
|
||||
|
@ -337,6 +368,8 @@ Store::add_message(const std::string& path, bool use_transaction)
|
|||
Result<Store::Id>
|
||||
Store::add_message(Message& msg, bool use_transaction)
|
||||
{
|
||||
std::lock_guard guard{priv_->lock_};
|
||||
|
||||
const auto mdir{mu_maildir_from_path(msg.path(),
|
||||
properties().root_maildir)};
|
||||
if (!mdir)
|
||||
|
@ -356,51 +389,28 @@ Store::add_message(Message& msg, bool use_transaction)
|
|||
if (is_personal)
|
||||
msg.set_flags(msg.flags() | Flags::Personal);
|
||||
|
||||
/* now, we're done with all the fields; generate the sexp string for this
|
||||
* message */
|
||||
msg.update_cached_sexp();
|
||||
if (use_transaction)
|
||||
priv_->transaction_inc();
|
||||
|
||||
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)
|
||||
priv_->transaction_inc();
|
||||
g_debug("added message @ %s; docid = %u", msg.path().c_str(), *res);
|
||||
|
||||
const auto docid = priv_->writable_db().add_document(
|
||||
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));
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
Store::update_message(Message& msg, unsigned docid)
|
||||
Result<Store::Id>
|
||||
Store::update_message(Message& msg, Store::Id docid)
|
||||
{
|
||||
msg.update_cached_sexp();
|
||||
|
||||
std::lock_guard guard{priv_->lock_};
|
||||
|
||||
return xapian_try(
|
||||
[&]{
|
||||
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);
|
||||
return priv_->update_message_unlocked(msg, docid);
|
||||
}
|
||||
|
||||
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>
|
||||
Store::move_message(Store::Id id,
|
||||
Option<const std::string&> target_mdir,
|
||||
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)
|
||||
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_maildir = target_mdir.value_or(msg->maildir());
|
||||
|
||||
/* 1. first determine the file system path of the target */
|
||||
const auto target_path =
|
||||
mu_maildir_determine_target(msg->path(),
|
||||
properties().root_maildir,
|
||||
target_maildir,
|
||||
target_flags,
|
||||
change_name);
|
||||
mu_maildir_determine_target(msg->path(), properties().root_maildir,
|
||||
target_maildir,target_flags, change_name);
|
||||
if (!target_path)
|
||||
return Err(target_path.error());
|
||||
|
||||
/* 2. let's move it */
|
||||
const auto move_res =
|
||||
mu_maildir_move_message(msg->path(),
|
||||
target_path.value(),
|
||||
true/*ignore dups*/);
|
||||
if (!move_res)
|
||||
return Err(move_res.error());
|
||||
if (const auto res = mu_maildir_move_message(
|
||||
msg->path(), target_path.value(), true/*ignore dups*/); !res)
|
||||
return Err(res.error());
|
||||
|
||||
/* 3. file move worked, now update the message with the new info.*/
|
||||
const auto update_res = msg->update_after_move(target_path.value(),
|
||||
target_maildir,
|
||||
target_flags);
|
||||
if (!update_res)
|
||||
return Err(update_res.error());
|
||||
if (auto&& res = msg->update_after_move(
|
||||
target_path.value(), target_maildir, target_flags); !res)
|
||||
return Err(res.error());
|
||||
|
||||
/* 4. update message worked; re-store it */
|
||||
if (!update_message(*msg, id))
|
||||
return Err(Error::Code::Store, "failed to update message <%u>", id);
|
||||
if (auto&& res = priv_->update_message_unlocked(*msg, old_path); !res)
|
||||
return Err(res.error());
|
||||
|
||||
/* 5. Profit! */
|
||||
/* 6. Profit! */
|
||||
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});
|
||||
}
|
||||
|
||||
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
|
||||
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()};
|
||||
for (auto it = priv_->db().allterms_begin(prefix);
|
||||
it != priv_->db().allterms_end(prefix); ++it) {
|
||||
++n;
|
||||
if (!func(*it))
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ public:
|
|||
* @param maildir maildir to use for this store
|
||||
* @param personal_addresses addresses that should be recognized as
|
||||
* 'personal' for identifying personal messages.
|
||||
* @param config a configuration object
|
||||
*/
|
||||
Store(const std::string& path,
|
||||
const std::string& maildir,
|
||||
|
@ -116,14 +117,6 @@ public:
|
|||
*/
|
||||
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
|
||||
* this on a read-only store.
|
||||
|
@ -205,9 +198,9 @@ public:
|
|||
* @param msg a 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
|
||||
|
@ -384,6 +377,7 @@ public:
|
|||
const std::unique_ptr<Private>& priv() const { return priv_; }
|
||||
|
||||
private:
|
||||
|
||||
std::unique_ptr<Private> priv_;
|
||||
};
|
||||
|
||||
|
|
|
@ -21,8 +21,10 @@
|
|||
|
||||
#include <glib.h>
|
||||
#include <stdlib.h>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <fstream>
|
||||
|
||||
#include <locale.h>
|
||||
|
||||
|
@ -30,6 +32,7 @@
|
|||
#include "mu-store.hh"
|
||||
#include "utils/mu-result.hh"
|
||||
#include <utils/mu-utils.hh>
|
||||
#include "mu-maildir.hh"
|
||||
|
||||
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
|
||||
main(int argc, char* argv[])
|
||||
{
|
||||
|
@ -236,6 +326,8 @@ main(int argc, char* argv[])
|
|||
test_message_mailing_list);
|
||||
g_test_add_func("/store/message/attachments",
|
||||
test_message_attachments);
|
||||
g_test_add_func("/store/index/move",
|
||||
test_index_move);
|
||||
|
||||
return g_test_run();
|
||||
}
|
||||
|
|
|
@ -190,7 +190,12 @@ Sexp::to_sexp_string() const
|
|||
sstrm << ')';
|
||||
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::Symbol:
|
||||
case Type::Empty:
|
||||
|
@ -231,7 +236,12 @@ Sexp::to_json_string() const
|
|||
}
|
||||
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:
|
||||
if (is_nil())
|
||||
sstrm << "false";
|
||||
|
|
|
@ -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
|
||||
** under the terms of the GNU General Public License as published by the
|
||||
|
@ -37,7 +37,7 @@ namespace Mu {
|
|||
/// Parse node
|
||||
struct Sexp {
|
||||
/// Node type
|
||||
enum struct Type { Empty, List, String, Number, Symbol };
|
||||
enum struct Type { Empty, List, String, Number, Symbol, Raw };
|
||||
|
||||
/**
|
||||
* Default CTOR
|
||||
|
@ -81,13 +81,30 @@ struct Sexp {
|
|||
}
|
||||
|
||||
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())
|
||||
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)};
|
||||
}
|
||||
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
|
||||
*/
|
||||
const std::string& value() const
|
||||
{
|
||||
const std::string& value() const {
|
||||
if (is_list())
|
||||
throw Error(Error::Code::InvalidArgument, "no value for list");
|
||||
if (is_empty())
|
||||
|
@ -110,15 +126,14 @@ struct Sexp {
|
|||
*
|
||||
* @return
|
||||
*/
|
||||
const Seq& list() const
|
||||
{
|
||||
const Seq& list() const {
|
||||
if (!is_list())
|
||||
throw Error(Error::Code::InvalidArgument, "not a list");
|
||||
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
|
||||
*/
|
||||
|
@ -142,6 +157,9 @@ struct Sexp {
|
|||
/// Helper struct to build mutable lists.
|
||||
///
|
||||
struct List {
|
||||
List () = default;
|
||||
List (const Seq& seq): seq_{seq} {}
|
||||
|
||||
/**
|
||||
* 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 sexp a sexp
|
||||
|
@ -172,8 +191,8 @@ struct Sexp {
|
|||
*
|
||||
* @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))
|
||||
throw Error{Error::Code::InvalidArgument,
|
||||
"invalid property name ('%s')",
|
||||
|
@ -183,13 +202,27 @@ struct Sexp {
|
|||
return *this;
|
||||
}
|
||||
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::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.
|
||||
*/
|
||||
|
@ -209,7 +242,7 @@ struct Sexp {
|
|||
*/
|
||||
size_t empty() const { return seq_.empty(); }
|
||||
|
||||
private:
|
||||
private:
|
||||
friend struct Sexp;
|
||||
Seq seq_;
|
||||
};
|
||||
|
@ -309,16 +342,14 @@ struct Sexp {
|
|||
return is_prop_list(list().begin() + 1, list().end());
|
||||
}
|
||||
|
||||
private:
|
||||
Sexp(Type typearg, std::string&& valuearg) : type_{typearg}, value_{std::move(valuearg)}
|
||||
{
|
||||
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)}
|
||||
{
|
||||
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())
|
||||
|
@ -353,10 +384,10 @@ struct Sexp {
|
|||
return b == e;
|
||||
}
|
||||
|
||||
const Type type_; /**< Type of node */
|
||||
const std::string value_; /**< String value of node (only for
|
||||
* non-Type::Lst)*/
|
||||
const Seq seq_; /**< Children of node (only for
|
||||
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) */
|
||||
};
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ test_parser()
|
|||
|
||||
check_parse(R"("foo
|
||||
bar")",
|
||||
"\"foo\nbar\"");
|
||||
"\"foo\nbar\"");
|
||||
}
|
||||
|
||||
static void
|
||||
|
@ -111,20 +111,47 @@ static void
|
|||
test_props()
|
||||
{
|
||||
auto sexp2 = Sexp::make_list(Sexp::make_string("foo"),
|
||||
Sexp::make_number(123),
|
||||
Sexp::make_symbol("blub"));
|
||||
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));
|
||||
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))");
|
||||
"(: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
|
||||
|
@ -140,6 +167,7 @@ try {
|
|||
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();
|
||||
|
|
|
@ -374,7 +374,12 @@ static bool
|
|||
output_sexp(const Option<Message>& msg, const OutputInfo& info, const MuConfig* opts, GError** err)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -476,7 +481,7 @@ output_query_results(const QueryResults& qres, const MuConfig* opts)
|
|||
if (!msg)
|
||||
continue;
|
||||
|
||||
if (opts->after != 0 && msg->mtime() < opts->after)
|
||||
if (opts->after != 0 && msg->changed() < opts->after)
|
||||
continue;
|
||||
|
||||
rv = output_func(msg,
|
||||
|
|
Loading…
Reference in New Issue