263 lines
12 KiB
C++
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 */
|