vtcode_commons/
reference.rs

1use std::path::{Path, PathBuf};
2use std::sync::{Arc, Mutex};
3
4use anyhow::{Error, Result};
5
6use crate::{ErrorReporter, TelemetrySink, WorkspacePaths};
7
8/// Reference implementation of [`WorkspacePaths`] backed by static [`PathBuf`]s.
9///
10/// This is useful for adopters who want to drive the extracted crates from an
11/// existing application without wiring additional indirection layers. The
12/// implementation is intentionally straightforward: callers provide the root
13/// workspace directory and configuration path up front and can optionally
14/// supply cache or telemetry directories.
15#[derive(Debug, Clone)]
16pub struct StaticWorkspacePaths {
17    root: PathBuf,
18    config: PathBuf,
19    cache: Option<PathBuf>,
20    telemetry: Option<PathBuf>,
21}
22
23impl StaticWorkspacePaths {
24    /// Creates a new [`StaticWorkspacePaths`] with the required workspace and
25    /// configuration directories.
26    pub fn new(root: impl Into<PathBuf>, config: impl Into<PathBuf>) -> Self {
27        Self {
28            root: root.into(),
29            config: config.into(),
30            cache: None,
31            telemetry: None,
32        }
33    }
34
35    /// Configures an optional cache directory used by the consumer.
36    pub fn with_cache_dir(mut self, cache: impl Into<PathBuf>) -> Self {
37        self.cache = Some(cache.into());
38        self
39    }
40
41    /// Configures an optional telemetry directory used by the consumer.
42    pub fn with_telemetry_dir(mut self, telemetry: impl Into<PathBuf>) -> Self {
43        self.telemetry = Some(telemetry.into());
44        self
45    }
46}
47
48impl WorkspacePaths for StaticWorkspacePaths {
49    fn workspace_root(&self) -> &Path {
50        &self.root
51    }
52
53    fn config_dir(&self) -> PathBuf {
54        self.config.clone()
55    }
56
57    fn cache_dir(&self) -> Option<PathBuf> {
58        self.cache.clone()
59    }
60
61    fn telemetry_dir(&self) -> Option<PathBuf> {
62        self.telemetry.clone()
63    }
64}
65
66/// In-memory telemetry sink that records cloned events for later inspection.
67///
68/// This helper is primarily intended for tests, examples, or prototypes that
69/// want to assert on the events emitted by a component without integrating a
70/// full telemetry backend. The recorded events can be retrieved via
71/// [`MemoryTelemetry::take`].
72#[derive(Debug, Default, Clone)]
73pub struct MemoryTelemetry<Event> {
74    events: Arc<Mutex<Vec<Event>>>,
75}
76
77impl<Event> MemoryTelemetry<Event> {
78    /// Creates a new memory-backed telemetry sink.
79    pub fn new() -> Self {
80        Self {
81            events: Arc::new(Mutex::new(Vec::new())),
82        }
83    }
84
85    /// Returns the recorded events, draining the internal buffer.
86    pub fn take(&self) -> Vec<Event> {
87        let mut events = self.events.lock().expect("telemetry poisoned");
88        std::mem::take(&mut *events)
89    }
90}
91
92impl<Event> TelemetrySink<Event> for MemoryTelemetry<Event>
93where
94    Event: Clone + Send + Sync,
95{
96    fn record(&self, event: &Event) -> Result<()> {
97        let mut events = self.events.lock().expect("telemetry poisoned");
98        events.push(event.clone());
99        Ok(())
100    }
101}
102
103/// Simple [`ErrorReporter`] that stores error messages in memory.
104///
105/// This helper is designed for tests and examples that need to assert on the
106/// errors emitted by a component without wiring an external monitoring system.
107/// Callers can retrieve captured messages via [`MemoryErrorReporter::take`].
108#[derive(Debug, Default, Clone)]
109pub struct MemoryErrorReporter {
110    messages: Arc<Mutex<Vec<String>>>,
111}
112
113impl MemoryErrorReporter {
114    /// Creates a new memory-backed error reporter.
115    pub fn new() -> Self {
116        Self {
117            messages: Arc::new(Mutex::new(Vec::new())),
118        }
119    }
120
121    /// Returns the captured error messages, draining the buffer.
122    pub fn take(&self) -> Vec<String> {
123        let mut messages = self.messages.lock().expect("reporter poisoned");
124        std::mem::take(&mut *messages)
125    }
126}
127
128impl ErrorReporter for MemoryErrorReporter {
129    fn capture(&self, error: &Error) -> Result<()> {
130        let mut messages = self.messages.lock().expect("reporter poisoned");
131        messages.push(format!("{error:?}"));
132        Ok(())
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use std::path::PathBuf;
140
141    #[test]
142    fn static_paths_exposes_optional_directories() {
143        let paths = StaticWorkspacePaths::new("/tmp/work", "/tmp/work/config")
144            .with_cache_dir("/tmp/work/cache")
145            .with_telemetry_dir("/tmp/work/telemetry");
146
147        assert_eq!(paths.workspace_root(), Path::new("/tmp/work"));
148        assert_eq!(paths.config_dir(), PathBuf::from("/tmp/work/config"));
149        assert_eq!(paths.cache_dir(), Some(PathBuf::from("/tmp/work/cache")));
150        assert_eq!(
151            paths.telemetry_dir(),
152            Some(PathBuf::from("/tmp/work/telemetry"))
153        );
154    }
155
156    #[test]
157    fn memory_telemetry_records_events() {
158        let telemetry = MemoryTelemetry::new();
159        telemetry.record(&"event-1").unwrap();
160        telemetry.record(&"event-2").unwrap();
161
162        assert_eq!(telemetry.take(), vec!["event-1", "event-2"]);
163        assert!(telemetry.take().is_empty());
164    }
165
166    #[test]
167    fn memory_error_reporter_captures_messages() {
168        let reporter = MemoryErrorReporter::new();
169        reporter.capture(&Error::msg("error-1")).unwrap();
170        reporter.capture(&Error::msg("error-2")).unwrap();
171
172        let messages = reporter.take();
173        assert_eq!(messages.len(), 2);
174        assert!(messages[0].contains("error-1"));
175        assert!(messages[1].contains("error-2"));
176        assert!(reporter.take().is_empty());
177    }
178}