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
13use std::fmt::{Debug, Display};
14
15use serde::Serialize;
16
17pub use self::channel::{SendError, event_channel};
18pub use self::receiver::EventReceiver;
19pub use self::sender::EventSender;
20
21/// Bounded channel that carries events from a command to its presenter
22mod channel;
23/// The half of the event channel that reads events
24mod receiver;
25/// The half of the event channel that sends events
26mod sender;
27
28/// Trait for artifact values that can be rendered as text or JSON
29///
30/// `Artifact` combines [`Display`] (for text rendering), [`Serialize`] (for JSON rendering), and
31/// [`Debug`] (for diagnostics). The Presenter uses the appropriate trait based on its output mode.
32///
33/// Command authors do not implement this trait directly. A blanket implementation covers any type
34/// that satisfies the required bounds.
35///
36/// # Examples
37///
38/// ```
39/// use std::fmt;
40///
41/// use serde::Serialize;
42///
43/// #[derive(Clone, Debug, Serialize)]
44/// struct UserCount {
45///     count: usize,
46/// }
47///
48/// impl fmt::Display for UserCount {
49///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50///         write!(f, "{} users", self.count)
51///     }
52/// }
53///
54/// // UserCount automatically implements Artifact — no manual impl needed.
55/// let artifact: Box<dyn clawless_core::event::Artifact> = Box::new(UserCount { count: 42 });
56/// assert_eq!(artifact.to_string(), "42 users");
57/// ```
58///
59// r[impl event.artifact.structured]
60// r[impl event.artifact.text]
61pub trait Artifact: Display + Debug + Send + Sync + erased_serde::Serialize {}
62
63// r[impl event.artifact.zero-cost]
64impl<T> Artifact for T where T: Display + Serialize + Debug + Send + Sync + 'static {}
65
66erased_serde::serialize_trait_object!(Artifact);
67
68/// Structured output event produced by commands
69///
70/// An `Event` represents a single piece of output that a command has produced. Events travel from
71/// the producer through an async channel to the Presenter, decoupling production from rendering.
72///
73/// Three variants:
74///
75/// - [`Message`] — informational text (shown at default verbosity and above).
76/// - [`Detail`] — supplementary text (shown only at verbose verbosity).
77/// - [`Event::Artifact`] — the primary data a command produces, carried as a trait object that the
78///   Presenter can render via [`Display`] or [`Serialize`].
79///
80/// The Presenter decides which events to render based on its verbosity setting.
81///
82/// [`Detail`]: Event::Detail
83/// [`Message`]: Event::Message
84// r[impl event.safety.event-send]
85#[derive(Debug)]
86pub enum Event {
87    /// Informational message
88    // r[impl event.output.message]
89    Message(String),
90    /// Supplementary detail
91    // r[impl event.output.detail]
92    Detail(String),
93    /// Primary command output
94    // r[impl event.output.artifact]
95    Artifact(Box<dyn Artifact>),
96}
97
98#[cfg(test)]
99mod tests {
100    // An assertion in a test panics by design. A `# Panics` section on every test
101    // would repeat that and give the reader no information.
102    #![allow(clippy::missing_panics_doc)]
103
104    use std::fmt;
105
106    use serde::Serialize;
107
108    use super::*;
109
110    #[derive(Clone, Debug, Serialize)]
111    struct TestArtifact {
112        value: String,
113    }
114
115    impl fmt::Display for TestArtifact {
116        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117            write!(f, "{}", self.value)
118        }
119    }
120
121    fn test_artifact() -> TestArtifact {
122        TestArtifact {
123            value: "hello".to_string(),
124        }
125    }
126
127    #[test]
128    fn artifact_debug_delegates_to_inner_type() {
129        let event = Event::Artifact(Box::new(test_artifact()));
130
131        let debug = format!("{event:?}");
132
133        assert!(debug.contains("hello"));
134    }
135
136    // r[verify event.artifact.text]
137    #[test]
138    fn artifact_display_renders_via_display_trait() {
139        let boxed: Box<dyn Artifact> = Box::new(test_artifact());
140
141        let display = boxed.to_string();
142
143        assert_eq!(display, "hello");
144    }
145
146    // r[verify event.artifact.structured]
147    #[test]
148    fn artifact_serializes_via_erased_serde() {
149        let boxed: Box<dyn Artifact> = Box::new(test_artifact());
150
151        let json = serde_json::to_string(&boxed).expect("should serialize");
152
153        assert_eq!(json, r#"{"value":"hello"}"#);
154    }
155
156    // r[verify event.output.detail]
157    #[test]
158    fn detail_with_empty_string_is_valid() {
159        let event = Event::Detail(String::new());
160
161        let debug = format!("{event:?}");
162
163        assert!(debug.contains("Detail"));
164    }
165
166    // r[verify event.output.message]
167    #[test]
168    fn message_with_empty_string_is_valid() {
169        let event = Event::Message(String::new());
170
171        let debug = format!("{event:?}");
172
173        assert!(debug.contains("Message"));
174    }
175
176    // r[verify event.safety.event-send]
177    #[test]
178    fn trait_send() {
179        fn assert_send<T: Send>() {}
180        assert_send::<Event>();
181    }
182
183    #[test]
184    fn trait_sync() {
185        fn assert_sync<T: Sync>() {}
186        assert_sync::<Event>();
187    }
188
189    #[test]
190    fn trait_unpin() {
191        fn assert_unpin<T: Unpin>() {}
192        assert_unpin::<Event>();
193    }
194}