mu-server: cleanup find prop handling

Rework the overly long find-handler.

Improve parsing of options, and return better return properties (for use
in mu4e).
This commit is contained in:
Dirk-Jan C. Binnema
2025-03-16 18:38:09 +02:00
parent e534f8ae79
commit a2a1838da4
2 changed files with 95 additions and 43 deletions

View File

@ -640,61 +640,121 @@ Server::Private::output_results(const QueryResults& qres, size_t batch_size) con
return n; return n;
} }
void struct FindProps {
Server::Private::find_handler(const Command& cmd) std::string query;
{ int batch_size{200};
const auto q{cmd.string_arg(":query").value_or("")}; int maxnum{-1};
const auto threads{cmd.boolean_arg(":threads")}; Field::Id sort_field_id{Field::Id::Date};
// perhaps let mu4e set this as frame-lines of the appropriate frame. QueryFlags flags{QueryFlags::SkipUnreadable};
const auto batch_size{cmd.number_arg(":batch-size").value_or(200)}; };
const auto descending{cmd.boolean_arg(":descending")}; // XXX: once we move to C++20, use designated initializers
const auto maxnum{cmd.number_arg(":maxnum").value_or(-1) /*unlimited*/};
const auto skip_dups{cmd.boolean_arg(":skip-dups")};
const auto include_related{cmd.boolean_arg(":include-related")};
static const std::pair<QueryFlags, std::string> flags_props[] = {
{ QueryFlags::Descending, ":descending" },
{ QueryFlags::SkipDuplicates, ":skip-dups" },
{ QueryFlags::IncludeRelated, ":include-related" },
{ QueryFlags::Threading, ":threads" }
};
/**
* Determine the find search properties from a command object
*
* @param cmd the command
*
* @return structure with find properties
*/
static FindProps
determine_find_props(const Command& cmd)
{
FindProps props{};
props.query = cmd.string_arg(":query").value_or("");
props.batch_size = cmd.number_arg(":batch-size").value_or(200);
if (props.batch_size < 1)
throw Error{Error::Code::InvalidArgument, "invalid batch-size {}", props.batch_size};
props.maxnum = cmd.number_arg(":maxnum").value_or(-1) /*unlimited*/;
if (props.maxnum < -1)
throw Error{Error::Code::InvalidArgument, "invalid max-num {}", props.maxnum};
const auto threads{cmd.boolean_arg(":threads")};
// complicated! // complicated!
auto sort_field_id = std::invoke([&]()->Field::Id { props.sort_field_id = std::invoke([&]()->Field::Id {
if (const auto arg = cmd.symbol_arg(":sortfield"); !arg) if (const auto arg = cmd.symbol_arg(":sortfield"); !arg)
return Field::Id::Date; return Field::Id::Date;
else if (arg->length() < 2) else if (arg->length() < 2)
throw Error{Error::Code::InvalidArgument, "invalid sort field '{}'", throw Error{Error::Code::InvalidArgument, "invalid sort field parameter '{}'",
*arg}; *arg};
else if (const auto field{field_from_name(arg->substr(1))}; !field) else if (const auto field{field_from_name(arg->substr(1))}; !field)
// Note: mu4e gives us ':date' etc., hence the 'substr'
throw Error{Error::Code::InvalidArgument, "invalid sort field '{}'", throw Error{Error::Code::InvalidArgument, "invalid sort field '{}'",
*arg}; *arg};
else if (!field->is_sortable())
throw Error{Error::Code::InvalidArgument, "not a sortable field '{}'",
*arg};
else if (threads && field->id != Field::Id::Date)
throw Error{Error::Code::InvalidArgument,
"with threads, you can only sort by date, not by '{}'", *arg};
else else
return field->id; return field->id;
}); });
if (batch_size < 1) for (const auto& item: flags_props) {
throw Error{Error::Code::InvalidArgument, "invalid batch-size {}", batch_size}; if (cmd.boolean_arg(item.second))
props.flags |= item.first;
}
auto qflags{QueryFlags::SkipUnreadable}; // don't show unreadables. return props;
}
/**
* Determine the result properties from the find results
*
* The result properties are passed back to the client
*
* @param props the find properties
*
* @return the result properties
*/
static Sexp
determine_result_props(const FindProps& props, size_t found)
{
Sexp resprops; Sexp resprops;
if (descending) {
qflags |= QueryFlags::Descending; resprops.put_props(
resprops.put_props(":reverse", Sexp::t_sym); ":found", found,
} ":query", props.query,
if (skip_dups) { ":query-sexp", parse_query(props.query, false/*!expand*/),
qflags |= QueryFlags::SkipDuplicates; ":query-sexp-expanded", parse_query(props.query, true/*expand*/),
resprops.put_props(":skip-dups", Sexp::t_sym); ":maxnum", props.maxnum,
} // mu4e expects ':'-prefixed sort-field, for historical reasons.
if (include_related) { ":sort-field", Sexp::Symbol{mu_format(":{}", field_from_id(props.sort_field_id).name)});
qflags |= QueryFlags::IncludeRelated;
resprops.put_props(":include-related", Sexp::t_sym); if (props.maxnum == -1)
} resprops.put_props(":full", Sexp::t_sym);
if (threads) {
qflags |= QueryFlags::Threading; for (const auto& item: flags_props) {
resprops.put_props(":threads", Sexp::t_sym); if (any_of(props.flags & item.first))
resprops.put_props(item.second, Sexp::t_sym);
} }
return resprops;
}
void
Server::Private::find_handler(const Command& cmd)
{
const auto props = determine_find_props(cmd);
StopWatch sw{mu_format("{} (indexing: {})", __func__, StopWatch sw{mu_format("{} (indexing: {})", __func__,
indexer().is_running() ? "yes" : "no")}; indexer().is_running() ? "yes" : "no")};
// we need to _lock_ the store while querying (which likely consists of // we need to _lock_ the store while querying (which likely consists of
// multiple actual queries) + grabbing the results. // multiple actual queries) + grabbing the results.
std::lock_guard l{store_.lock()}; std::lock_guard l{store_.lock()};
auto qres{store_.run_query(q, sort_field_id, qflags, maxnum)}; auto qres{store_.run_query(props.query, props.sort_field_id,
props.flags, props.maxnum)};
if (!qres) if (!qres)
throw Error(Error::Code::Query, "failed to run query: {}", qres.error().what()); throw Error(Error::Code::Query, "failed to run query: {}", qres.error().what());
@ -702,15 +762,10 @@ Server::Private::find_handler(const Command& cmd)
* 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_sym)); output_sexp(Sexp().put_props(":erase", Sexp::t_sym));
const auto bsize{static_cast<size_t>(batch_size)}; const auto bsize{static_cast<size_t>(props.batch_size)};
const auto foundnum = output_results(*qres, bsize); const auto foundnum = output_results(*qres, bsize);
output_sexp(resprops.put_props( output_sexp(determine_result_props(props, foundnum));
":found", foundnum,
":query", q,
":query-sexp", parse_query(q, false/*!expand*/).to_string(),
":query-sexp-expanded", parse_query(q, true/*expand*/).to_string(),
":maxnum", maxnum));
} }
void void

View File

@ -22,15 +22,12 @@
#include "mu-utils.hh" #include "mu-utils.hh"
#include <stdexcept>
#include <vector> #include <vector>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <iostream> #include <iostream>
#include <variant> #include <variant>
#include <cinttypes>
#include <ostream> #include <ostream>
#include <cassert>
#include <utils/mu-result.hh> #include <utils/mu-result.hh>
#include <utils/mu-option.hh> #include <utils/mu-option.hh>
@ -55,7 +52,7 @@ struct Sexp {
Symbol(const std::string& s): name{s} {} Symbol(const std::string& s): name{s} {}
Symbol(std::string&& s): name(std::move(s)) {} Symbol(std::string&& s): name(std::move(s)) {}
Symbol(const char* str): Symbol(std::string{str}) {} Symbol(const char* str): Symbol(std::string{str}) {}
Symbol(std::string_view sv): Symbol(std::string{sv}) {} Symbol(const std::string_view& sv): Symbol(std::string{sv}) {}
operator const std::string&() const {return name; } operator const std::string&() const {return name; }
std::string name; std::string name;