Skip to main content

clawless_core/
output.rs

1//! Command output interface
2//!
3//! This module defines [`Output`], the interface commands use to produce events. `Output` wraps an
4//! [`EventSender`] and provides typed methods for each event kind, decoupling commands from the
5//! rendering strategy. Commands call [`message`], [`detail`], or [`artifact`] to emit events into
6//! the channel; the Presenter consumes them and decides how to render.
7//!
8//! [`artifact`]: Output::artifact
9//! [`detail`]: Output::detail
10//! [`message`]: Output::message
11
12use std::fmt::{Debug, Display};
13
14use serde::Serialize;
15
16use crate::event::{Event, EventSender, SendError};
17
18/// Command output interface that wraps an event channel sender
19///
20/// `Output` provides typed methods for each event kind, decoupling commands from the event channel
21/// API. Commands call [`message`], [`detail`], or [`artifact`] to emit events; the Presenter
22/// consumes them for rendering.
23///
24/// `Output` is cheaply clonable. Cloning an `Output` produces another handle to the same
25/// underlying channel, not an independent channel.
26///
27/// # Examples
28///
29/// ```
30/// use clawless_core::event::event_channel;
31/// use clawless_core::output::Output;
32///
33/// # #[tokio::main]
34/// # async fn main() {
35/// let (sender, mut receiver) = event_channel();
36/// let output = Output::new(sender);
37///
38/// output.message("hello").await.expect("should send");
39/// # }
40/// ```
41///
42/// [`artifact`]: Output::artifact
43/// [`detail`]: Output::detail
44/// [`message`]: Output::message
45// r[impl output.safety.clone]
46// r[impl output.safety.send]
47// r[impl output.safety.concurrent]
48#[derive(Clone, Debug)]
49pub struct Output {
50    /// Channel that carries the emitted events
51    sender: EventSender,
52}
53
54impl Output {
55    /// Creates a new `Output` wrapping the given event sender
56    ///
57    /// # Examples
58    ///
59    /// ```
60    /// use clawless_core::event::event_channel;
61    /// use clawless_core::output::Output;
62    ///
63    /// let (sender, _receiver) = event_channel();
64    /// let output = Output::new(sender);
65    /// ```
66    pub fn new(sender: EventSender) -> Self {
67        Self { sender }
68    }
69
70    /// Sends an informational message event
71    ///
72    /// Converts the value to a string via [`Display`] and sends it as an [`Event::Message`].
73    ///
74    /// # Errors
75    ///
76    /// Returns [`SendError`] if the [`EventReceiver`] has been dropped.
77    ///
78    /// # Examples
79    ///
80    /// ```
81    /// use clawless_core::event::event_channel;
82    /// use clawless_core::output::Output;
83    ///
84    /// # #[tokio::main]
85    /// # async fn main() {
86    /// let (sender, _receiver) = event_channel();
87    /// let output = Output::new(sender);
88    ///
89    /// output.message("processing files").await.expect("should send");
90    /// output.message(format!("found {} items", 42)).await.expect("should send");
91    /// # }
92    /// ```
93    ///
94    /// [`EventReceiver`]: crate::event::EventReceiver
95    // r[impl output.send.message]
96    // r[impl output.send.async]
97    pub async fn message(&self, message: impl Display) -> Result<(), SendError> {
98        self.sender.send(Event::Message(message.to_string())).await
99    }
100
101    /// Sends a supplementary detail event
102    ///
103    /// Converts the value to a string via [`Display`] and sends it as an [`Event::Detail`].
104    /// Details carry lower-priority information that the Presenter may suppress at default
105    /// verbosity.
106    ///
107    /// # Errors
108    ///
109    /// Returns [`SendError`] if the [`EventReceiver`] has been dropped.
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use clawless_core::event::event_channel;
115    /// use clawless_core::output::Output;
116    ///
117    /// # #[tokio::main]
118    /// # async fn main() {
119    /// let (sender, _receiver) = event_channel();
120    /// let output = Output::new(sender);
121    ///
122    /// output.detail("reading config from ~/.config/app.toml").await.expect("should send");
123    /// # }
124    /// ```
125    ///
126    /// [`EventReceiver`]: crate::event::EventReceiver
127    // r[impl output.send.detail]
128    pub async fn detail(&self, detail: impl Display) -> Result<(), SendError> {
129        self.sender.send(Event::Detail(detail.to_string())).await
130    }
131
132    /// Sends a structured artifact event
133    ///
134    /// Wraps the value in a [`Box`] and sends it as an [`Event::Artifact`]. The value must
135    /// implement [`Display`] (for text rendering), [`Serialize`] (for JSON rendering), and
136    /// [`Debug`] (for diagnostics).
137    ///
138    /// # Errors
139    ///
140    /// Returns [`SendError`] if the [`EventReceiver`] has been dropped.
141    ///
142    /// # Examples
143    ///
144    /// ```
145    /// use std::fmt;
146    ///
147    /// use serde::Serialize;
148    ///
149    /// use clawless_core::event::event_channel;
150    /// use clawless_core::output::Output;
151    ///
152    /// #[derive(Clone, Debug, Serialize)]
153    /// struct UserCount {
154    ///     count: usize,
155    /// }
156    ///
157    /// impl fmt::Display for UserCount {
158    ///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159    ///         write!(f, "{} users", self.count)
160    ///     }
161    /// }
162    ///
163    /// # #[tokio::main]
164    /// # async fn main() {
165    /// let (sender, _receiver) = event_channel();
166    /// let output = Output::new(sender);
167    ///
168    /// output.artifact(UserCount { count: 42 }).await.expect("should send");
169    /// # }
170    /// ```
171    ///
172    /// [`EventReceiver`]: crate::event::EventReceiver
173    // r[impl output.send.artifact]
174    pub async fn artifact<T>(&self, value: T) -> Result<(), SendError>
175    where
176        T: Display + Serialize + Debug + Send + Sync + 'static,
177    {
178        self.sender.send(Event::Artifact(Box::new(value))).await
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    // An assertion in a test panics by design. A `# Panics` section on every test
185    // would repeat that and give the reader no information.
186    #![allow(clippy::missing_panics_doc)]
187
188    use std::fmt;
189
190    use serde::Serialize;
191
192    use super::*;
193    use crate::event::event_channel;
194
195    #[derive(Clone, Debug, Serialize)]
196    struct TestArtifact {
197        value: String,
198    }
199
200    impl fmt::Display for TestArtifact {
201        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202            write!(f, "{}", self.value)
203        }
204    }
205
206    // r[verify output.send.artifact]
207    #[tokio::test]
208    async fn artifact_sends_artifact_event() {
209        let (sender, mut receiver) = event_channel();
210        let output = Output::new(sender);
211
212        output
213            .artifact(TestArtifact {
214                value: "result".to_string(),
215            })
216            .await
217            .expect("should send");
218
219        let event = receiver.recv().await.expect("should receive");
220
221        assert!(matches!(event, Event::Artifact(ref a) if a.to_string() == "result"));
222    }
223
224    #[tokio::test]
225    async fn artifact_to_closed_channel_returns_error() {
226        let (sender, receiver) = event_channel();
227        let output = Output::new(sender);
228        drop(receiver);
229
230        let error = output
231            .artifact(TestArtifact {
232                value: "lost".to_string(),
233            })
234            .await
235            .expect_err("should fail");
236
237        assert_eq!(error.to_string(), "event channel closed");
238    }
239
240    // r[verify output.safety.clone]
241    #[tokio::test]
242    async fn clone_produces_independent_handle() {
243        let (sender, mut receiver) = event_channel();
244        let output = Output::new(sender);
245        let cloned = output.clone();
246
247        output.message("from original").await.expect("should send");
248        cloned.message("from clone").await.expect("should send");
249
250        let first = receiver.recv().await.expect("should receive first");
251        let second = receiver.recv().await.expect("should receive second");
252
253        assert!(matches!(first, Event::Message(ref s) if s == "from original"));
254        assert!(matches!(second, Event::Message(ref s) if s == "from clone"));
255    }
256
257    // r[verify output.send.detail]
258    #[tokio::test]
259    async fn detail_sends_detail_event() {
260        let (sender, mut receiver) = event_channel();
261        let output = Output::new(sender);
262
263        output
264            .detail("supplementary info")
265            .await
266            .expect("should send");
267
268        let event = receiver.recv().await.expect("should receive");
269
270        assert!(matches!(event, Event::Detail(ref s) if s == "supplementary info"));
271    }
272
273    #[tokio::test]
274    async fn detail_to_closed_channel_returns_error() {
275        let (sender, receiver) = event_channel();
276        let output = Output::new(sender);
277        drop(receiver);
278
279        let error = output.detail("lost").await.expect_err("should fail");
280
281        assert_eq!(error.to_string(), "event channel closed");
282    }
283
284    // r[verify output.send.message]
285    // r[verify output.send.async]
286    #[tokio::test]
287    async fn message_sends_message_event() {
288        let (sender, mut receiver) = event_channel();
289        let output = Output::new(sender);
290
291        output.message("hello").await.expect("should send");
292
293        let event = receiver.recv().await.expect("should receive");
294
295        assert!(matches!(event, Event::Message(ref s) if s == "hello"));
296    }
297
298    // r[verify output.send.error]
299    #[tokio::test]
300    async fn message_to_closed_channel_returns_error() {
301        let (sender, receiver) = event_channel();
302        let output = Output::new(sender);
303        drop(receiver);
304
305        let error = output.message("lost").await.expect_err("should fail");
306
307        assert_eq!(error.to_string(), "event channel closed");
308    }
309
310    #[tokio::test]
311    async fn message_with_format_args_sends_formatted_string() {
312        let (sender, mut receiver) = event_channel();
313        let output = Output::new(sender);
314
315        output
316            .message(format!("count: {}", 42))
317            .await
318            .expect("should send");
319
320        let event = receiver.recv().await.expect("should receive");
321
322        assert!(matches!(event, Event::Message(ref s) if s == "count: 42"));
323    }
324
325    #[test]
326    fn trait_send() {
327        fn assert_send<T: Send>() {}
328        assert_send::<Output>();
329    }
330
331    #[test]
332    fn trait_sync() {
333        fn assert_sync<T: Sync>() {}
334        assert_sync::<Output>();
335    }
336
337    #[test]
338    fn trait_unpin() {
339        fn assert_unpin<T: Unpin>() {}
340        assert_unpin::<Output>();
341    }
342}