objectiveai_cli_sdk/output/handle.rs
1//! Destinations for [`super::Output::emit`].
2//!
3//! - [`Handle::Stdout`] — this process's stdout (with fatal-error
4//! mirror to stderr).
5//! - [`Handle::Stdin`] — a child process's stdin, for programmatic
6//! embedders that spawn a subprocess to consume the cli's output.
7//! - [`Handle::Collect`] — an in-memory shared `Vec`, for tests or
8//! in-process embedders that want the events without a subprocess.
9
10use std::sync::Arc;
11
12use serde::Serialize;
13use tokio::process::ChildStdin;
14use tokio::sync::Mutex;
15
16use super::{Notification, Output};
17
18/// Destination for [`super::Output::emit`].
19///
20/// `Arc<tokio::sync::Mutex<_>>` on the non-unit variants lets the
21/// handle be cloned cheaply across the command tree's call chain and
22/// guarantees concurrent emits serialize correctly. `tokio::sync::Mutex`
23/// (not `std::sync::Mutex`) because we hold the guard across `.await`
24/// boundaries during async writes, which would deadlock with std's.
25#[derive(Clone)]
26pub enum Handle {
27 /// Write each line to this process's stdout; mirror fatal
28 /// `Output::Error` lines to stderr.
29 Stdout,
30 /// Write each line to a child process's stdin.
31 Stdin(Arc<Mutex<ChildStdin>>),
32 /// Push each emitted `Output<T>` (reserialized as `Output<Value>`
33 /// for a uniform storage type) into a shared vector.
34 Collect(Arc<Mutex<Vec<Output<serde_json::Value>>>>),
35}
36
37impl Default for Handle {
38 fn default() -> Self {
39 Handle::Stdout
40 }
41}
42
43impl Handle {
44 /// Emit `output` to this destination. Panics on write failure to
45 /// match `println!` semantics.
46 pub async fn emit<T: Serialize>(&self, output: &Output<T>) {
47 match self {
48 Handle::Stdout => {
49 let json = serde_json::to_string(output)
50 .expect("Output<T> serializes when T: Serialize");
51 println!("{json}");
52 if matches!(output, Output::Error(e) if e.fatal) {
53 eprintln!("{json}");
54 }
55 }
56 Handle::Stdin(stdin) => {
57 let json = serde_json::to_string(output)
58 .expect("Output<T> serializes when T: Serialize");
59 use tokio::io::AsyncWriteExt;
60 let mut guard = stdin.lock().await;
61 guard
62 .write_all(json.as_bytes())
63 .await
64 .expect("emit to child stdin failed");
65 guard
66 .write_all(b"\n")
67 .await
68 .expect("emit to child stdin failed");
69 }
70 Handle::Collect(vec) => {
71 // Rebuild variant-wise as Output<Value> so storage is
72 // uniform without paying a full serialize → deserialize
73 // roundtrip.
74 let typed = match output {
75 Output::Error(e) => Output::Error(e.clone()),
76 Output::Notification(n) => Output::Notification(Notification {
77 value: serde_json::to_value(&n.value)
78 .expect("T serializes when T: Serialize"),
79 }),
80 Output::Begin => Output::Begin,
81 Output::End => Output::End,
82 };
83 vec.lock().await.push(typed);
84 }
85 }
86 }
87}