1use serde::{Deserialize, Serialize};
14
15pub const CHANNEL_VERSION: &str = "1";
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25#[serde(tag = "kind", rename_all = "kebab-case")]
26pub enum ChildMessage {
27 Ready,
29 Metric {
31 name: String,
33 value: f64,
35 },
36 ActionReply {
38 action: String,
40 body: String,
42 #[serde(skip_serializing_if = "Option::is_none", default)]
46 id: Option<u64>,
47 },
48 LambLabel {
53 pid: u32,
55 label: LambLabel,
57 },
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(try_from = "String", into = "String")]
68pub struct LambLabel(String);
69
70impl LambLabel {
71 pub const MAX_CHARS: usize = 64;
75
76 pub fn new(label: impl Into<String>) -> Result<Self, LambLabelError> {
85 let label = label.into();
86 let chars = label.chars().count();
87 if chars > Self::MAX_CHARS {
88 return Err(LambLabelError::TooLong { chars });
89 }
90 if label.chars().any(char::is_control) {
91 return Err(LambLabelError::ControlCharacter);
92 }
93 Ok(Self(label))
94 }
95
96 #[must_use]
98 pub fn as_str(&self) -> &str {
99 &self.0
100 }
101
102 #[must_use]
104 pub fn is_empty(&self) -> bool {
105 self.0.is_empty()
106 }
107}
108
109impl TryFrom<String> for LambLabel {
110 type Error = LambLabelError;
111
112 fn try_from(label: String) -> Result<Self, Self::Error> {
113 Self::new(label)
114 }
115}
116
117impl From<LambLabel> for String {
118 fn from(label: LambLabel) -> Self {
119 label.0
120 }
121}
122
123#[non_exhaustive]
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub enum LambLabelError {
128 TooLong {
130 chars: usize,
132 },
133 ControlCharacter,
136}
137
138impl core::fmt::Display for LambLabelError {
139 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
140 match self {
141 Self::TooLong { chars } => write!(
142 f,
143 "a lamb label holds at most {} characters, and this one has {chars}",
144 LambLabel::MAX_CHARS
145 ),
146 Self::ControlCharacter => f.write_str("a lamb label cannot hold a control character"),
147 }
148 }
149}
150
151impl core::error::Error for LambLabelError {}
152
153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
156#[serde(tag = "kind", rename_all = "kebab-case")]
157pub enum ShepherdMessage {
158 Shutdown,
160 Action {
162 name: String,
164 #[serde(skip_serializing_if = "Option::is_none", default)]
176 params: Option<String>,
177 id: u64,
185 },
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
196 fn ready_wire_fixture_round_trips() {
197 let fixture = r#"{"kind":"ready"}"#;
198 assert_eq!(
199 serde_json::from_str::<ChildMessage>(fixture).unwrap(),
200 ChildMessage::Ready
201 );
202 assert_eq!(
203 serde_json::to_string(&ChildMessage::Ready).unwrap(),
204 fixture
205 );
206 }
207
208 #[test]
209 fn metric_wire_fixture_round_trips() {
210 let fixture = r#"{"kind":"metric","name":"rps","value":42.0}"#;
211 let msg = ChildMessage::Metric {
212 name: "rps".to_string(),
213 value: 42.0,
214 };
215 assert_eq!(serde_json::from_str::<ChildMessage>(fixture).unwrap(), msg);
216 assert_eq!(serde_json::to_string(&msg).unwrap(), fixture);
217 }
218
219 #[test]
220 fn an_action_reply_without_an_id_round_trips() {
221 let fixture = r#"{"kind":"action-reply","action":"gc","body":"ok"}"#;
222 let msg = ChildMessage::ActionReply {
223 action: "gc".to_string(),
224 body: "ok".to_string(),
225 id: None,
226 };
227 assert_eq!(serde_json::from_str::<ChildMessage>(fixture).unwrap(), msg);
228 assert_eq!(serde_json::to_string(&msg).unwrap(), fixture);
229 }
230
231 #[test]
232 fn an_action_reply_with_an_echoed_id_round_trips() {
233 let fixture = r#"{"kind":"action-reply","action":"gc","body":"ok","id":7}"#;
234 let msg = ChildMessage::ActionReply {
235 action: "gc".to_string(),
236 body: "ok".to_string(),
237 id: Some(7),
238 };
239 assert_eq!(serde_json::from_str::<ChildMessage>(fixture).unwrap(), msg);
240 assert_eq!(serde_json::to_string(&msg).unwrap(), fixture);
241 }
242
243 #[test]
244 fn shutdown_wire_fixture_round_trips() {
245 let fixture = r#"{"kind":"shutdown"}"#;
246 assert_eq!(
247 serde_json::from_str::<ShepherdMessage>(fixture).unwrap(),
248 ShepherdMessage::Shutdown
249 );
250 assert_eq!(
251 serde_json::to_string(&ShepherdMessage::Shutdown).unwrap(),
252 fixture
253 );
254 }
255
256 #[test]
258 fn an_action_carries_its_id_with_or_without_params() {
259 let bare = r#"{"kind":"action","name":"gc","id":7}"#;
260 let bare_msg = ShepherdMessage::Action {
261 name: "gc".to_string(),
262 params: None,
263 id: 7,
264 };
265 assert_eq!(serde_json::to_string(&bare_msg).unwrap(), bare);
266 assert_eq!(
267 serde_json::from_str::<ShepherdMessage>(bare).unwrap(),
268 bare_msg
269 );
270
271 let with_params = r#"{"kind":"action","name":"set-log-level","params":"debug","id":8}"#;
272 let with_params_msg = ShepherdMessage::Action {
273 name: "set-log-level".to_string(),
274 params: Some("debug".to_string()),
275 id: 8,
276 };
277 assert_eq!(
278 serde_json::to_string(&with_params_msg).unwrap(),
279 with_params
280 );
281 assert_eq!(
282 serde_json::from_str::<ShepherdMessage>(with_params).unwrap(),
283 with_params_msg
284 );
285 }
286
287 #[test]
288 fn a_lamb_label_round_trips() {
289 let fixture = r#"{"kind":"lamb-label","pid":4312,"label":"worker 1"}"#;
290 let msg = ChildMessage::LambLabel {
291 pid: 4312,
292 label: LambLabel::new("worker 1").unwrap(),
293 };
294 assert_eq!(serde_json::from_str::<ChildMessage>(fixture).unwrap(), msg);
295 assert_eq!(serde_json::to_string(&msg).unwrap(), fixture);
296 }
297
298 #[test]
300 fn an_empty_lamb_label_is_on_the_wire_and_clears() {
301 let fixture = r#"{"kind":"lamb-label","pid":4312,"label":""}"#;
302 let ChildMessage::LambLabel { label, .. } =
303 serde_json::from_str::<ChildMessage>(fixture).unwrap()
304 else {
305 panic!("{fixture} decoded as another kind");
306 };
307 assert!(label.is_empty());
308 assert!(
309 serde_json::from_str::<ChildMessage>(r#"{"kind":"lamb-label","pid":4312}"#).is_err()
310 );
311 }
312
313 #[test]
314 fn a_label_is_counted_in_characters_up_to_the_limit() {
315 let at_limit = "é".repeat(LambLabel::MAX_CHARS);
316 assert_eq!(LambLabel::new(at_limit.clone()).unwrap().as_str(), at_limit);
317 assert_eq!(
318 LambLabel::new(format!("{at_limit}e")),
319 Err(LambLabelError::TooLong {
320 chars: LambLabel::MAX_CHARS + 1
321 })
322 );
323 }
324
325 #[test]
326 fn a_label_with_a_control_character_is_refused_on_the_wire_too() {
327 for label in ["a\nb", "\u{1b}[2J", "tab\there", "\u{9b}31m"] {
328 assert_eq!(
329 LambLabel::new(label),
330 Err(LambLabelError::ControlCharacter),
331 "{label:?}"
332 );
333 let frame = serde_json::json!({"kind": "lamb-label", "pid": 1, "label": label});
334 assert!(
335 serde_json::from_value::<ChildMessage>(frame).is_err(),
336 "{label:?} decoded"
337 );
338 }
339 }
340
341 #[test]
342 fn a_label_error_says_the_limit_and_the_length() {
343 assert_eq!(
344 LambLabelError::TooLong { chars: 70 }.to_string(),
345 "a lamb label holds at most 64 characters, and this one has 70"
346 );
347 }
348}