Skip to main content

ndjson_rpc/
protocol.rs

1//! Wire format: line-delimited JSON over a duplex byte stream. Each
2//! request is one JSON object on one line; each response is one JSON
3//! object on one line; lines end with `\n`. No length prefix — the
4//! framing is the newline. NDJSON keeps tools such as `nc -U` piped
5//! through `jq` usable for ad-hoc poking. The same frame shapes ride
6//! the HTTP-over-TCP transport (NDJSON over chunked encoding).
7
8use serde::{Deserialize, Serialize};
9
10/// Client → server frame.
11///
12/// `id` is client-assigned and echoed by the server's response so a
13/// future multiplexed transport can interleave concurrent requests on
14/// one socket. The current Unix implementation serialises
15/// request/response per-connection; the wire shape doesn't depend on
16/// that.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Request {
19	pub id: u64,
20	pub verb: String,
21	#[serde(default)]
22	pub args: serde_json::Value,
23}
24
25/// Server → client frame.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Response {
28	pub id: u64,
29	#[serde(flatten)]
30	pub outcome: ResponseOutcome,
31}
32
33/// Successful result or structured error. Flattened into `Response`
34/// so the wire shape is `{"id":N,"result":{...}}` or
35/// `{"id":N,"error":{...}}` rather than a nested `outcome` key.
36///
37/// Streaming verbs use the additional `Event` and `End` variants:
38///
39/// - `Event { event }` — one frame in a streaming response. The server
40///   may emit zero or more of these per request.
41/// - `End {}` — terminates a streaming response normally. Encoded as
42///   `{"id":N,"end":{}}` so the client can match on the field name.
43///
44/// `#[serde(untagged)]` collapses each variant to its single field —
45/// the keys (`result`, `error`, `event`, `end`) are mutually
46/// exclusive, so the discriminator is the field name itself rather
47/// than a separate `"kind"` tag.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(untagged)]
50pub enum ResponseOutcome {
51	Result { result: serde_json::Value },
52	Error { error: WireError },
53	Event { event: serde_json::Value },
54	End { end: EndMarker },
55}
56
57/// Empty marker payload for the `End` outcome. Encoded as `{}` so
58/// future fields (e.g. a final summary) can be added without breaking
59/// the wire shape.
60#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
61pub struct EndMarker {}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct WireError {
65	pub kind: WireErrorKind,
66	pub message: String,
67}
68
69/// Error category. The full string message carries detail; the kind is
70/// the machine-readable discriminator.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum WireErrorKind {
74	UnknownVerb,
75	BadArgs,
76	Internal,
77	/// Future-proof for streaming verbs and other deferred capabilities.
78	NotImplemented,
79}
80
81/// Encode a value as JSON and append `\n`. Centralises framing so
82/// server.rs / client.rs share one implementation.
83///
84/// # Errors
85/// Returns the underlying [`serde_json::Error`] if `value` fails to
86/// serialize.
87pub fn encode_line<T: Serialize>(value: &T) -> Result<Vec<u8>, serde_json::Error> {
88	let mut buf = serde_json::to_vec(value)?;
89	buf.push(b'\n');
90	Ok(buf)
91}
92
93#[cfg(test)]
94mod tests {
95	use super::*;
96
97	#[test]
98	fn request_round_trips_through_json_with_args() {
99		let req =
100			Request { id: 42, verb: "stats".to_string(), args: serde_json::json!({ "scope": "all" }) };
101		let encoded = serde_json::to_string(&req).expect("serialize");
102		let decoded: Request = serde_json::from_str(&encoded).expect("deserialize");
103		assert_eq!(decoded.id, 42);
104		assert_eq!(decoded.verb, "stats");
105		assert_eq!(decoded.args, serde_json::json!({ "scope": "all" }));
106	}
107
108	#[test]
109	fn request_default_args_are_null() {
110		// Args are optional on the wire; missing key decodes as Value::Null.
111		let raw = r#"{"id":1,"verb":"ping"}"#;
112		let req: Request = serde_json::from_str(raw).expect("deserialize");
113		assert!(req.args.is_null());
114	}
115
116	#[test]
117	fn response_result_serializes_with_flat_result_key() {
118		let resp = Response {
119			id: 7,
120			outcome: ResponseOutcome::Result { result: serde_json::json!({ "pong": true }) },
121		};
122		let value = serde_json::to_value(&resp).expect("to_value");
123		assert_eq!(value["id"], 7);
124		assert_eq!(value["result"], serde_json::json!({ "pong": true }));
125		assert!(value.get("error").is_none(), "result frame must not carry error key");
126		assert!(value.get("outcome").is_none(), "must flatten — no nested outcome key");
127	}
128
129	#[test]
130	fn response_error_serializes_with_flat_error_key() {
131		let resp = Response {
132			id: 3,
133			outcome: ResponseOutcome::Error {
134				error: WireError { kind: WireErrorKind::UnknownVerb, message: "no such verb".to_string() },
135			},
136		};
137		let value = serde_json::to_value(&resp).expect("to_value");
138		assert_eq!(value["id"], 3);
139		assert_eq!(value["error"]["kind"], "unknown_verb");
140		assert_eq!(value["error"]["message"], "no such verb");
141		assert!(value.get("result").is_none());
142	}
143
144	#[test]
145	fn unknown_verb_kind_round_trips_via_snake_case() {
146		for kind in [
147			WireErrorKind::UnknownVerb,
148			WireErrorKind::BadArgs,
149			WireErrorKind::Internal,
150			WireErrorKind::NotImplemented,
151		] {
152			let s = serde_json::to_string(&kind).expect("serialize kind");
153			let back: WireErrorKind = serde_json::from_str(&s).expect("deserialize kind");
154			assert_eq!(kind, back);
155		}
156		assert_eq!(serde_json::to_string(&WireErrorKind::UnknownVerb).unwrap(), "\"unknown_verb\"");
157		assert_eq!(serde_json::to_string(&WireErrorKind::BadArgs).unwrap(), "\"bad_args\"");
158		assert_eq!(
159			serde_json::to_string(&WireErrorKind::NotImplemented).unwrap(),
160			"\"not_implemented\""
161		);
162	}
163
164	#[test]
165	fn response_event_outcome_serializes_with_event_key() {
166		let resp = Response {
167			id: 9,
168			outcome: ResponseOutcome::Event { event: serde_json::json!({ "kind": "trajectory" }) },
169		};
170		let value = serde_json::to_value(&resp).expect("to_value");
171		assert_eq!(value["id"], 9);
172		assert_eq!(value["event"]["kind"], "trajectory");
173		assert!(value.get("result").is_none());
174		assert!(value.get("error").is_none());
175		assert!(value.get("end").is_none());
176	}
177
178	#[test]
179	fn response_end_outcome_serializes_as_empty_end_object() {
180		let resp = Response { id: 4, outcome: ResponseOutcome::End { end: EndMarker {} } };
181		let value = serde_json::to_value(&resp).expect("to_value");
182		assert_eq!(value["id"], 4);
183		assert_eq!(value["end"], serde_json::json!({}));
184		assert!(value.get("event").is_none());
185	}
186
187	#[test]
188	fn response_event_round_trips_through_json() {
189		// Round-trip a few mixed outcomes to confirm the untagged enum
190		// disambiguates by the field name (`result` / `error` / `event`
191		// / `end`) and not by ordering.
192		let frames = vec![
193			Response { id: 1, outcome: ResponseOutcome::Result { result: serde_json::json!(42) } },
194			Response { id: 2, outcome: ResponseOutcome::Event { event: serde_json::json!("hi") } },
195			Response { id: 3, outcome: ResponseOutcome::End { end: EndMarker {} } },
196		];
197		for f in frames {
198			let s = serde_json::to_string(&f).expect("serialize");
199			let back: Response = serde_json::from_str(&s).expect("deserialize");
200			assert_eq!(back.id, f.id);
201			match (&f.outcome, &back.outcome) {
202				(ResponseOutcome::Result { .. }, ResponseOutcome::Result { .. })
203				| (ResponseOutcome::Event { .. }, ResponseOutcome::Event { .. })
204				| (ResponseOutcome::End { .. }, ResponseOutcome::End { .. }) => {}
205				other => panic!("variant changed: {other:?}"),
206			}
207		}
208	}
209
210	#[test]
211	fn encode_line_appends_newline() {
212		let req = Request { id: 1, verb: "ping".to_string(), args: serde_json::Value::Null };
213		let bytes = encode_line(&req).expect("encode");
214		assert_eq!(*bytes.last().expect("non-empty"), b'\n');
215		// Body before the newline must be valid JSON of the same shape.
216		let body = &bytes[..bytes.len() - 1];
217		let decoded: Request = serde_json::from_slice(body).expect("decode body");
218		assert_eq!(decoded.verb, "ping");
219	}
220}