use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
pub const MARKER_PREFIX: &str = "ryo-debug";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DebugMarker {
pub session_id: String,
pub timestamp: u64,
pub description: Option<String>,
}
impl DebugMarker {
pub fn new() -> Self {
Self {
session_id: Self::generate_session_id(),
timestamp: Self::current_timestamp(),
description: None,
}
}
pub fn with_session(session_id: impl Into<String>) -> Self {
Self {
session_id: session_id.into(),
timestamp: Self::current_timestamp(),
description: None,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn to_comment(&self) -> String {
let desc = self.description.as_deref().unwrap_or("");
format!(
"/* {}:{}:{}:{} */",
MARKER_PREFIX, self.session_id, self.timestamp, desc
)
}
pub fn to_marker_string(&self) -> String {
let desc = self.description.as_deref().unwrap_or("");
format!(
"{}:{}:{}:{}",
MARKER_PREFIX, self.session_id, self.timestamp, desc
)
}
pub fn from_comment(s: &str) -> Option<Self> {
let s = s.trim();
let inner = if s.starts_with("/*") && s.ends_with("*/") {
s.strip_prefix("/*")?.strip_suffix("*/")?.trim()
} else {
s
};
let rest = inner.strip_prefix(MARKER_PREFIX)?.strip_prefix(':')?;
let parts: Vec<&str> = rest.splitn(3, ':').collect();
if parts.len() < 2 {
return None;
}
let session_id = parts[0].to_string();
let timestamp = parts[1].parse().ok()?;
let description = parts
.get(2)
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
Some(Self {
session_id,
timestamp,
description,
})
}
pub fn from_string_literal(s: &str) -> Option<Self> {
let s = s.trim();
let inner = s.strip_prefix('"')?.strip_suffix('"')?;
Self::from_comment(inner)
}
pub fn contains_marker(s: &str) -> bool {
s.contains(&format!("/* {}:", MARKER_PREFIX))
|| s.contains(&format!("\"{}:", MARKER_PREFIX))
}
pub fn extract_markers(s: &str) -> Vec<Self> {
let mut markers = Vec::new();
let prefix = format!("/* {}:", MARKER_PREFIX);
let mut search_start = 0;
while let Some(start) = s[search_start..].find(&prefix) {
let abs_start = search_start + start;
if let Some(end_offset) = s[abs_start..].find("*/") {
let end = abs_start + end_offset + 2;
if let Some(marker) = Self::from_comment(&s[abs_start..end]) {
markers.push(marker);
}
search_start = end;
} else {
break;
}
}
markers
}
fn generate_session_id() -> String {
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hasher};
let state = RandomState::new();
let mut hasher = state.build_hasher();
hasher.write_u64(Self::current_timestamp());
hasher.write_usize(std::process::id() as usize);
format!("{:08x}", hasher.finish() as u32)
}
fn current_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
}
impl Default for DebugMarker {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct DebugSession {
session_id: String,
}
impl DebugSession {
pub fn new() -> Self {
Self {
session_id: DebugMarker::generate_session_id(),
}
}
pub fn with_id(session_id: impl Into<String>) -> Self {
Self {
session_id: session_id.into(),
}
}
pub fn id(&self) -> &str {
&self.session_id
}
pub fn marker(&self) -> DebugMarker {
DebugMarker::with_session(&self.session_id)
}
pub fn marker_with_desc(&self, description: impl Into<String>) -> DebugMarker {
self.marker().with_description(description)
}
}
impl Default for DebugSession {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_marker_roundtrip() {
let marker = DebugMarker::with_session("test123").with_description("after map");
let comment = marker.to_comment();
assert!(comment.contains("ryo-debug:test123:"));
assert!(comment.contains(":after map"));
let parsed = DebugMarker::from_comment(&comment).unwrap();
assert_eq!(parsed.session_id, "test123");
assert_eq!(parsed.description, Some("after map".to_string()));
}
#[test]
fn test_marker_without_description() {
let marker = DebugMarker::with_session("abc");
let comment = marker.to_comment();
let parsed = DebugMarker::from_comment(&comment).unwrap();
assert_eq!(parsed.session_id, "abc");
assert!(parsed.description.is_none());
}
#[test]
fn test_contains_marker() {
let code = r#"
items.iter()
.inspect(|x| { /* ryo-debug:abc:123:test */ dbg!(x); })
.collect()
"#;
assert!(DebugMarker::contains_marker(code));
let code_without = "items.iter().collect()";
assert!(!DebugMarker::contains_marker(code_without));
}
#[test]
fn test_extract_markers() {
let code = r#"
items /* ryo-debug:s1:100:first */ .iter()
.inspect(|x| { /* ryo-debug:s1:101:second */ dbg!(x); })
.collect()
"#;
let markers = DebugMarker::extract_markers(code);
assert_eq!(markers.len(), 2);
assert_eq!(markers[0].session_id, "s1");
assert_eq!(markers[0].description, Some("first".to_string()));
assert_eq!(markers[1].session_id, "s1");
assert_eq!(markers[1].description, Some("second".to_string()));
}
#[test]
fn test_session() {
let session = DebugSession::new();
let m1 = session.marker_with_desc("first");
let m2 = session.marker_with_desc("second");
assert_eq!(m1.session_id, m2.session_id);
assert_eq!(m1.session_id, session.id());
}
}