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/// Structured error frame. `message` is a human-readable summary;
64/// `details`, when present, carries verb-specific structured context
65/// (compile error site, timeout stage, etc.) that the CLI can render
66/// alongside the message.
67///
68/// `details` is omitted from the wire when `None` so existing
69/// consumers that only inspect `kind` / `message` see the same shape
70/// they always did.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct WireError {
73	pub kind: WireErrorKind,
74	pub message: String,
75	#[serde(default, skip_serializing_if = "Option::is_none")]
76	pub details: Option<serde_json::Value>,
77}
78
79impl WireError {
80	#[must_use]
81	pub fn new(kind: WireErrorKind, message: impl Into<String>) -> Self {
82		Self { kind, message: message.into(), details: None }
83	}
84
85	#[must_use]
86	pub fn with_details(mut self, details: serde_json::Value) -> Self {
87		self.details = Some(details);
88		self
89	}
90}
91
92/// Error category. The full string message carries detail; the kind is
93/// the machine-readable discriminator.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(rename_all = "snake_case")]
96pub enum WireErrorKind {
97	UnknownVerb,
98	BadArgs,
99	Internal,
100	/// A bounded operation exceeded its time budget. Used by handlers
101	/// that wrap an inner future in `tokio::time::timeout` (or one of
102	/// `vane_core::timeout_with`'s named stages).
103	Timeout,
104	/// Future-proof for streaming verbs and other deferred capabilities.
105	NotImplemented,
106}
107
108/// Encode a value as JSON and append `\n`. Centralises framing so
109/// server.rs / client.rs share one implementation.
110///
111/// # Errors
112/// Returns the underlying [`serde_json::Error`] if `value` fails to
113/// serialize.
114pub fn encode_line<T: Serialize>(value: &T) -> Result<Vec<u8>, serde_json::Error> {
115	let mut buf = serde_json::to_vec(value)?;
116	buf.push(b'\n');
117	Ok(buf)
118}
119
120#[cfg(test)]
121mod tests {
122	use super::*;
123
124	#[test]
125	fn request_round_trips_through_json_with_args() {
126		let req =
127			Request { id: 42, verb: "stats".to_string(), args: serde_json::json!({ "scope": "all" }) };
128		let encoded = serde_json::to_string(&req).expect("serialize");
129		let decoded: Request = serde_json::from_str(&encoded).expect("deserialize");
130		assert_eq!(decoded.id, 42);
131		assert_eq!(decoded.verb, "stats");
132		assert_eq!(decoded.args, serde_json::json!({ "scope": "all" }));
133	}
134
135	#[test]
136	fn request_default_args_are_null() {
137		// Args are optional on the wire; missing key decodes as Value::Null.
138		let raw = r#"{"id":1,"verb":"ping"}"#;
139		let req: Request = serde_json::from_str(raw).expect("deserialize");
140		assert!(req.args.is_null());
141	}
142
143	#[test]
144	fn response_result_serializes_with_flat_result_key() {
145		let resp = Response {
146			id: 7,
147			outcome: ResponseOutcome::Result { result: serde_json::json!({ "pong": true }) },
148		};
149		let value = serde_json::to_value(&resp).expect("to_value");
150		assert_eq!(value["id"], 7);
151		assert_eq!(value["result"], serde_json::json!({ "pong": true }));
152		assert!(value.get("error").is_none(), "result frame must not carry error key");
153		assert!(value.get("outcome").is_none(), "must flatten — no nested outcome key");
154	}
155
156	#[test]
157	fn response_error_serializes_with_flat_error_key() {
158		let resp = Response {
159			id: 3,
160			outcome: ResponseOutcome::Error {
161				error: WireError::new(WireErrorKind::UnknownVerb, "no such verb"),
162			},
163		};
164		let value = serde_json::to_value(&resp).expect("to_value");
165		assert_eq!(value["id"], 3);
166		assert_eq!(value["error"]["kind"], "unknown_verb");
167		assert_eq!(value["error"]["message"], "no such verb");
168		assert!(value.get("result").is_none());
169		assert!(value["error"].get("details").is_none(), "details omitted when None");
170	}
171
172	#[test]
173	fn wire_error_with_details_round_trips() {
174		let err = WireError::new(WireErrorKind::BadArgs, "compile failed")
175			.with_details(serde_json::json!({"file": "rules/a.json", "line": 12}));
176		let s = serde_json::to_string(&err).expect("serialize");
177		assert!(s.contains("\"details\""), "details present on the wire: {s}");
178		let back: WireError = serde_json::from_str(&s).expect("deserialize");
179		assert_eq!(back.kind, WireErrorKind::BadArgs);
180		assert_eq!(back.details.expect("details")["line"], 12);
181	}
182
183	#[test]
184	fn unknown_verb_kind_round_trips_via_snake_case() {
185		for kind in [
186			WireErrorKind::UnknownVerb,
187			WireErrorKind::BadArgs,
188			WireErrorKind::Internal,
189			WireErrorKind::Timeout,
190			WireErrorKind::NotImplemented,
191		] {
192			let s = serde_json::to_string(&kind).expect("serialize kind");
193			let back: WireErrorKind = serde_json::from_str(&s).expect("deserialize kind");
194			assert_eq!(kind, back);
195		}
196		assert_eq!(serde_json::to_string(&WireErrorKind::UnknownVerb).unwrap(), "\"unknown_verb\"");
197		assert_eq!(serde_json::to_string(&WireErrorKind::BadArgs).unwrap(), "\"bad_args\"");
198		assert_eq!(serde_json::to_string(&WireErrorKind::Timeout).unwrap(), "\"timeout\"");
199		assert_eq!(
200			serde_json::to_string(&WireErrorKind::NotImplemented).unwrap(),
201			"\"not_implemented\""
202		);
203	}
204
205	#[test]
206	fn response_event_outcome_serializes_with_event_key() {
207		let resp = Response {
208			id: 9,
209			outcome: ResponseOutcome::Event { event: serde_json::json!({ "kind": "trajectory" }) },
210		};
211		let value = serde_json::to_value(&resp).expect("to_value");
212		assert_eq!(value["id"], 9);
213		assert_eq!(value["event"]["kind"], "trajectory");
214		assert!(value.get("result").is_none());
215		assert!(value.get("error").is_none());
216		assert!(value.get("end").is_none());
217	}
218
219	#[test]
220	fn response_end_outcome_serializes_as_empty_end_object() {
221		let resp = Response { id: 4, outcome: ResponseOutcome::End { end: EndMarker {} } };
222		let value = serde_json::to_value(&resp).expect("to_value");
223		assert_eq!(value["id"], 4);
224		assert_eq!(value["end"], serde_json::json!({}));
225		assert!(value.get("event").is_none());
226	}
227
228	#[test]
229	fn response_event_round_trips_through_json() {
230		// Round-trip a few mixed outcomes to confirm the untagged enum
231		// disambiguates by the field name (`result` / `error` / `event`
232		// / `end`) and not by ordering.
233		let frames = vec![
234			Response { id: 1, outcome: ResponseOutcome::Result { result: serde_json::json!(42) } },
235			Response { id: 2, outcome: ResponseOutcome::Event { event: serde_json::json!("hi") } },
236			Response { id: 3, outcome: ResponseOutcome::End { end: EndMarker {} } },
237		];
238		for f in frames {
239			let s = serde_json::to_string(&f).expect("serialize");
240			let back: Response = serde_json::from_str(&s).expect("deserialize");
241			assert_eq!(back.id, f.id);
242			match (&f.outcome, &back.outcome) {
243				(ResponseOutcome::Result { .. }, ResponseOutcome::Result { .. })
244				| (ResponseOutcome::Event { .. }, ResponseOutcome::Event { .. })
245				| (ResponseOutcome::End { .. }, ResponseOutcome::End { .. }) => {}
246				other => panic!("variant changed: {other:?}"),
247			}
248		}
249	}
250
251	#[test]
252	fn encode_line_appends_newline() {
253		let req = Request { id: 1, verb: "ping".to_string(), args: serde_json::Value::Null };
254		let bytes = encode_line(&req).expect("encode");
255		assert_eq!(*bytes.last().expect("non-empty"), b'\n');
256		// Body before the newline must be valid JSON of the same shape.
257		let body = &bytes[..bytes.len() - 1];
258		let decoded: Request = serde_json::from_slice(body).expect("decode body");
259		assert_eq!(decoded.verb, "ping");
260	}
261}