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}