xo-umbrella2/xo-printjson/include/xo/printjson/JsonPrintState.hpp
Roland Conybeare 339edf668c
Some checks failed
cmake-docker / cmake-build (clang, clang++) (push) Failing after 42m42s
cmake-docker / cmake-build (gcc, g++) (push) Has been cancelled
CI / smoke-test (push) Failing after 39m28s
xo-reflect xo-printjson: reflect supports map types [FEATURE]
2026-10-11 01:26:34 -04:00

263 lines
12 KiB
C++

/** @file JsonPrintState.hpp
*
* @author Roland Conybeare, Oct 2026
**/
#pragma once
#include <xo/reflect/Ownership.hpp>
#include <xo/reflect/struct/GuardedVisit.hpp>
#include <xo/reflect/TaggedPtr.hpp>
#include <xo/reflect/TypeDescr.hpp>
#include <cstdint>
#include <iosfwd>
#include <string_view>
#include <unordered_map>
namespace xo {
namespace json {
class PrintJson;
class JsonObject;
/** @brief one json print in progress
*
* PrintJson's entry points (print, print_tp, print_obj) make one
* per top-level value. A JsonPrinter receives it, and writes and
* recurses only through it: what a print needs to know about
* itself travels here, in plain sight, rather than in the printer
* or the printer table.
*
* A printer must not call an entry point to print a child: that
* starts a print within a print. See
* .xo-backlog/xo-printjson/issues/02.
*
* Each object prints once. The first time a print reaches an
* object -- a value its printer writes as a json object -- it
* prints in full, carrying "_id_": n; every later time, as
* {"_ref_": n}. So any graph, cycles included, prints in output
* linear in its size. Ids number objects 1, 2, 3 .. in order of
* first mention, within one print.
*
* An object's identity is its address, and the type it first
* printed as. Another object at the same address -- a struct's
* first by-value member -- is part of the first: it prints in
* full, with no "_id_", and is never a ref's target.
*
* Where an object prints in full follows ownership
* (.xo-backlog/xo-printjson/issues/08): through an owning or shared
* edge -- a member held by value, a std::unique_ptr, an rp<T> -- it
* prints at first appearance, as above. Through a borrowed edge --
* a raw pointer, unless reflection says otherwise -- only ever as
* {"_ref_": n}, without reading the pointee. A borrowed ref's
* target that nothing places, or that a different type (not derived
* from the pointee's) occupies, is listed when the top-level object
* closes:
*
* "_unplaced_": [{"_ref_": n, "_type_": .., "_address_": ..}, ..]
*
* Each names an object, as a ref does, without defining it: every
* ref in the output resolves to an "_id_", or is listed here.
*
* absent when empty. Not written if the top-level value is not a
* json object.
*
* A pointer handed to an entry point (PrintJson::print_tp ..) is
* the caller vouching for its target: the top-level edge places it
* (shared), whatever the pointer type's default, and its object is
* the top-level object.
*
* Nesting is bounded: print() aborts, with a backtrace, past
* PrintJson::max_depth() -- see print()
*
* Printer will acquire locks for a struct's reflected members
* (@see JsonMembers::reflected_members, @see PrintJson::guard_mode()),
* so caller must not already hold them.
**/
class JsonPrintState {
public:
using TaggedPtr = xo::reflect::TaggedPtr;
using TypeDescr = xo::reflect::TypeDescr;
public:
/** a print of json on @p p_os, through @p pjson's printers,
* nesting at most @p pjson->max_depth() deep
**/
JsonPrintState(PrintJson const * pjson, std::ostream * p_os);
JsonPrintState(JsonPrintState const &) = delete;
JsonPrintState & operator=(JsonPrintState const &) = delete;
/** where json goes **/
std::ostream * p_os() const { return p_os_; }
/** true iff a printer was provided for @p td (PrintJson::provide_printer) **/
bool has_printer(TypeDescr td) const;
/** write json for @p tp: by its type's printer, if one was
* provided, else by the generic behaviour for its metatype
* (pointer, vector, struct). The one way a printer recurses.
*
* @p tp is a lasting object -- a member, an element, a
* pointee -- so takes part in identity: an object printed
* already becomes {"_ref_": n}.
*
* Each call nests one deeper than its caller. A call that
* would nest deeper than max_depth() prints a diagnosis and a
* backtrace on stderr, then aborts
**/
void print(TaggedPtr tp);
/** as print(), for a computed value with no lasting address
* (e.g. an atomic's load()): no identity for @p tp itself,
* since another temporary may later occupy its address. What
* it points to still takes part
**/
void print_value(TaggedPtr tp);
/** the pointee of the pointer @p ptr, reached by an edge of kind
* @p edge: owning or shared, print() it (null if none); borrowed,
* a ref to it, never reading it. Nests one deeper, as print()
* of the pointer would. For a member whose reflection overrides
* its pointer type's ownership (StructMember::ownership()); a
* pointer print()ed takes its type's
* (TypeDescr::child_edge_ownership())
**/
void print_pointee(TaggedPtr ptr, reflect::Ownership edge);
/** {"_ref_": n} for the object at @p p, printed in full elsewhere
* in this print (before or after), or null. @p p must be the
* address that object prints at: typed as its printer is keyed
* (e.g. a WsSender const *, not a dynamic_cast to void const *)
**/
void print_ref(void const * p);
/** open the json object for the value being printed, from its
* printer's print_json; at most once per print_json call.
* Writes {"_name_": @p name, the type keys of @p td, and (if
* it has identity) "_id_": n. Add keys, then close()
**/
JsonObject open_object(std::string_view name, TypeDescr td);
/** open_object, named and typed by @p tp's type **/
JsonObject open_object(TaggedPtr tp);
/** true iff the object at @p p has printed in full, in this print **/
bool is_printed(void const * p) const;
/** true iff print(@p tp) would print a ref: an object of @p tp's
* type has printed in full at @p tp's address, in this print
**/
bool is_printed(TaggedPtr tp) const;
/** open a json object for the object at @p p, one the printer
* writes inline itself rather than through print(): it takes
* part in identity like any other, "_id_" included. Ask
* is_printed(@p p) first -- if so, print_ref(@p p) instead.
* Independent of the value print_json was called for
**/
JsonObject open_object_at(void const * p, std::string_view name, TypeDescr td);
/** current nesting: print() calls in progress **/
std::uint32_t depth() const { return depth_; }
/** nesting limit, from PrintJson::max_depth() **/
std::uint32_t max_depth() const { return max_depth_; }
/** how guards are taken, from PrintJson::guard_mode() **/
reflect::GuardMode guard_mode() const { return guard_mode_; }
private:
/** an object met in this print **/
struct ObjectEntry {
/** its "_id_" / "_ref_" **/
std::uint32_t id_ = 0;
/** the type it printed as; nullptr while only referred to **/
TypeDescr type_ = nullptr;
/** the pointee type of the first borrowed ref to it; it is
* placed for that ref only as this type, or one derived from
* it. nullptr if no borrowed ref, or an untyped one
* (print_ref)
**/
TypeDescr ref_type_ = nullptr;
};
/** the value print_node is dispatching, for open_object **/
struct Pending {
/** a print_json call is in progress that has not yet opened its object **/
bool open_ = false;
/** the value's address, if it takes part in identity; else nullptr **/
void const * address_ = nullptr;
/** the value's type **/
TypeDescr type_ = nullptr;
};
/** print(), print_value(): @p identity false for print_value **/
void print_node(TaggedPtr tp, bool identity);
/** the entry for the object at @p p, made (unprinted) on first mention **/
ObjectEntry & entry_for(void const * p);
/** past max_depth_: diagnose, backtrace, abort **/
[[noreturn]] void abort_too_deep(TaggedPtr tp) const;
/** printer misuse (open_object twice, or outside print_json): diagnose, abort **/
[[noreturn]] void abort_misuse(char const * what) const;
/** {"_name_": .., type keys, and "_id_" if @p p non-null: owns @p p's entry.
* @p is_root: the top-level value's own object, which writes
* "_unplaced_" as it closes
**/
JsonObject open_object_aux(void const * p, std::string_view name, TypeDescr td,
bool is_root);
/** print() of the top-level value, from an entry point
* (PrintJson::print_tp, validate_tp): a pointer's target is
* placed, whatever its type's default
**/
void print_root(TaggedPtr tp);
/** a map (MapTdx), within print_node's depth level: a json
* object keyed by each key's json -- quoted if not a json
* string already -- when keys are atomic; else an array of
* {"_key_": .., "_value_": ..}. In key order
**/
void print_map(TaggedPtr tp);
/** @p key's json, as a json object key: its printed form, quoted
* unless already a json string. No identity: printed as a value
**/
std::string map_key_string(TaggedPtr key);
/** print_pointee, within a depth level already counted **/
void print_pointee_aux(TaggedPtr ptr, reflect::Ownership edge);
/** , "_unplaced_": [..] -- if any; from the root JsonObject's close() **/
void write_unplaced();
private:
/* close() of the root object writes "_unplaced_" */
friend class JsonObject;
/* entry points print the top-level value via print_root */
friend class PrintJson;
private:
/** the printer table **/
PrintJson const * pjson_ = nullptr;
/** where json goes **/
std::ostream * p_os_ = nullptr;
/** print() calls in progress **/
std::uint32_t depth_ = 0;
/** depth of the top-level object, which writes "_unplaced_":
* 1, or 2 when print_root reaches it through a pointer
**/
std::uint32_t root_depth_ = 1;
/** print() aborts past this depth **/
std::uint32_t max_depth_ = 0;
/** see guard_mode() **/
reflect::GuardMode guard_mode_ = reflect::GuardMode::blocking;
/** objects met in this print, by address. A std::unordered_map
* for now; a DArenaHashMap from a pool of temporary arenas is
* the intent (.xo-backlog/xo-printjson/issues/02)
**/
std::unordered_map<void const *, ObjectEntry> objects_;
/** id for the next object first mentioned **/
std::uint32_t next_id_ = 1;
/** see Pending **/
Pending pending_;
}; /*JsonPrintState*/
} /*namespace json*/
} /*namespace xo*/
/* end JsonPrintState.hpp */