frust_devtools_protocol/
codec.rs1use serde::Serialize;
7use serde_json::Value;
8
9use crate::types::Incoming;
10
11pub fn encode_line(value: &impl Serialize) -> String {
23 let line = serde_json::to_string(value)
24 .expect("frust-devtools-protocol wire types are always representable as JSON");
25 debug_assert!(
26 !line.contains('\n'),
27 "encode_line must never produce an embedded newline (NDJSON framing contract)"
28 );
29 line
30}
31
32pub fn decode_line(line: &str) -> Result<Incoming, DecodeError> {
41 let value: Value = serde_json::from_str(line.trim_end_matches('\n'))?;
42 let obj = value.as_object().ok_or(DecodeError::NotAnObject)?;
43 let has_method = obj.contains_key("method");
44 let has_id = obj.contains_key("id");
45
46 if has_method && has_id {
47 Ok(Incoming::Request(serde_json::from_value(value)?))
48 } else if has_method {
49 Ok(Incoming::Notification(serde_json::from_value(value)?))
50 } else if has_id {
51 Ok(Incoming::Response(serde_json::from_value(value)?))
52 } else {
53 Err(DecodeError::UnrecognizedShape)
54 }
55}
56
57#[derive(Debug)]
59pub enum DecodeError {
60 InvalidJson(serde_json::Error),
62 NotAnObject,
65 UnrecognizedShape,
68}
69
70impl std::fmt::Display for DecodeError {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 match self {
73 DecodeError::InvalidJson(e) => write!(f, "invalid JSON: {e}"),
74 DecodeError::NotAnObject => write!(f, "line is not a JSON object"),
75 DecodeError::UnrecognizedShape => write!(
76 f,
77 "object has neither a `method` nor an `id` field — not a Request, Response, or Notification"
78 ),
79 }
80 }
81}
82
83impl std::error::Error for DecodeError {
84 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
85 match self {
86 DecodeError::InvalidJson(e) => Some(e),
87 DecodeError::NotAnObject | DecodeError::UnrecognizedShape => None,
88 }
89 }
90}
91
92impl From<serde_json::Error> for DecodeError {
93 fn from(e: serde_json::Error) -> Self {
94 DecodeError::InvalidJson(e)
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::types::{Request, Response};
102
103 #[test]
104 fn encode_line_never_embeds_a_raw_newline() {
105 let req = Request::new(
106 1,
107 "input_text",
108 serde_json::json!({"text": "line one\nline two"}),
109 );
110 let line = encode_line(&req);
111 assert!(
112 !line.contains('\n'),
113 "embedded newline in encoded params must be escaped, not literal: {line:?}"
114 );
115 let decoded: Request = serde_json::from_str(&line).unwrap();
117 assert_eq!(decoded.params["text"], "line one\nline two");
118 }
119
120 #[test]
121 fn decode_line_discriminates_request() {
122 let line = encode_line(&Request::new(1, "handshake", Value::Null));
123 match decode_line(&line).unwrap() {
124 Incoming::Request(r) => assert_eq!(r.method, "handshake"),
125 other => panic!("expected Request, got {other:?}"),
126 }
127 }
128
129 #[test]
130 fn decode_line_discriminates_notification() {
131 let notif = crate::types::Notification::new("frame_stats", serde_json::json!({"n": 1}));
132 let line = encode_line(¬if);
133 match decode_line(&line).unwrap() {
134 Incoming::Notification(n) => assert_eq!(n.method, "frame_stats"),
135 other => panic!("expected Notification, got {other:?}"),
136 }
137 }
138
139 #[test]
140 fn decode_line_discriminates_response() {
141 let resp = Response::success(9, serde_json::json!({"ok": true}));
142 let line = encode_line(&resp);
143 match decode_line(&line).unwrap() {
144 Incoming::Response(r) => assert_eq!(r.id, 9),
145 other => panic!("expected Response, got {other:?}"),
146 }
147 }
148
149 #[test]
150 fn decode_line_accepts_trailing_newline() {
151 let line = format!(
152 "{}\n",
153 encode_line(&Request::new(1, "handshake", Value::Null))
154 );
155 assert!(decode_line(&line).is_ok());
156 }
157
158 #[test]
159 fn decode_line_rejects_shape_with_neither_method_nor_id() {
160 let err = decode_line(r#"{"foo":"bar"}"#).unwrap_err();
161 assert!(matches!(err, DecodeError::UnrecognizedShape));
162 }
163
164 #[test]
165 fn decode_line_rejects_non_object() {
166 let err = decode_line("[1,2,3]").unwrap_err();
167 assert!(matches!(err, DecodeError::NotAnObject));
168 }
169
170 #[test]
171 fn decode_line_rejects_invalid_json() {
172 let err = decode_line("not json").unwrap_err();
173 assert!(matches!(err, DecodeError::InvalidJson(_)));
174 }
175
176 #[test]
177 fn decode_line_tolerates_unknown_method_name() {
178 let req = Request::new(1, "some_future_method", Value::Null);
182 let line = encode_line(&req);
183 match decode_line(&line).unwrap() {
184 Incoming::Request(r) => assert_eq!(r.method, "some_future_method"),
185 other => panic!("expected Request, got {other:?}"),
186 }
187 }
188
189 #[test]
190 fn decode_line_error_impls_std_error() {
191 let err = decode_line("not json").unwrap_err();
192 let _: &dyn std::error::Error = &err;
193 assert!(std::error::Error::source(&err).is_some());
194 }
195}