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