1mod emitter;
10
11use std::io::Write;
12use std::sync::{Mutex, PoisonError};
13
14use isoloom_manifest::OutputRole;
15use serde::{Deserialize, Serialize};
16
17pub(crate) use emitter::Emitter;
18
19use crate::error::ErrorCode;
20use crate::state::{Op, RunState};
21
22pub const EVENTS_VERSION: u32 = 0;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
27#[serde(rename_all = "snake_case")]
28#[non_exhaustive]
29pub enum StepState {
30 Running,
32 Done,
34 Failed,
36 Skipped,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
42#[serde(rename_all = "snake_case")]
43#[non_exhaustive]
44pub enum Stream {
45 Stdout,
47 Stderr,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
53#[serde(rename_all = "snake_case")]
54#[non_exhaustive]
55pub enum ReadyState {
56 Waiting,
58 Ok,
60 Failed,
62}
63
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
66#[serde(tag = "event", rename_all = "snake_case")]
67#[non_exhaustive]
68pub enum EventKind {
69 State {
71 from: RunState,
73 to: RunState,
75 },
76 Step {
78 step: String,
80 state: StepState,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
84 note: Option<String>,
85 },
86 Log {
88 step: String,
90 stream: Stream,
92 line: String,
94 },
95 Ready {
97 condition: String,
99 state: ReadyState,
101 detail: String,
103 },
104 Output {
106 name: String,
108 role: OutputRole,
110 value: Option<String>,
112 sensitive: bool,
114 },
115 Check {
117 runner: String,
119 from: String,
121 name: String,
123 ok: bool,
125 detail: String,
127 },
128 Warning {
130 code: WarningCode,
132 message: String,
134 },
135 Result {
137 ok: bool,
139 data: serde_json::Value,
141 },
142 Error {
144 code: ErrorCode,
146 message: String,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
150 step: Option<String>,
151 },
152}
153
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
156#[non_exhaustive]
157pub struct Event {
158 pub v: u32,
160 pub seq: u64,
162 pub time: String,
164 pub run: String,
166 pub op: Op,
168 #[serde(flatten)]
170 pub kind: EventKind,
171}
172
173pub trait Events: Send + Sync {
175 fn emit(&self, event: &Event);
177}
178
179impl<F: Fn(&Event) + Send + Sync> Events for F {
180 fn emit(&self, event: &Event) {
181 self(event);
182 }
183}
184
185#[derive(Debug)]
187pub struct JsonLines<W>(Mutex<W>);
188
189impl<W: Write + Send> JsonLines<W> {
190 pub fn new(w: W) -> JsonLines<W> {
192 JsonLines(Mutex::new(w))
193 }
194}
195
196impl<W: Write + Send> Events for JsonLines<W> {
197 fn emit(&self, event: &Event) {
198 let mut w = self.0.lock().unwrap_or_else(PoisonError::into_inner);
199 let written = serde_json::to_string(event)
200 .map_err(std::io::Error::from)
201 .and_then(|text| writeln!(w, "{text}"))
202 .and_then(|()| w.flush());
203 if let Err(e) = written {
206 tracing::debug!(error = %e, "an event wasn't written");
207 }
208 }
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
213#[serde(rename_all = "snake_case")]
214#[non_exhaustive]
215pub enum WarningCode {
216 StepTolerated,
218 HookFailed,
220 CheckRunnerFailed,
222 CheckCleanupFailed,
224 LeftoversRemoved,
226 OrphanRemoved,
228 OtherProvider,
230 SavedStateDiscarded,
232 Recovering,
234 Retrying,
236}
237
238#[derive(Debug, Default)]
240pub struct Recorder(Mutex<Vec<Event>>);
241
242impl Recorder {
243 #[must_use]
245 pub fn new() -> Recorder {
246 Recorder::default()
247 }
248
249 pub fn events(&self) -> Vec<Event> {
251 self.0.lock().unwrap_or_else(PoisonError::into_inner).clone()
252 }
253}
254
255impl Events for Recorder {
256 fn emit(&self, event: &Event) {
257 self.0.lock().unwrap_or_else(PoisonError::into_inner).push(event.clone());
258 }
259}
260
261#[must_use]
263pub fn events_schema() -> serde_json::Value {
264 let mut schema = serde_json::to_value(schemars::schema_for!(Event)).unwrap_or_default();
265 if let Some(fields) = schema.as_object_mut() {
266 fields.insert("title".to_owned(), serde_json::json!("Isothrum runner event, version 0"));
267 }
268 schema
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 fn event(kind: EventKind) -> Event {
276 Event {
277 v: 0,
278 seq: 1,
279 time: "2026-10-15T12:00:00Z".into(),
280 run: "flag-portal".into(),
281 op: Op::Up,
282 kind,
283 }
284 }
285
286 #[test]
287 fn an_event_has_the_documented_shape() {
288 let e = event(EventKind::Step {
289 step: "up".into(),
290 state: StepState::Running,
291 note: Some("Starting…".into()),
292 });
293 assert_eq!(
294 serde_json::to_value(&e).unwrap(),
295 serde_json::json!({"v":0,"seq":1,"time":"2026-10-15T12:00:00Z","run":"flag-portal","op":"up","event":"step","step":"up","state":"running","note":"Starting…"})
296 );
297 let error = event(EventKind::Error {
298 code: ErrorCode::PortInUse,
299 message: "m".into(),
300 step: None,
301 });
302 let back: Event = serde_json::from_value(serde_json::to_value(&error).unwrap()).unwrap();
303 assert_eq!(back, error);
304 }
305
306 #[test]
307 fn json_lines_are_one_event_per_line() {
308 let sink = JsonLines::new(Vec::new());
309 sink.emit(&event(EventKind::State {
310 from: RunState::Absent,
311 to: RunState::Starting,
312 }));
313 sink.emit(&event(EventKind::Result {
314 ok: true,
315 data: serde_json::json!({}),
316 }));
317 let text = String::from_utf8(sink.0.into_inner().unwrap()).unwrap();
318 let lines: Vec<Event> = text.lines().map(|l| serde_json::from_str(l).unwrap()).collect();
319 assert_eq!(lines.len(), 2);
320 }
321
322 #[test]
323 fn the_schema_names_every_event_kind() {
324 let schema = events_schema().to_string();
325 for kind in ["state", "step", "log", "ready", "output", "check", "warning", "result", "error"] {
326 assert!(schema.contains(&format!("\"{kind}\"")), "{kind}");
327 }
328 }
329}