Skip to main content

clawless_core/event/
mod.rs

1//! Event types for structured command output
2//!
3//! This module defines [`Event`], the structured message type that commands produce and the
4//! Presenter consumes. Events decouple output production from rendering: a command emits events
5//! through Output, and the Presenter decides how to render them based on its output mode and
6//! verbosity settings.
7//!
8//! The [`Artifact`] trait enables the [`Event::Artifact`] variant to carry a type-erased value that
9//! supports both text rendering ([`Display`]) and JSON serialization ([`Serialize`]). A blanket
10//! implementation covers any type satisfying the required bounds, so command authors derive the
11//! usual traits and pass values to Output without manual trait implementation.
12//!
13//! The [`Event::Process`] variant carries the [`ProcessEvent`] of an external program that a
14//! command runs. Those events arrive while the program runs, which lets a presenter show progress
15//! instead of waiting for the program to end.
16//!
17//! The [`Event::Prompt`] variant carries a [`PromptRequest`], which is a question for the user.
18//! The request arrives after the events that the command sent before it.
19//!
20//! [`ProcessEvent`]: process::ProcessEvent
21//! [`PromptRequest`]: prompt::PromptRequest
22
23use std::fmt::{Debug, Display};
24
25use serde::Serialize;
26
27pub use self::channel::{SendError, event_channel};
28use self::process::ProcessEvent;
29use self::prompt::PromptRequest;
30pub use self::receiver::EventReceiver;
31pub use self::sender::EventSender;
32
33pub mod process;
34pub mod prompt;
35
36/// Bounded channel that carries events from a command to its presenter
37mod channel;
38/// The half of the event channel that reads events
39mod receiver;
40/// The half of the event channel that sends events
41mod sender;
42
43/// Trait for artifact values that can be rendered as text or JSON
44///
45/// `Artifact` combines [`Display`] (for text rendering), [`Serialize`] (for JSON rendering), and
46/// [`Debug`] (for diagnostics). The Presenter uses the appropriate trait based on its output mode.
47///
48/// Command authors do not implement this trait directly. A blanket implementation covers any type
49/// that satisfies the required bounds.
50///
51/// # Examples
52///
53/// ```
54/// use std::fmt;
55///
56/// use serde::Serialize;
57///
58/// #[derive(Clone, Debug, Serialize)]
59/// struct UserCount {
60///     count: usize,
61/// }
62///
63/// impl fmt::Display for UserCount {
64///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65///         write!(f, "{} users", self.count)
66///     }
67/// }
68///
69/// // UserCount automatically implements Artifact — no manual impl needed.
70/// let artifact: Box<dyn clawless_core::event::Artifact> = Box::new(UserCount { count: 42 });
71/// assert_eq!(artifact.to_string(), "42 users");
72/// ```
73///
74// r[impl event.artifact.structured]
75// r[impl event.artifact.text]
76pub trait Artifact: Display + Debug + Send + Sync + erased_serde::Serialize {}
77
78// r[impl event.artifact.zero-cost]
79impl<T> Artifact for T where T: Display + Serialize + Debug + Send + Sync + 'static {}
80
81erased_serde::serialize_trait_object!(Artifact);
82
83/// Structured output event produced by commands
84///
85/// An `Event` represents a single piece of output that a command has produced. Events travel from
86/// the producer through an async channel to the Presenter, decoupling production from rendering.
87///
88/// Five variants:
89///
90/// - [`Message`] — informational text (shown at default verbosity and above).
91/// - [`Detail`] — supplementary text (shown only at verbose verbosity).
92/// - [`Event::Artifact`] — the primary data a command produces, carried as a trait object that the
93///   Presenter can render via [`Display`] or [`Serialize`].
94/// - [`Event::Process`] — one step in the run of an external program that the command started.
95/// - [`Event::Prompt`] — a question for the user, with the reply that answers it.
96///
97/// The Presenter decides which events to render based on its verbosity setting. Verbosity does
98/// not apply to a prompt, because the command waits for the answer. A presenter that cannot ask
99/// the user drops the request.
100///
101/// [`Detail`]: Event::Detail
102/// [`Message`]: Event::Message
103// r[impl event.safety.event-send]
104#[derive(Debug)]
105pub enum Event {
106    /// Informational message
107    // r[impl event.output.message]
108    Message(String),
109    /// Supplementary detail
110    // r[impl event.output.detail]
111    Detail(String),
112    /// Primary command output
113    // r[impl event.output.artifact]
114    Artifact(Box<dyn Artifact>),
115    /// One step in the run of an external program
116    ///
117    /// The event is boxed so that the size of an `Event` stays that of its text variants. A run
118    /// names its command in the events that start and end it, and an unboxed variant would make
119    /// every message in the channel as large as that name.
120    // r[impl event.output.process]
121    Process(Box<ProcessEvent>),
122    /// A question for the user
123    ///
124    /// The request is boxed so that the size of an `Event` stays that of its text variants.
125    Prompt(Box<PromptRequest>),
126}
127
128#[cfg(test)]
129mod tests {
130    // An assertion in a test panics by design. A `# Panics` section on every test
131    // would repeat that and give the reader no information.
132    #![allow(clippy::missing_panics_doc)]
133
134    use std::fmt;
135
136    use serde::Serialize;
137
138    use super::*;
139    use crate::event::process::RunId;
140    use crate::process::Invocation;
141    use crate::prompt::Confirm;
142
143    #[derive(Clone, Debug, Serialize)]
144    struct TestArtifact {
145        value: String,
146    }
147
148    impl fmt::Display for TestArtifact {
149        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150            write!(f, "{}", self.value)
151        }
152    }
153
154    fn test_artifact() -> TestArtifact {
155        TestArtifact {
156            value: "hello".to_string(),
157        }
158    }
159
160    #[test]
161    fn artifact_debug_delegates_to_inner_type() {
162        let event = Event::Artifact(Box::new(test_artifact()));
163
164        let debug = format!("{event:?}");
165
166        assert!(debug.contains("hello"));
167    }
168
169    // r[verify event.artifact.text]
170    #[test]
171    fn artifact_display_renders_via_display_trait() {
172        let boxed: Box<dyn Artifact> = Box::new(test_artifact());
173
174        let display = boxed.to_string();
175
176        assert_eq!(display, "hello");
177    }
178
179    // r[verify event.artifact.structured]
180    #[test]
181    fn artifact_serializes_via_erased_serde() {
182        let boxed: Box<dyn Artifact> = Box::new(test_artifact());
183
184        let json = serde_json::to_string(&boxed).expect("should serialize");
185
186        assert_eq!(json, r#"{"value":"hello"}"#);
187    }
188
189    // r[verify event.output.detail]
190    #[test]
191    fn detail_with_empty_string_is_valid() {
192        let event = Event::Detail(String::new());
193
194        let debug = format!("{event:?}");
195
196        assert!(debug.contains("Detail"));
197    }
198
199    // r[verify event.output.message]
200    #[test]
201    fn message_with_empty_string_is_valid() {
202        let event = Event::Message(String::new());
203
204        let debug = format!("{event:?}");
205
206        assert!(debug.contains("Message"));
207    }
208
209    // r[verify event.output.process]
210    #[test]
211    fn process_carries_the_event_of_a_run() {
212        let id = RunId::next();
213
214        let event = Event::Process(Box::new(ProcessEvent::Started {
215            id,
216            invocation: Invocation::new("git"),
217            process_id: None,
218        }));
219
220        assert_eq!(
221            match event {
222                Event::Process(event) => Some(event.id()),
223                Event::Message(_) => None,
224                Event::Detail(_) => None,
225                Event::Artifact(_) => None,
226                Event::Prompt(_) => None,
227            },
228            Some(id)
229        );
230    }
231
232    #[test]
233    fn prompt_carries_the_request_of_a_command() {
234        let (request, _pending) = PromptRequest::confirm(Confirm::new("Release?"));
235
236        let event = Event::Prompt(Box::new(request));
237
238        assert_eq!(
239            match event {
240                Event::Prompt(request) => Some(request.question().to_owned()),
241                Event::Message(_) => None,
242                Event::Detail(_) => None,
243                Event::Artifact(_) => None,
244                Event::Process(_) => None,
245            },
246            Some("Release?".to_owned())
247        );
248    }
249
250    // r[verify event.safety.event-send]
251    #[test]
252    fn trait_send() {
253        fn assert_send<T: Send>() {}
254        assert_send::<Event>();
255    }
256
257    #[test]
258    fn trait_sync() {
259        fn assert_sync<T: Sync>() {}
260        assert_sync::<Event>();
261    }
262
263    #[test]
264    fn trait_unpin() {
265        fn assert_unpin<T: Unpin>() {}
266        assert_unpin::<Event>();
267    }
268}