summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDirk-Jan C. Binnema <djcb@djcbsoftware.nl>2025-07-27 09:17:55 +0300
committerDirk-Jan C. Binnema <djcb@djcbsoftware.nl>2025-08-15 21:02:24 +0300
commit552bb3a7c82fc7472ab8d3a5a0fd32fd9ea72909 (patch)
treecca09a480b86affefd272bd0b4b2076f031659fe
parent34d3bf2e28138f31faaed55ba98c3088449c9fdf (diff)
message: add support for labels + tests
Labels are strings associated with messages, which can be used for searching them.
-rw-r--r--lib/message/meson.build3
-rw-r--r--lib/message/mu-document.hh7
-rw-r--r--lib/message/mu-fields.hh22
-rw-r--r--lib/message/mu-labels.cc244
-rw-r--r--lib/message/mu-labels.hh88
-rw-r--r--lib/message/mu-message.cc7
-rw-r--r--lib/message/mu-message.hh23
-rw-r--r--lib/message/tests/meson.build7
8 files changed, 390 insertions, 11 deletions
diff --git a/lib/message/meson.build b/lib/message/meson.build
index 006bb18..31052b8 100644
--- a/lib/message/meson.build
+++ b/lib/message/meson.build
@@ -1,4 +1,4 @@
-## Copyright (C) 2022-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+## Copyright (C) 2022-2025 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
@@ -25,6 +25,7 @@ lib_mu_message=static_library(
'mu-document.cc',
'mu-fields.cc',
'mu-flags.cc',
+ 'mu-labels.cc',
'mu-priority.cc',
'mu-mime-object.cc',
],
diff --git a/lib/message/mu-document.hh b/lib/message/mu-document.hh
index 5119044..9702562 100644
--- a/lib/message/mu-document.hh
+++ b/lib/message/mu-document.hh
@@ -1,4 +1,4 @@
-/** Copyright (C) 2022-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+/** Copyright (C) 2022-2025 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
@@ -28,6 +28,7 @@
#include "mu-priority.hh"
#include "mu-flags.hh"
#include "mu-contact.hh"
+#include "mu-labels.hh"
#include <utils/mu-option.hh>
#include <utils/mu-sexp.hh>
@@ -102,7 +103,6 @@ public:
*/
void add(Field::Id field_id, const std::vector<std::string>& vals);
-
/**
* Add message-contacts to the document, if non-empty
*
@@ -139,12 +139,13 @@ public:
/**
- * Add message flags to the document
+ * Add message flags to the document
*
* @param flags mesage flags.
*/
void add(Flags flags);
+
/**
* Remove values and terms for some field.
*
diff --git a/lib/message/mu-fields.hh b/lib/message/mu-fields.hh
index 62c79e0..e17f271 100644
--- a/lib/message/mu-fields.hh
+++ b/lib/message/mu-fields.hh
@@ -1,5 +1,5 @@
/*
-** Copyright (C) 2022-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+** Copyright (C) 2022-2025 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
@@ -65,6 +65,10 @@ struct Field {
Tags, /**< Message Tags */
ThreadId, /**< Thread Id */
To, /**< To: recipient */
+
+ // XXX: re-order when we update the db-schema.
+ Labels, /**< Labels */
+
//
_count_ /**< Number of Ids */
};
@@ -462,6 +466,19 @@ static constexpr std::array<Field, Field::id_size()>
Field::Flag::NormalTerm |
Field::Flag::PhrasableTerm,
},
+ {
+ Field::Id::Labels,
+ Field::Type::StringList,
+ "labels", "label",
+ "Message label(s)",
+ "label:projectx",
+ 'q',
+ Field::Flag::BooleanTerm |
+ Field::Flag::Value |
+ Field::Flag::IncludeInSexp
+,
+ },
+
}};
/*
@@ -476,8 +493,7 @@ static constexpr std::array<Field, Field::id_size()>
* @return ref of the message field.
*/
constexpr const Field&
-field_from_id(Field::Id id)
-{
+field_from_id(Field::Id id) {
return Fields.at(static_cast<size_t>(id));
}
diff --git a/lib/message/mu-labels.cc b/lib/message/mu-labels.cc
new file mode 100644
index 0000000..3f800eb
--- /dev/null
+++ b/lib/message/mu-labels.cc
@@ -0,0 +1,244 @@
+/*
+** Copyright (C) 2025 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-labels.hh"
+#include <set>
+#include <algorithm>
+
+using namespace Mu;
+using namespace Mu::Labels;
+
+
+Result<void>
+Mu::Labels::validate_label(const std::string &label)
+{
+ if (label.empty())
+ return Err(Error{Error::Code::InvalidArgument,
+ "labels cannot be empty"});
+ else if (!g_utf8_validate(label.c_str(), label.size(), {})) // perhpps put hex in err str?
+ return Err(Error{Error::Code::InvalidArgument,
+ "labels must be valid UTF-8"});
+
+ const auto cstr{label.c_str()};
+
+ // labels must be at least two characters and not start with a
+ // dash. these limitations are there to avoid confusion with
+ // command-line parameters.
+ if (cstr[0] == '-' || cstr[0] == '+')
+ return Err(Error{Error::Code::InvalidArgument,
+ "labels cannot start with '+' or '-' ({})", label});
+
+ for (auto cur = cstr; cur && *cur; cur = g_utf8_next_char(cur)) {
+
+ const gunichar uc = g_utf8_get_char(cur);
+ if (g_unichar_isalnum(uc))
+ continue; // alphanum is okay
+
+ // almost all non-ctrl ascii is allowed _except_ =,<,>,$,[]
+ if (uc > ' ' && uc <= '~') {
+ switch (uc) {
+ case '"':
+ case '/':
+ case '\\':
+ case '*':
+ case '$':
+ return Err(Error{Error::Code::InvalidArgument,
+ "illegal character '{}' in label ({})", uc, label});
+ default:
+ break;
+ }
+ } else
+ return Err(Error{Error::Code::InvalidArgument,
+ "illegal non alpha-numeric character '{}' in label ({})",
+ uc, label});
+ }
+
+ return Ok();
+}
+
+Result<DeltaLabel>
+Mu::Labels::parse_delta_label(const std::string &expr)
+{
+ if (expr.size() < 1)
+ return Err(Error{Error::Code::InvalidArgument,
+ "empty labels are invalid"});
+ const auto cstr{expr.c_str()};
+
+ // first char; either '+' or '-'
+ if (cstr[0] != '+' && cstr[0] != '-')
+ return Err(Error{Error::Code::InvalidArgument,
+ "invalid label expression '{}'; "
+ "must start with '+' or '-'",
+ expr});
+ Delta delta{cstr[0] == '+' ? Delta::Add : Delta::Remove};
+ std::string label{expr.substr(1)};
+
+ if (const auto res = validate_label(label); !res)
+ return Err(res.error());
+
+ return Ok(DeltaLabel{std::move(delta), std::move(label)});
+}
+
+std::pair<LabelVec, DeltaLabelVec>
+Mu::Labels::updated_labels(const LabelVec& labels, const DeltaLabelVec& deltas)
+{
+ // quite complicated!
+
+ // First, the delta; put in a set for uniqueness; and use a special
+ // comparison operator so "add" and "remove" deltas are considered "the same"
+ // for the set; then fill the set from the end of the deltas vec to the begining,
+ // so "the last one wins", as we want.
+ const auto cmp_delta_label=[](const DeltaLabel& dl1, const DeltaLabel& dl2) {
+ return dl1.second < dl2.second;
+ };
+ // only one change per label, last one wins
+ std::set<DeltaLabel, decltype(cmp_delta_label)> working_deltas{
+ deltas.rbegin(), deltas.rend()
+ };
+
+ // working set of lables; we start with _all_ (uniquified)
+ std::set<std::string> working_labels{labels.begin(), labels.end()};
+
+ // keep track of the deltas that actually changed something (ie.
+ // removing a non-existing label or adding an already existing one is
+ // not a change.)
+ DeltaLabelVec effective_deltas;
+
+ // now check each of our "workin deltas", apply on the working_labels, and
+ // if they changed anything, add to 'effectivc_deltas
+ for (auto& delta: working_deltas) {
+ switch (delta.first) {
+ case Delta::Add:
+ // add to the _effective_ deltas if the element wasn't
+ // there before.
+ if (working_labels.emplace(delta.second).second)
+ effective_deltas.emplace_back(std::move(delta));
+ break;
+ case Delta::Remove:
+ // add to the _effective_ deltas if the element was
+ // actually removed.
+ if (working_labels.erase(delta.second) > 0U)
+ effective_deltas.emplace_back(std::move(delta));
+ break;
+ default:
+ // can't have Neutral here.
+ throw std::runtime_error("invalid delta");
+ }
+ }
+
+
+ return {{ working_labels.begin(), working_labels.end()}, effective_deltas};
+}
+
+
+
+#ifdef BUILD_TESTS
+
+#include "utils/mu-test-utils.hh"
+
+static void
+test_parse_delta_label()
+{
+
+ {
+ const auto expr = parse_delta_label("+foo");
+ assert_valid_result(expr);
+ g_assert_true(expr->first == Delta::Add);
+ assert_equal(expr->second, "foo");
+ }
+
+
+ {
+ const auto expr = parse_delta_label("-bar@cuux");
+ assert_valid_result(expr);
+ g_assert_true(expr->first == Delta::Remove);
+ assert_equal(expr->second, "bar@cuux");
+ }
+
+ g_assert_false(!!parse_delta_label("ravenking"));
+ g_assert_false(!!parse_delta_label("+norrell strange"));
+ g_assert_false(!!parse_delta_label("-😨"));
+}
+
+static void
+test_validate_label()
+{
+ g_assert_true(!!validate_label("ravenking"));
+ g_assert_true(!!validate_label("@raven+king"));
+ g_assert_true(!!validate_label("operation:mindcrime"));
+
+ g_assert_false(!!validate_label("norrell strange"));
+ g_assert_false(!!validate_label("😨"));
+ g_assert_false(!!validate_label(""));
+ g_assert_false(!!validate_label("+"));
+ g_assert_false(!!validate_label("-"));
+}
+
+static void
+test_updated_labels()
+{
+ const auto assert_eq=[](const LabelVec& labels, const DeltaLabelVec& deltas,
+ const LabelVec& exp_labels, const DeltaLabelVec& exp_deltas) {
+
+ const auto& [res_labels, res_deltas] = updated_labels(labels, deltas);
+
+ assert_equal_seq_str(res_labels, exp_labels);
+ g_assert_cmpuint(res_deltas.size(), ==, exp_deltas.size());
+ for (size_t i{}; i != res_deltas.size(); ++i) {
+ g_assert_true(res_deltas[i].first == exp_deltas[i].first);
+ assert_equal(res_deltas[i].second, exp_deltas[i].second);
+ }
+ };
+
+ const auto delta_labels = [](std::initializer_list<std::string> strs)->DeltaLabelVec {
+ DeltaLabelVec deltas;
+ std::transform(strs.begin(), strs.end(), std::back_inserter(deltas),
+ [](auto str) {
+ const auto res = parse_delta_label(str);
+ assert_valid_result(res);
+ return *res;
+ });
+ return deltas;
+ };
+
+ assert_eq({"foo", "bar", "cuux"}, delta_labels({"+fnorb", "+bar", "-bar", "+bar", "-cuux"}),
+ {"bar", "fnorb", "foo"}, delta_labels({"-cuux", "+fnorb"}));
+
+ assert_eq({}, delta_labels({"-fnorb", "-fnorb", "+whiteward", "+altesia", "+fnorb"}),
+ {"altesia", "fnorb", "whiteward"}, delta_labels({"+altesia", "+fnorb", "+whiteward"}));
+
+
+ assert_eq({"piranesi", "hyperion", "mordor", "piranesi"}, delta_labels({}),
+ {"hyperion", "mordor", "piranesi"}, delta_labels({}));
+}
+
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/message/labels/parse-delta-label", test_parse_delta_label);
+ g_test_add_func("/message/labels/validate-label", test_validate_label);
+ g_test_add_func("/message/labels/updated-labels", test_updated_labels);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
diff --git a/lib/message/mu-labels.hh b/lib/message/mu-labels.hh
new file mode 100644
index 0000000..7971b96
--- /dev/null
+++ b/lib/message/mu-labels.hh
@@ -0,0 +1,88 @@
+/*
+** Copyright (C) 2025 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_LABELS_HH
+#define MU_LABELS_HH
+
+#include <utils/mu-result.hh>
+
+#include <string>
+#include <vector>
+#include <utility>
+
+namespace Mu {
+namespace Labels {
+
+using LabelVec = std::vector<std::string>;
+
+enum struct Delta { Add='+', Remove='-'};
+using DeltaLabel = std::pair<Delta, std::string>;
+using DeltaLabelVec = std::vector<DeltaLabel>;
+
+/**
+ * Parse a label expression, i.e., a label prefixed with '+' or '-'
+ *
+ * This also validates the label, as per valid_label()
+ *
+ * @param expr expression
+ *
+ * @return a result with either a DeltaLabel or an error
+ */
+Result<DeltaLabel> parse_delta_label(const std::string& expr);
+
+/**
+ * Is the label (without +/- prefix) valid?
+ *
+ * @param label some label
+ *
+ * @return either Ok or some error
+ */
+Result<void> validate_label(const std::string &label);
+
+/**
+ * Apply deltas to labels and return the result as well as the
+ * effective changes.
+ *
+ * The deltas are handled in order; 'last one wins', hence:
+ * { +foo, -foo } ==> no foo in the result
+ * and
+ * { -foo, +foo } ==> foo in results
+ *
+ * The result labels do not contain duplicates. Order is not necessarily
+ * maintained.
+ *
+ * The result is a pair, the first element is LabelVec with the results
+ * as explained.
+ *
+ * The second is a DeltaVec with the _effective_ changes; this the input
+ * DeltaVec but without any +<label> if label was already in labels; and without
+ * -<label> if <label> was not in labels.
+ *
+ * @param labels existing labels
+ * @param deltas deltas for these labels
+ *
+ * @return updated labels
+ */
+std::pair<LabelVec, DeltaLabelVec> updated_labels(const LabelVec& labels, const DeltaLabelVec& delta);
+
+} // Labels
+} // Mu
+
+
+#endif /*MU_LABELS_HH*/
diff --git a/lib/message/mu-message.cc b/lib/message/mu-message.cc
index 339abd1..2d7c17f 100644
--- a/lib/message/mu-message.cc
+++ b/lib/message/mu-message.cc
@@ -240,6 +240,13 @@ Message::set_flags(Flags flags)
priv_->doc.add(flags);
}
+void
+Message::set_labels(const Labels::LabelVec& labels)
+{
+ priv_->doc.remove(Field::Id::Labels);
+ priv_->doc.add(Field::Id::Labels, labels);
+}
+
bool
Message::load_mime_message(bool reload) const
{
diff --git a/lib/message/mu-message.hh b/lib/message/mu-message.hh
index c6b6917..0202558 100644
--- a/lib/message/mu-message.hh
+++ b/lib/message/mu-message.hh
@@ -31,6 +31,7 @@
#include "mu-priority.hh"
#include "mu-flags.hh"
#include "mu-fields.hh"
+#include "mu-labels.hh"
#include "mu-document.hh"
#include "mu-message-part.hh"
#include "mu-message-file.hh"
@@ -342,15 +343,29 @@ public:
}
/**
- * get the list of tags (ie., X-Label)
+ * Get the labels for this message
+ *
+ * @return a list with the tags for this msg. Don't modify/free
+ */
+ Labels::LabelVec labels() const {
+ return document().string_vec_value(Field::Id::Labels);
+ }
+
+ /**
+ * Set the labels for this message, removing the existing ones.
*
- * @param msg a valid MuMsg
+ * @param labels the new labels
+ */
+ void set_labels(const Labels::LabelVec& labels);
+
+
+ /**
+ * get the list of tags (ie., X-Label)
*
* @return a list with the tags for this msg. Don't modify/free
*/
std::vector<std::string> tags() const {
- return document()
- .string_vec_value(Field::Id::Tags);
+ return document().string_vec_value(Field::Id::Tags);
}
/*
diff --git a/lib/message/tests/meson.build b/lib/message/tests/meson.build
index 880a188..7dfbdd4 100644
--- a/lib/message/tests/meson.build
+++ b/lib/message/tests/meson.build
@@ -42,6 +42,13 @@ test('test-fields',
cpp_args: ['-DBUILD_TESTS'],
dependencies: [glib_dep, gmime_dep, lib_mu_message_dep]))
+test('test-labels',
+ executable('test-labels',
+ '../mu-labels.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_utils_dep]))
+
test('test-flags',
executable('test-flags',
'../mu-flags.cc',