Skip to main content

dbgflow_core/
session.rs

1//! Session types and persistence.
2//!
3//! This module defines the core data model for debugging sessions, including
4//! graph nodes, edges, and recorded execution events. It also provides helpers
5//! for reading and writing session files.
6
7use std::env;
8use std::fs;
9use std::path::{Path, PathBuf};
10use std::process;
11
12use serde::{Deserialize, Serialize};
13
14/// Graph node metadata persisted in a debugging session.
15#[derive(Clone, Debug, Serialize, Deserialize)]
16pub struct Node {
17    /// Stable node identifier within a session.
18    pub id: String,
19    /// Original traced function identifier when this node represents a specific invocation.
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub function_id: Option<String>,
22    /// Call identifier for traced function invocations.
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub call_id: Option<u64>,
25    /// Human-readable label shown in the UI.
26    pub label: String,
27    /// Semantic node type.
28    pub kind: NodeKind,
29    /// Rust module path where the node originated.
30    pub module_path: String,
31    /// Source file path captured for the node.
32    pub file: String,
33    /// Source line captured for the node.
34    pub line: u32,
35    /// Optional source code snippet shown by the UI.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub source: Option<String>,
38}
39
40/// Supported node types in the session graph.
41#[derive(Clone, Debug, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum NodeKind {
44    /// A traced function node.
45    Function,
46    /// A `#[ui_debug]` data node.
47    Type,
48    /// A test node emitted by `#[dbg_test]` or test helpers.
49    Test,
50}
51
52/// Directed edge between two graph nodes.
53#[derive(Clone, Debug, Serialize, Deserialize)]
54pub struct Edge {
55    /// Parent or source node identifier.
56    pub from: String,
57    /// Child or target node identifier.
58    pub to: String,
59    /// Semantic edge type used by the UI.
60    #[serde(default)]
61    pub kind: EdgeKind,
62    /// Optional short label rendered by the UI.
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub label: Option<String>,
65}
66
67/// Supported edge types in the session graph.
68#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
69#[serde(rename_all = "snake_case")]
70pub enum EdgeKind {
71    /// Control flow between traced functions.
72    #[default]
73    ControlFlow,
74    /// Data flow from a function into a data node snapshot.
75    DataFlow,
76    /// Link from a test node to the node it failed or passed on.
77    TestLink,
78}
79
80/// Named value preview attached to an event.
81#[derive(Clone, Debug, Serialize, Deserialize)]
82pub struct ValueSlot {
83    /// Logical field name or label.
84    pub name: String,
85    /// String preview rendered by the UI.
86    pub preview: String,
87}
88
89/// Single recorded execution event.
90#[derive(Clone, Debug, Serialize, Deserialize)]
91pub struct Event {
92    /// Monotonic sequence number within the session.
93    pub seq: u64,
94    /// Call identifier when the event belongs to a traced function invocation.
95    pub call_id: Option<u64>,
96    /// Parent call identifier when nested under another traced invocation.
97    pub parent_call_id: Option<u64>,
98    /// Node identifier this event belongs to.
99    pub node_id: String,
100    /// Event category.
101    pub kind: EventKind,
102    /// Short title shown in the UI timeline.
103    pub title: String,
104    /// Attached value previews.
105    pub values: Vec<ValueSlot>,
106}
107
108/// Supported event kinds emitted by the runtime.
109#[derive(Clone, Debug, Serialize, Deserialize)]
110#[serde(rename_all = "snake_case")]
111pub enum EventKind {
112    /// Function entry event.
113    FunctionEnter,
114    /// Function exit or unwind event.
115    FunctionExit,
116    /// Snapshot of a `#[ui_debug]` value.
117    ValueSnapshot,
118    /// Test start event.
119    TestStarted,
120    /// Test success event.
121    TestPassed,
122    /// Test failure event.
123    TestFailed,
124}
125
126/// Complete replayable debugging session.
127#[derive(Clone, Debug, Serialize, Deserialize)]
128pub struct Session {
129    /// Session title shown in the UI.
130    pub title: String,
131    /// All discovered graph nodes.
132    pub nodes: Vec<Node>,
133    /// All graph edges.
134    pub edges: Vec<Edge>,
135    /// Ordered execution events.
136    pub events: Vec<Event>,
137}
138
139impl Session {
140    /// Creates an empty session with the given title.
141    pub fn new(title: impl Into<String>) -> Self {
142        Self {
143            title: title.into(),
144            nodes: Vec::new(),
145            edges: Vec::new(),
146            events: Vec::new(),
147        }
148    }
149}
150
151/// Static metadata generated for traced functions.
152#[derive(Clone, Copy, Debug)]
153pub struct FunctionMeta {
154    /// Stable function identifier.
155    pub id: &'static str,
156    /// Human-readable label.
157    pub label: &'static str,
158    /// Rust module path.
159    pub module_path: &'static str,
160    /// Source file.
161    pub file: &'static str,
162    /// Source line.
163    pub line: u32,
164    /// Original source snippet captured from the macro input.
165    pub source: &'static str,
166}
167
168/// Static metadata generated for `#[ui_debug]` types.
169#[derive(Clone, Copy, Debug)]
170pub struct TypeMeta {
171    /// Stable type identifier.
172    pub id: &'static str,
173    /// Human-readable label.
174    pub label: &'static str,
175    /// Rust module path.
176    pub module_path: &'static str,
177    /// Source file.
178    pub file: &'static str,
179    /// Source line.
180    pub line: u32,
181    /// Original source snippet captured from the macro input.
182    pub source: &'static str,
183}
184
185// ---------------------------------------------------------------------------
186// Session persistence
187// ---------------------------------------------------------------------------
188
189/// Writes a session to a JSON file.
190pub fn write_session_json(session: &Session, path: impl AsRef<Path>) -> std::io::Result<()> {
191    let json = serde_json::to_string_pretty(session).map_err(std::io::Error::other)?;
192
193    if let Some(parent) = path.as_ref().parent() {
194        fs::create_dir_all(parent)?;
195    }
196
197    fs::write(path, json)
198}
199
200/// Reads a session from a JSON file.
201pub fn read_session_json(path: impl AsRef<Path>) -> std::io::Result<Session> {
202    let content = fs::read_to_string(path)?;
203    serde_json::from_str(&content).map_err(std::io::Error::other)
204}
205
206/// Sanitizes a label string into a safe filename component.
207fn sanitize_filename(label: &str) -> String {
208    let sanitized: String = label
209        .chars()
210        .map(|ch| match ch {
211            'a'..='z' | 'A'..='Z' | '0'..='9' => ch,
212            _ => '-',
213        })
214        .collect();
215
216    sanitized.trim_matches('-').to_owned()
217}
218
219/// Writes a session into a directory using a sanitized file name.
220pub fn write_session_snapshot_in_dir(
221    session: &Session,
222    dir: impl AsRef<Path>,
223    label: impl AsRef<str>,
224) -> std::io::Result<PathBuf> {
225    fs::create_dir_all(&dir)?;
226
227    let file_name = format!(
228        "{}-{}.json",
229        sanitize_filename(label.as_ref()),
230        process::id()
231    );
232    let path = dir.as_ref().join(file_name);
233    write_session_json(session, &path)?;
234    Ok(path)
235}
236
237/// Writes a session into the directory pointed to by `DBG_SESSION_DIR`.
238///
239/// Returns `Ok(None)` if the environment variable is not set.
240pub fn write_session_snapshot_from_env(
241    session: &Session,
242    label: impl AsRef<str>,
243) -> std::io::Result<Option<PathBuf>> {
244    match env::var("DBG_SESSION_DIR") {
245        Ok(dir) => write_session_snapshot_in_dir(session, dir, label).map(Some),
246        Err(env::VarError::NotPresent) => Ok(None),
247        Err(error) => Err(std::io::Error::other(error)),
248    }
249}