xo-umbrella2/xo-ppsink/include/xo/ppsink/pretty_struct.hpp
2026-08-09 17:12:39 -04:00

258 lines
9.9 KiB
C++

/** @file pretty_struct.hpp
*
* @author Roland Conybeare, Aug 2026
*
* sink.pretty_struct("Name", fields...): render a struct-shaped value as
*
* <Name :f1 v1 :f2 v2>
*
* breaking, when it does not fit the margin, to
*
* <Name
* :f1 v1
* :f2 v2>
*
* The ppsink replacement for legacy xo::print::ppstate::pretty_struct
* (xo/indentlog/print/pretty.hpp). Two differences from the legacy verb:
*
* 1. no trial-fit pass. Legacy print_pretty() was called twice -- once with
* ppii.upto() true to see whether the group fit, then again to emit. Here
* the caller emits ONE begin/split/end token stream and PpState decides
* downstream whether each group breaks. All upto() branching disappears.
*
* 2. returns void, not bool.
*
* Own header (not folded into pretty.hpp / Prettifier.hpp) because this is a
* layout *vocabulary* built on top of the dispatch verb, not part of it: a
* Prettifier author includes this only when the type is struct-shaped.
* Follows the pattern of PpSink::pp / dwim / operator(), which are likewise
* declared in PpSink.hpp and defined in the header that supplies them.
**/
#pragma once
#include "tag.hpp" /* Prettifier, PpSink, pretty(), color_guard */
#include <concepts>
#include <string_view>
namespace xo::pp {
// ------------------------------------------------------------------ field
/** @brief one named field of a struct, holding its value BY REFERENCE.
*
* The ppsink counterpart to legacy xo::refrtag. Two differences from
* xo::pp::tag:
*
* 1. by reference, not by value. tag() decays and copies its value (see
* tag.hpp), which for a std::string member means a heap allocation on
* every print, and for a move-only member is a compile error. A
* struct printer names *members*, so a reference is right.
*
* LIFETIME: @p value must outlive the pretty_struct() call. Binding a
* prvalue directly in the call expression is safe -- the temporary
* lives to the end of the full-expression -- but is easy to break when
* refactoring, so prefer passing a member or a named local. In
* particular, an accessor that returns BY VALUE (e.g. `metatype()`)
* wants a named local.
*
* 2. carries a @p present flag, so a field can be omitted entirely
* (legacy refrtag's third argument).
*
* Anything renderable can be a field: pretty_struct() prints each field
* through pretty(), and only *consults* present() when the field type has
* one. So tag("k", v) is a perfectly good field when a copy is wanted.
**/
template <typename Name, typename Value>
class field_impl {
public:
field_impl(const Name & name, const Value & value, bool present)
: name_{&name}, value_{&value}, present_{present} {}
const Name & name() const { return *name_; }
const Value & value() const { return *value_; }
bool present() const { return present_; }
private:
/** @p Name is usually a string literal or std::string_view, but any
* renderable will do -- notably concat() (concat.hpp), which composes
* a label without building a std::string for it.
**/
const Name * name_;
const Value * value_;
bool present_ = true;
};
/** ":name value" naming a struct member; @p value captured BY REFERENCE
* (see field_impl for the lifetime rule). Omitted from the output
* entirely when @p present is false.
**/
template <typename Name, typename Value>
field_impl<Name, Value>
field(const Name & name, const Value & value, bool present = true) {
return field_impl<Name, Value>(name, value, present);
}
/** render ":name value" -- same shape as Prettifier<tag_impl>, without the
* copy, and without tag's optional leading space (pretty_struct emits the
* separator itself).
**/
template <typename Name, typename Value>
struct Prettifier<field_impl<Name, Value>> {
static void print(PpSink & sink, const field_impl<Name, Value> & f) {
sink.begin(0);
{
/* color just the ":name" (value keeps its own color/structure).
* struct_tag_color, NOT tag_color: a struct field and a log tag
* are deliberately distinguishable (PpStyle.hpp).
*/
color_guard g(sink, sink.style().struct_tag_color);
sink.put(":");
/* pp(), not put(): a name may be any renderable (e.g. concat).
* For a string_view or literal this is the same string-like
* leaf that put() would have taken.
*/
sink.pp(f.name());
}
sink.split(1, sink.style().tag_value_offset);
sink.pp(f.value());
sink.end();
}
};
// ---------------------------------------------------------- pretty_struct
namespace detail {
/** true iff F reports its own presence (legacy xo::has_present).
*
* Deliberately NOT named has_present: xo::pp nests inside xo, so an
* unqualified use would also find legacy ::xo::has_present when
* <xo/indentlog/print/tag.hpp> is in the same translation unit.
**/
template <typename F>
concept has_present_flag = requires (const F & f) {
{ f.present() } -> std::convertible_to<bool>;
};
} /*namespace detail*/
/** @brief RAII scope emitting <Name :f1 v1 :f2 v2> one field at a time.
*
* The single place the struct token stream is produced: pretty_struct()
* is a thin variadic wrapper over this, so the four shape rules below are
* stated once. Reach for struct_scope directly only when the field count
* is a runtime value -- a loop cannot contribute to a variadic pack.
*
* Emits, in order:
* put("<") put(name) begin()
* [ separator pp(field) ]* -- absent fields skipped entirely
* put(">") end() -- from the destructor
*
* Four shape rules, each easy to get subtly wrong (span_pp.hpp and commit
* e07b98b1 each got one wrong):
*
* - put(name) precedes begin(), so the header stays on the opening line
* when the group breaks.
*
* - begin() takes no offset: the indent comes from the sink's own
* PpConfig::indent_width, and *accumulates*, so a struct nested inside
* a struct indents one further level.
*
* - split(1), not split(1, indent). begin() has already moved the
* running indent and PpState adds a split's offset ON TOP of it, so
* passing the indent to both double-counts (broken fields land at 2x
* the intended column).
*
* - a separator before EVERY field including the first, matching legacy,
* which emits " " ahead of each member. Without it the first field
* abuts the name: <Name:f1 v1 ...>.
*
* The closing ">" belongs INSIDE the group (before end()), which the
* destructor makes impossible to forget.
**/
class struct_scope {
public:
/** @p force_break: separate fields with newline() rather than split(),
* so the struct always renders multi-line even where it would fit.
* A deliberate layout policy; see PpSink::struct_open.
**/
struct_scope(PpSink & sink, std::string_view name, bool force_break)
: sink_{sink}, force_break_{force_break}
{
sink_.put("<");
sink_.put(name);
sink_.begin();
}
~struct_scope() {
sink_.put(">");
sink_.end();
}
/* holds a PpSink & and owns an emission bracket: neither copyable nor
* movable. struct_open() returns one by value via guaranteed elision.
*/
struct_scope(const struct_scope &) = delete;
struct_scope & operator=(const struct_scope &) = delete;
/** add ":name value"; omitted entirely when @p present is false **/
template <typename Name, typename Value>
struct_scope & field(const Name & name,
const Value & value,
bool present = true)
{
if (present) {
this->separator();
/* qualified: `field` is also this member's name */
sink_.pp(xo::pp::field(name, value));
}
return *this;
}
/** add an already-built field-like value (anything renderable); its
* present() is consulted when it has one, as with pretty_struct().
**/
template <typename F>
struct_scope & item(const F & f) {
if constexpr (detail::has_present_flag<F>) {
if (!f.present())
return *this; /* absent: drop the field AND its separator */
}
this->separator();
sink_.pp(f);
return *this;
}
private:
void separator() {
if (force_break_)
sink_.newline(0);
else
sink_.split(1, 0);
}
private:
PpSink & sink_;
/** true: always break between fields, regardless of fit **/
bool force_break_ = false;
}; /*struct_scope*/
/** see PpSink::struct_open **/
inline struct_scope
PpSink::struct_open(std::string_view name, bool force_break) {
return struct_scope(*this, name, force_break);
}
/** see PpSink::pretty_struct. A compile-time field list is just a
* struct_scope that never force-breaks.
**/
template <typename... Fields>
void
PpSink::pretty_struct(std::string_view name, const Fields &... fields) {
struct_scope st(*this, name, false /*force_break*/);
(st.item(fields), ...);
}
} /*namespace xo::pp*/
/* end pretty_struct.hpp */