rpc: introduce HelpElision variant and ElideGroup helper

Switch RPCResultOptions::print_elision to a variant
<HelpElisionNone, HelpElisionSkip, std::string> and add a constructor
that copies a result with replacement options.

Use the new ElideGroup() in TxDoc() to apply elision to field groups
instead of setting print_elision per field. Type::ELISION stays as a
deprecated alias.
This commit is contained in:
Renato Britto
2026-03-16 19:43:21 -03:00
committed by satsfy (Renato Britto)
parent 47da4f9b71
commit 44fc3a290d
3 changed files with 71 additions and 29 deletions

View File

@@ -346,16 +346,14 @@ void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const
std::vector<RPCResult> TxDoc(const TxDocOptions& opts)
{
std::optional<std::string> maybe_skip{};
if (opts.elision_description) maybe_skip.emplace();
return {
{RPCResult::Type::STR_HEX, "txid", opts.txid_field_doc, {}, {.print_elision=opts.elision_description}},
{RPCResult::Type::STR_HEX, "hash", "The transaction hash (differs from txid for witness transactions)", {}, {.print_elision=maybe_skip}},
{RPCResult::Type::NUM, "size", "The serialized transaction size", {}, {.print_elision=maybe_skip}},
{RPCResult::Type::NUM, "vsize", "The virtual transaction size (differs from size for witness transactions)", {}, {.print_elision=maybe_skip}},
{RPCResult::Type::NUM, "weight", "The transaction's weight (between vsize*4-3 and vsize*4)", {}, {.print_elision=maybe_skip}},
{RPCResult::Type::NUM, "version", "The version", {}, {.print_elision=maybe_skip}},
{RPCResult::Type::NUM_TIME, "locktime", "The lock time", {}, {.print_elision=maybe_skip}},
auto fields = std::vector<RPCResult>{
{RPCResult::Type::STR_HEX, "txid", opts.txid_field_doc},
{RPCResult::Type::STR_HEX, "hash", "The transaction hash (differs from txid for witness transactions)"},
{RPCResult::Type::NUM, "size", "The serialized transaction size"},
{RPCResult::Type::NUM, "vsize", "The virtual transaction size (differs from size for witness transactions)"},
{RPCResult::Type::NUM, "weight", "The transaction's weight (between vsize*4-3 and vsize*4)"},
{RPCResult::Type::NUM, "version", "The version"},
{RPCResult::Type::NUM_TIME, "locktime", "The lock time"},
{RPCResult::Type::ARR, "vin", "",
{
{RPCResult::Type::OBJ, "", "",
@@ -374,7 +372,7 @@ std::vector<RPCResult> TxDoc(const TxDocOptions& opts)
}},
{RPCResult::Type::NUM, "sequence", "The script sequence number"},
}},
}, {.print_elision=maybe_skip}},
}},
{RPCResult::Type::ARR, "vout", "",
{
{RPCResult::Type::OBJ, "", "", Cat(
@@ -383,11 +381,16 @@ std::vector<RPCResult> TxDoc(const TxDocOptions& opts)
{RPCResult::Type::NUM, "n", "index"},
{RPCResult::Type::OBJ, "scriptPubKey", "", ScriptPubKeyDoc()},
},
opts.wallet ?
opts.wallet ?
std::vector<RPCResult>{{RPCResult::Type::BOOL, "ischange", /*optional=*/true, "Output script is change (only present if true)"}} :
std::vector<RPCResult>{}
)
},
}, {.print_elision=maybe_skip}},
)},
}},
};
if (opts.elision_description) {
fields = ElideGroup(std::move(fields), *opts.elision_description);
}
return fields;
}

View File

@@ -1017,16 +1017,25 @@ void RPCResult::ToSections(Sections& sections, const OuterType outer_type, const
// Ensure at least one elision description exists, if there is any elision
const auto elision_has_description{[](const std::vector<RPCResult>& inner) {
return std::ranges::none_of(inner, [](const auto& res) { return res.m_opts.print_elision.has_value(); }) ||
std::ranges::any_of(inner, [](const auto& res) { return res.m_opts.print_elision.has_value() && !res.m_opts.print_elision->empty(); });
const auto is_elided = [](const RPCResult& res) {
return !std::holds_alternative<HelpElisionNone>(res.m_opts.print_elision);
};
const auto has_summary_text = [](const RPCResult& res) {
const auto* text = std::get_if<std::string>(&res.m_opts.print_elision);
return text && !text->empty();
};
return std::ranges::none_of(inner, is_elided) || std::ranges::any_of(inner, has_summary_text);
}};
if (m_opts.print_elision) {
if (!m_opts.print_elision->empty()) {
sections.PushSection({indent + "..." + maybe_separator, *m_opts.print_elision});
if (const auto* text = std::get_if<std::string>(&m_opts.print_elision)) {
if (!text->empty()) {
sections.PushSection({indent + "..." + maybe_separator, *text});
}
return;
}
if (std::holds_alternative<HelpElisionSkip>(m_opts.print_elision)) {
return;
}
switch (m_type) {
case Type::ELISION: {
@@ -1418,3 +1427,20 @@ uint256 GetTarget(const CBlockIndex& blockindex, const uint256 pow_limit)
arith_uint256 target{*CHECK_NONFATAL(DeriveTarget(blockindex.nBits, pow_limit))};
return ArithToUint256(target);
}
std::vector<RPCResult> ElideGroup(std::vector<RPCResult> fields, std::string summary)
{
if (fields.empty()) return fields;
std::vector<RPCResult> result;
result.reserve(fields.size());
for (size_t i = 0; i < fields.size(); ++i) {
RPCResultOptions opts = fields[i].m_opts;
if (i == 0) {
opts.print_elision = summary;
} else {
opts.print_elision = HelpElisionSkip{};
}
result.emplace_back(fields[i], std::move(opts));
}
return result;
}

View File

@@ -292,18 +292,17 @@ struct RPCArg {
std::string ToDescriptionString(bool is_named_arg) const;
};
/// Controls how an RPCResult is rendered in human-readable help text.
/// The std::string alternative carries the summary text rendered as "...".
struct HelpElisionNone {}; //!< field printed normally
struct HelpElisionSkip {}; //!< field hidden from help
using HelpElision = std::variant<HelpElisionNone, HelpElisionSkip, std::string>;
struct RPCResultOptions {
bool skip_type_check{false};
/// Whether to treat this as elided in the human-readable description, and
/// possibly supply a description for the elision. Normally, there will be
/// one string on any of the elided results, for example `Same output as
/// verbosity = 1`, and all other elided strings will be empty.
///
/// - If nullopt: normal display.
/// - If empty string: suppress from help.
/// - If non-empty: show "..." with this description.
std::optional<std::string> print_elision{std::nullopt};
HelpElision print_elision{HelpElisionNone{}};
};
// NOLINTNEXTLINE(misc-no-recursion)
struct RPCResult {
enum class Type {
@@ -385,6 +384,16 @@ struct RPCResult {
RPCResultOptions opts = {})
: RPCResult{type, std::move(m_key_name), /*optional=*/false, std::move(description), std::move(inner), std::move(opts)} {}
/// Copy with replacement options, for stamping new opts onto an existing result.
RPCResult(const RPCResult& other, RPCResultOptions opts)
: m_type{other.m_type},
m_key_name{other.m_key_name},
m_inner{other.m_inner},
m_optional{other.m_optional},
m_opts{std::move(opts)},
m_description{other.m_description},
m_cond{other.m_cond} {}
/** Append the sections of the result. */
void ToSections(Sections& sections, OuterType outer_type = OuterType::NONE, int current_indent = 0) const;
/** Return the type string of the result when it is in an object (dict). */
@@ -400,6 +409,10 @@ private:
void CheckInnerDoc() const;
};
/// Stamp elision onto an entire vector of RPCResult fields at once.
/// Merges into existing m_opts so that flags like skip_type_check are preserved.
std::vector<RPCResult> ElideGroup(std::vector<RPCResult> fields, std::string summary = "");
struct RPCResults {
const std::vector<RPCResult> m_results;