Skip to main content

agent_first_http/shared/
envelope.rs

1//! Protocol envelope writer.
2//!
3//! Every `afhttp` command emits one structured value per invocation: a
4//! single line of JSON, wrapped with a top-level `code` field (`"fetch"`,
5//! `"health"`, `"error"`, etc.) and a trailing newline. JSON is the only
6//! output format — one request in, one line of structured JSON out.
7
8use serde::Serialize;
9use std::io::Write;
10
11use crate::shared::error::Error;
12
13/// Emit a single envelope payload to `writer`. The payload is wrapped with
14/// a top-level `code` field and written as one line of JSON followed by a
15/// newline.
16///
17/// Uses `serde_json::to_writer` and never panics on well-formed input — but
18/// we still funnel through this single seam so `print_stdout` /
19/// `print_stderr` stay clippy-denied at crate level.
20pub fn emit<W: Write, T: Serialize>(writer: &mut W, code: &str, payload: &T) -> Result<(), Error> {
21    let mut value = serde_json::to_value(payload).map_err(|e| {
22        Error::new(
23            crate::shared::error::ErrorCode::InternalError,
24            format!("envelope: failed to serialize payload: {e}"),
25        )
26    })?;
27    if let serde_json::Value::Object(ref mut map) = value {
28        map.insert("code".into(), serde_json::Value::String(code.to_string()));
29    } else {
30        return Err(Error::new(
31            crate::shared::error::ErrorCode::InternalError,
32            "envelope: payload must serialize to a JSON object",
33        ));
34    }
35
36    serde_json::to_writer(&mut *writer, &value).map_err(|e| {
37        Error::new(
38            crate::shared::error::ErrorCode::IoError,
39            format!("envelope: write failed: {e}"),
40        )
41    })?;
42    writer.write_all(b"\n")?;
43    Ok(())
44}
45
46/// Convenience: emit an `Error` value with `code: "error"`.
47pub fn emit_error<W: Write>(writer: &mut W, err: &Error) -> Result<(), Error> {
48    emit(writer, "error", err)
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[derive(Serialize)]
56    struct HealthPayload {
57        status: &'static str,
58        uptime_s: u64,
59    }
60
61    #[test]
62    fn json_envelope_is_single_line_with_code_field() {
63        let mut buf = Vec::new();
64        let payload = HealthPayload {
65            status: "ok",
66            uptime_s: 42,
67        };
68        emit(&mut buf, "health", &payload).unwrap();
69        let s = String::from_utf8(buf).unwrap_or_default();
70        assert!(s.ends_with('\n'));
71        let trimmed = s.trim_end();
72        let parsed: serde_json::Value = serde_json::from_str(trimmed).unwrap();
73        assert_eq!(parsed["code"], "health");
74        assert_eq!(parsed["status"], "ok");
75        assert_eq!(parsed["uptime_s"], 42);
76        assert_eq!(trimmed.lines().count(), 1);
77    }
78
79    #[test]
80    fn error_envelope_uses_error_code_tag() {
81        let mut buf = Vec::new();
82        let err = Error::new(
83            crate::shared::error::ErrorCode::NavigationTimeout,
84            "no load",
85        );
86        emit_error(&mut buf, &err).unwrap();
87        let parsed: serde_json::Value =
88            serde_json::from_slice(&buf).unwrap_or(serde_json::Value::Null);
89        assert_eq!(parsed["code"], "error");
90        assert_eq!(parsed["error_code"], "navigation_timeout");
91        assert_eq!(parsed["error"], "no load");
92        assert_eq!(parsed["retryable"], true);
93    }
94}