Skip to main content

shep_core/protocol/request/
handshake.rs

1//! The opening frame pair: a client says hello, the daemon answers.
2
3use serde::{Deserialize, Serialize};
4
5/// Client's opening frame
6///
7/// No `deny_unknown_fields`: refusing an unknown field here would refuse a
8/// newer client before `protocol` is read.
9// wire format: changing this is a breaking change
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct Hello {
12    /// Client crate version (semver string)
13    pub client_version: String,
14    /// [`crate::protocol::PROTOCOL_VERSION`] the client speaks
15    pub protocol: u32,
16    /// The name this client was registered under as a dog, when it is one.
17    ///
18    /// `None` for every other client; a bare `Client` cannot set it. The
19    /// daemon needs it to name a dog it refuses at the handshake, which never
20    /// reaches `Request::DogConfig`. A dog reads its own name from
21    /// `$SHEP_DOG_NAME`.
22    ///
23    /// Absent on the wire rather than `null`, so
24    /// [`crate::protocol::PROTOCOL_VERSION`] does not move for it.
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub dog_name: Option<String>,
27}
28
29/// Daemon's handshake answer
30// wire format: changing this is a breaking change
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct HelloAck {
33    /// Daemon crate version
34    pub daemon_version: String,
35    /// Protocol version the daemon speaks
36    pub protocol: u32,
37    /// Daemon pid
38    pub pid: u32,
39    /// The oldest protocol this daemon accepts, or `None` from a daemon
40    /// predating the floor.
41    ///
42    /// Absent rather than `null` on the wire, so it does not move
43    /// [`crate::protocol::PROTOCOL_VERSION`].
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub min_supported: Option<u32>,
46}
47
48#[cfg(test)]
49mod tests {
50    use super::super::{HelloReply, RpcError, RpcErrorCode};
51    use super::*;
52    use crate::protocol::PROTOCOL_VERSION;
53
54    #[test]
55    fn hello_handshake_shape() {
56        let hello = Hello {
57            client_version: "0.1.0".to_string(),
58            protocol: PROTOCOL_VERSION,
59            dog_name: None,
60        };
61        let json = serde_json::to_string(&hello).unwrap();
62        assert_eq!(json, r#"{"client_version":"0.1.0","protocol":11}"#);
63    }
64
65    #[test]
66    fn a_dogs_hello_names_the_dog_and_nothing_elses_does() {
67        let dog = Hello {
68            client_version: "0.1.0".to_string(),
69            protocol: PROTOCOL_VERSION,
70            dog_name: Some("metrics".to_string()),
71        };
72        let json = serde_json::to_string(&dog).unwrap();
73        assert_eq!(
74            json,
75            r#"{"client_version":"0.1.0","protocol":11,"dog_name":"metrics"}"#
76        );
77        assert_eq!(serde_json::from_str::<Hello>(&json).unwrap(), dog);
78    }
79
80    /// `Hello` is the version-negotiation frame, so `deny_unknown_fields`
81    /// here would refuse a newer client before `protocol` is read, leaving
82    /// neither peer able to report the skew.
83    #[test]
84    fn a_hello_without_a_dog_name_still_parses() {
85        let fixture = r#"{"client_version":"0.1.14","protocol":2}"#;
86        let hello: Hello = serde_json::from_str(fixture).unwrap();
87        assert_eq!(hello.protocol, 2);
88        assert_eq!(hello.dog_name, None);
89
90        // The other direction: an older daemon ignores a key it does not
91        // know. `unknown_to_an_older_daemon` stands in for `dog_name`.
92        let newer = r#"{"client_version":"9.9.9","protocol":2,"dog_name":"metrics","unknown_to_an_older_daemon":true}"#;
93        let hello: Hello = serde_json::from_str(newer).unwrap();
94        assert_eq!(hello.protocol, 2);
95        assert_eq!(hello.dog_name.as_deref(), Some("metrics"));
96    }
97
98    #[test]
99    fn hello_ack_handshake_shape() {
100        let ack = HelloAck {
101            daemon_version: "0.5.0".to_string(),
102            protocol: PROTOCOL_VERSION,
103            pid: 1234,
104            min_supported: Some(crate::protocol::MIN_SUPPORTED),
105        };
106        let json = serde_json::to_string(&ack).unwrap();
107        assert_eq!(
108            json,
109            r#"{"daemon_version":"0.5.0","protocol":11,"pid":1234,"min_supported":8}"#
110        );
111        assert_eq!(serde_json::from_str::<HelloAck>(&json).unwrap(), ack);
112    }
113
114    /// `min_supported` is `None` from a daemon predating the floor, and the
115    /// omission has to be a missing key rather than `null`, or it would move
116    /// `PROTOCOL_VERSION` for every daemon that already ships one.
117    #[test]
118    fn hello_ack_without_min_supported_omits_the_key_not_nulls_it() {
119        let ack = HelloAck {
120            daemon_version: "0.5.0".to_string(),
121            protocol: PROTOCOL_VERSION,
122            pid: 1234,
123            min_supported: None,
124        };
125        let json = serde_json::to_string(&ack).unwrap();
126        assert_eq!(
127            json,
128            r#"{"daemon_version":"0.5.0","protocol":11,"pid":1234}"#
129        );
130        assert!(!json.contains("min_supported"));
131    }
132
133    /// An old daemon fixture, from before the floor existed, still decodes.
134    #[test]
135    fn an_old_hello_ack_without_min_supported_still_parses() {
136        let fixture = r#"{"daemon_version":"0.1.14","protocol":2,"pid":9}"#;
137        let ack: HelloAck = serde_json::from_str(fixture).unwrap();
138        assert_eq!(ack.protocol, 2);
139        assert_eq!(ack.min_supported, None);
140    }
141
142    #[test]
143    fn hello_reply_carries_typed_skew_error() {
144        let refusal: HelloReply = Err(RpcError {
145            code: RpcErrorCode::ProtocolMismatch,
146            message: "daemon speaks protocol 1, client sent 2".to_string(),
147            daemon_version: None,
148        });
149        let json = serde_json::to_string(&refusal).unwrap();
150        assert_eq!(
151            json,
152            r#"{"Err":{"code":"protocol_mismatch","message":"daemon speaks protocol 1, client sent 2"}}"#
153        );
154        let back: HelloReply = serde_json::from_str(&json).unwrap();
155        assert_eq!(back, refusal);
156    }
157
158    #[test]
159    fn v1_hello_ack_fixture_still_deserializes() {
160        let fixture = r#"{"Ok":{"daemon_version":"0.1.0","protocol":1,"pid":4242}}"#;
161        let ack: HelloReply = serde_json::from_str(fixture).unwrap();
162        assert_eq!(ack.unwrap().pid, 4242);
163    }
164}