Skip to main content

graphrefly_graph/
debug.rs

1//! Optional binding-side handle → debug value rendering (F sub-slice,
2//! 2026-05-10).
3//!
4//! [`Graph::describe`](crate::Graph::describe) surfaces node values as
5//! `DescribeValue::Handle(HandleId)` — raw u64 view, no FFI. Consumers
6//! who want a TS-style `value: T` view in the JSON output pass an
7//! [`DebugBindingBoundary`] impl to
8//! [`Graph::describe_with_debug`](crate::Graph::describe_with_debug);
9//! that variant renders each handle via the trait before serializing.
10//!
11//! # Why a separate trait (not on `BindingBoundary`)
12//!
13//! Keeping the hot-path FFI trait ([`graphrefly_core::BindingBoundary`])
14//! free of debug / introspection methods means:
15//!
16//! - Minimal bindings (test stubs, perf benches without describe
17//!   needs) don't have to implement debug-rendering. They impl
18//!   `BindingBoundary` only.
19//! - `graphrefly-core` stays serde-free. The debug rendering returns
20//!   `serde_json::Value`, which is a `graphrefly-graph` concern
21//!   (where describe-output JSON already lives).
22//! - Production bindings (napi-rs, pyo3, wasm-bindgen) implement both
23//!   traits side-by-side. The describe-rendering path is opt-in via
24//!   `describe_with_debug`; default `describe` callers pay no cost.
25//!
26//! The trait is intentionally minimal — one method, no default impl.
27//! Bindings are expected to look up the registered `T` for the
28//! handle and project it via `serde_json` (or a custom translator
29//! for non-serde-friendly value types).
30
31use graphrefly_core::HandleId;
32
33/// Render handles into JSON-shaped debug values.
34///
35/// Bindings that want to support
36/// [`Graph::describe_with_debug`](crate::Graph::describe_with_debug)
37/// implement this trait alongside
38/// [`graphrefly_core::BindingBoundary`]. The two traits cover the
39/// two FFI surfaces:
40///
41/// - `BindingBoundary` is the hot-path trait — invoked per fn-fire,
42///   per equals check, per handle refcount op. Bindings ALWAYS impl
43///   this.
44/// - `DebugBindingBoundary` is the cold-path trait — invoked once per
45///   named node per `describe_with_debug` call. Bindings impl this
46///   ONLY if they want their `Graph::describe()` output to surface
47///   `value: T` shapes instead of raw `value: <u64>` handles.
48///
49/// # Implementation guidance
50///
51/// For a typical binding that stores `HashMap<HandleId, T>` where
52/// `T: Serialize`, the implementation is one line:
53///
54/// ```ignore
55/// impl DebugBindingBoundary for MyBinding {
56///     fn handle_to_debug(&self, h: HandleId) -> serde_json::Value {
57///         self.values.borrow_mut().get(&h)
58///             .map(|v| serde_json::to_value(v).unwrap_or(serde_json::Value::Null))
59///             .unwrap_or(serde_json::Value::Null)
60///     }
61/// }
62/// ```
63///
64/// For value types that don't implement `serde::Serialize` (e.g.,
65/// JS function handles in napi-rs), the binding can return a
66/// descriptor object like `{ "type": "function", "fn_id": 42 }` or
67/// fall back to `Value::String(format!("<opaque:{h:?}>"))`.
68///
69/// # Thread safety
70///
71/// `Send + Sync` per the same rationale as `BindingBoundary`: the
72/// Core dispatcher may be cloned across threads, so any binding it
73/// holds must also be thread-safe.
74pub trait DebugBindingBoundary: Send + Sync {
75    /// Render `handle` as a JSON value. Implementations typically
76    /// look the handle up in the binding's value registry and
77    /// serialize via `serde_json::to_value(&value)`. Return
78    /// `serde_json::Value::Null` for unknown / dangling handles
79    /// (the call may race with concurrent `release_handle`
80    /// finalizers).
81    fn handle_to_debug(&self, handle: HandleId) -> serde_json::Value;
82}