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}