Skip to main content

shep_core/protocol/
frame.rs

1//! Anything the daemon writes to a connected client
2//!
3//! The server sends two kinds of frames on one socket: [`Reply`] (answers to
4//! requests) and [`BusEvent`] (broadcast events). This type decodes either,
5//! untagged, because their JSON key sets are disjoint (`id`/`result` vs
6//! `event`), at zero cost to the wire.
7//!
8//! Deserialization needs `serde/std` for buffered content semantics, available
9//! via `serde_json`'s `std` feature (already enabled workspace-wide).
10
11use serde::{Deserialize, Serialize};
12
13use crate::protocol::{BusEvent, Reply};
14
15/// Anything the daemon writes to a connected client
16///
17/// Round-trips to byte-identical output, since the daemon serializes
18/// `Reply`/`BusEvent` directly (pinned by `server_frame_is_byte_identical`).
19// wire format: changing existing variants is a breaking change
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21#[serde(untagged)]
22// Growth is anticipated: a future frame kind (progress, flow control) is
23// additive here and stays additive on the wire.
24#[non_exhaustive]
25pub enum ServerFrame {
26    /// An answer to one request (from [`Envelope`](crate::protocol::Envelope))
27    Reply(Reply),
28    /// One subscribed bus event
29    Event(BusEvent),
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35    use crate::protocol::{
36        BusEvent, ProcessEventKind, ProcessInfo, Reply, Response, RpcError, RpcErrorCode,
37        encode_frame,
38    };
39    use crate::status::ProcStatus;
40
41    fn sample_reply() -> Reply {
42        Reply {
43            id: 7,
44            result: Ok(Response::Pong),
45        }
46    }
47
48    fn sample_event() -> BusEvent {
49        BusEvent::Process {
50            event: ProcessEventKind::Online,
51            info: ProcessInfo {
52                id: 3,
53                name: "web".to_string(),
54                status: ProcStatus::Online,
55                pid: Some(4242),
56                restarts: 0,
57                uptime_ms: 0,
58                fold: None,
59                depends_on: Vec::new(),
60                out_file: Some("/home/ada/.shep/logs/web-0-out.log".to_string()),
61                err_file: Some("/home/ada/.shep/logs/web-0-err.log".to_string()),
62                cpu_percent: None,
63                memory_bytes: None,
64                cpu_ms: None,
65                dog: None,
66                lambs: None,
67                last_exit: None,
68                smit: None,
69                instance: None,
70                handshook: None,
71                dog_stale: None,
72                pending: None,
73                overridden: None,
74                max_memory: None,
75                level_rules: Vec::new(),
76                reload_deadline_ms: None,
77            },
78            manually: false,
79            at_ms: 1_700_000_000_000,
80        }
81    }
82
83    #[test]
84    fn server_frame_decodes_both_directions_of_the_stream() {
85        // The two shapes are disjoint: a Reply has no `event` key and an
86        // event has no `id`/`result` pair, so untagged never guesses wrong.
87        let reply = r#"{"id":1,"result":{"Ok":{"kind":"pong"}}}"#;
88        assert!(matches!(
89            serde_json::from_str::<ServerFrame>(reply).unwrap(),
90            ServerFrame::Reply(Reply { id: 1, .. })
91        ));
92        let event = r#"{"event":"log_out","data":{"id":3,"line":"ready"}}"#;
93        assert!(matches!(
94            serde_json::from_str::<ServerFrame>(event).unwrap(),
95            ServerFrame::Event(BusEvent::LogOut { id: 3, .. })
96        ));
97    }
98
99    #[test]
100    fn server_frame_is_byte_identical_to_its_payload() {
101        // The daemon encodes Reply/BusEvent directly; if wrapping ever
102        // started adding bytes, every client would break at once.
103        let reply = sample_reply();
104        assert_eq!(
105            encode_frame(&ServerFrame::Reply(reply.clone())).unwrap(),
106            encode_frame(&reply).unwrap()
107        );
108        let event = sample_event();
109        assert_eq!(
110            encode_frame(&ServerFrame::Event(event.clone())).unwrap(),
111            encode_frame(&event).unwrap()
112        );
113    }
114
115    #[test]
116    fn an_error_reply_still_decodes_as_a_reply_frame() {
117        let err = Reply {
118            id: 2,
119            result: Err(RpcError {
120                code: RpcErrorCode::DeadlineExceeded,
121                message: "request deadline of 5000 ms expired".to_string(),
122                daemon_version: None,
123            }),
124        };
125        let json = serde_json::to_string(&err).unwrap();
126        assert_eq!(
127            serde_json::from_str::<ServerFrame>(&json).unwrap(),
128            ServerFrame::Reply(err)
129        );
130    }
131}