Skip to main content

sqlmodel_console/
traits.rs

1//! Traits for console-aware components.
2//!
3//! This module defines traits that allow database connections, pools, and other
4//! components to receive and use console output capabilities.
5//!
6//! # Example
7//!
8//! ```rust
9//! use sqlmodel_console::{ConsoleAware, SqlModelConsole};
10//! use std::sync::Arc;
11//!
12//! struct MyConnection {
13//!     console: Option<Arc<SqlModelConsole>>,
14//! }
15//!
16//! impl ConsoleAware for MyConnection {
17//!     fn set_console(&mut self, console: Option<Arc<SqlModelConsole>>) {
18//!         self.console = console;
19//!     }
20//!
21//!     fn console(&self) -> Option<&Arc<SqlModelConsole>> {
22//!         self.console.as_ref()
23//!     }
24//! }
25//!
26//! let mut conn = MyConnection { console: None };
27//! let console = Arc::new(SqlModelConsole::new());
28//! conn.set_console(Some(console));
29//!
30//! // Now the connection can emit rich output
31//! conn.emit_status("Connecting...");
32//! ```
33
34use std::sync::Arc;
35
36use crate::SqlModelConsole;
37
38/// Trait for components that can accept a console for rich output.
39///
40/// Implementing this trait allows database connections, pools, and other
41/// components to emit styled console output when a console is attached.
42///
43/// The trait uses `Arc<SqlModelConsole>` to allow sharing a single console
44/// across multiple components without lifetime complications.
45///
46/// # Design Notes
47///
48/// - **Optional attachment**: Components work without a console attached,
49///   silently ignoring output calls. This makes console support opt-in.
50///
51/// - **Thread-safe sharing**: Using `Arc` allows the same console to be
52///   shared across threads and async tasks.
53///
54/// - **Default method implementations**: The `emit_*` methods have default
55///   implementations that only require `set_console` and `console` to be defined.
56///
57/// # Example
58///
59/// ```rust
60/// use sqlmodel_console::{ConsoleAware, SqlModelConsole, OutputMode};
61/// use std::sync::Arc;
62///
63/// struct DatabasePool {
64///     console: Option<Arc<SqlModelConsole>>,
65///     connections: Vec<String>,
66/// }
67///
68/// impl ConsoleAware for DatabasePool {
69///     fn set_console(&mut self, console: Option<Arc<SqlModelConsole>>) {
70///         self.console = console;
71///     }
72///
73///     fn console(&self) -> Option<&Arc<SqlModelConsole>> {
74///         self.console.as_ref()
75///     }
76/// }
77///
78/// let mut pool = DatabasePool {
79///     console: None,
80///     connections: Vec::new(),
81/// };
82///
83/// // No console attached - emit calls are silently ignored
84/// pool.emit_status("Starting pool...");
85///
86/// // Attach a console
87/// let console = Arc::new(SqlModelConsole::with_mode(OutputMode::Plain));
88/// pool.set_console(Some(console));
89///
90/// // Now emit calls produce output
91/// pool.emit_success("Pool ready with 5 connections");
92/// ```
93pub trait ConsoleAware {
94    /// Attach or detach a console.
95    ///
96    /// Pass `Some(console)` to enable rich output.
97    /// Pass `None` to disable console output.
98    fn set_console(&mut self, console: Option<Arc<SqlModelConsole>>);
99
100    /// Get reference to the attached console, if any.
101    fn console(&self) -> Option<&Arc<SqlModelConsole>>;
102
103    /// Check if a console is attached.
104    ///
105    /// This is a convenience method that returns `true` if a console
106    /// is currently attached.
107    fn has_console(&self) -> bool {
108        self.console().is_some()
109    }
110
111    /// Emit a status message if console is attached.
112    ///
113    /// Status messages are informational and go to stderr.
114    /// If no console is attached, this is a no-op.
115    fn emit_status(&self, message: &str) {
116        if let Some(console) = self.console() {
117            console.status(message);
118        }
119    }
120
121    /// Emit a success message if console is attached.
122    ///
123    /// Success messages indicate successful completion of an operation.
124    /// If no console is attached, this is a no-op.
125    fn emit_success(&self, message: &str) {
126        if let Some(console) = self.console() {
127            console.success(message);
128        }
129    }
130
131    /// Emit an error message if console is attached.
132    ///
133    /// Error messages indicate failures or problems.
134    /// If no console is attached, this is a no-op.
135    fn emit_error(&self, message: &str) {
136        if let Some(console) = self.console() {
137            console.error(message);
138        }
139    }
140
141    /// Emit a warning message if console is attached.
142    ///
143    /// Warning messages indicate potential issues that don't prevent operation.
144    /// If no console is attached, this is a no-op.
145    fn emit_warning(&self, message: &str) {
146        if let Some(console) = self.console() {
147            console.warning(message);
148        }
149    }
150
151    /// Emit an info message if console is attached.
152    ///
153    /// Info messages provide helpful information to the user.
154    /// If no console is attached, this is a no-op.
155    fn emit_info(&self, message: &str) {
156        if let Some(console) = self.console() {
157            console.info(message);
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use crate::OutputMode;
166
167    /// Mock connection for testing ConsoleAware trait.
168    struct MockConnection {
169        console: Option<Arc<SqlModelConsole>>,
170    }
171
172    impl MockConnection {
173        fn new() -> Self {
174            Self { console: None }
175        }
176    }
177
178    impl ConsoleAware for MockConnection {
179        fn set_console(&mut self, console: Option<Arc<SqlModelConsole>>) {
180            self.console = console;
181        }
182
183        fn console(&self) -> Option<&Arc<SqlModelConsole>> {
184            self.console.as_ref()
185        }
186    }
187
188    #[test]
189    fn test_has_console_false_initially() {
190        let conn = MockConnection::new();
191        assert!(!conn.has_console());
192    }
193
194    #[test]
195    fn test_has_console_true_after_set() {
196        let mut conn = MockConnection::new();
197        let console = Arc::new(SqlModelConsole::with_mode(OutputMode::Plain));
198        conn.set_console(Some(console));
199        assert!(conn.has_console());
200    }
201
202    #[test]
203    fn test_set_console_none_detaches() {
204        let mut conn = MockConnection::new();
205        let console = Arc::new(SqlModelConsole::with_mode(OutputMode::Plain));
206        conn.set_console(Some(console));
207        assert!(conn.has_console());
208        conn.set_console(None);
209        assert!(!conn.has_console());
210    }
211
212    #[test]
213    fn test_console_returns_reference() {
214        let mut conn = MockConnection::new();
215        let console = Arc::new(SqlModelConsole::with_mode(OutputMode::Plain));
216        conn.set_console(Some(console.clone()));
217
218        let returned = conn.console().unwrap();
219        // Verify it's the same console (mode matches)
220        assert!(returned.is_plain());
221    }
222
223    #[test]
224    fn test_emit_methods_no_panic_without_console() {
225        let conn = MockConnection::new();
226        // These should not panic even without a console
227        conn.emit_status("test status");
228        conn.emit_success("test success");
229        conn.emit_error("test error");
230        conn.emit_warning("test warning");
231        conn.emit_info("test info");
232    }
233
234    #[test]
235    fn test_emit_methods_with_console() {
236        let mut conn = MockConnection::new();
237        let console = Arc::new(SqlModelConsole::with_mode(OutputMode::Plain));
238        conn.set_console(Some(console));
239
240        // These should not panic with console attached
241        // (output goes to stderr but we can't easily capture it)
242        conn.emit_status("connecting to database");
243        conn.emit_success("connection established");
244        conn.emit_error("query failed");
245        conn.emit_warning("deprecated feature used");
246        conn.emit_info("using connection pool");
247    }
248
249    #[test]
250    fn test_shared_console_across_components() {
251        let console = Arc::new(SqlModelConsole::with_mode(OutputMode::Json));
252
253        let mut conn1 = MockConnection::new();
254        let mut conn2 = MockConnection::new();
255
256        conn1.set_console(Some(console.clone()));
257        conn2.set_console(Some(console.clone()));
258
259        // Both connections share the same console
260        assert!(conn1.console().unwrap().is_json());
261        assert!(conn2.console().unwrap().is_json());
262
263        // Arc reference count is 3 (original + 2 connections)
264        assert_eq!(Arc::strong_count(&console), 3);
265    }
266
267    #[test]
268    fn test_console_none_returns_none() {
269        let conn = MockConnection::new();
270        assert!(conn.console().is_none());
271    }
272}