Skip to main content

rivetkit_client/protocol/
codec.rs

1use anyhow::{anyhow, Context, Result};
2use rivetkit_client_protocol as wire;
3use serde::Serialize;
4use serde_json::{json, Value as JsonValue};
5use vbare::OwnedVersionedData;
6
7use crate::EncodingKind;
8
9use super::{to_client, to_server};
10
11pub fn encode_to_server(encoding: EncodingKind, value: &to_server::ToServer) -> Result<Vec<u8>> {
12	match encoding {
13		EncodingKind::Json => Ok(serde_json::to_vec(&to_server_json_value(value)?)?),
14		EncodingKind::Cbor => Ok(serde_cbor::to_vec(&to_server_json_value(value)?)?),
15		EncodingKind::Bare => encode_to_server_bare(value),
16	}
17}
18
19pub fn decode_to_client(encoding: EncodingKind, payload: &[u8]) -> Result<to_client::ToClient> {
20	match encoding {
21		EncodingKind::Json => {
22			let value: JsonValue =
23				serde_json::from_slice(payload).context("decode actor websocket json response")?;
24			to_client_from_json_value(&value)
25		}
26		EncodingKind::Cbor => {
27			let value: JsonValue =
28				serde_cbor::from_slice(payload).context("decode actor websocket cbor response")?;
29			to_client_from_json_value(&value)
30		}
31		EncodingKind::Bare => decode_to_client_bare(payload),
32	}
33}
34
35pub fn encode_http_action_request(encoding: EncodingKind, args: &[JsonValue]) -> Result<Vec<u8>> {
36	match encoding {
37		EncodingKind::Json => Ok(serde_json::to_vec(&json!({ "args": args }))?),
38		EncodingKind::Cbor => Ok(serde_cbor::to_vec(&json!({ "args": args }))?),
39		EncodingKind::Bare => {
40			wire::versioned::HttpActionRequest::wrap_latest(wire::HttpActionRequest {
41				args: serde_cbor::to_vec(&args.to_vec())?,
42			})
43			.serialize_with_embedded_version(wire::PROTOCOL_VERSION)
44		}
45	}
46}
47
48pub fn decode_http_action_response(encoding: EncodingKind, payload: &[u8]) -> Result<JsonValue> {
49	match encoding {
50		EncodingKind::Json => {
51			let value: JsonValue = serde_json::from_slice(payload)?;
52			value
53				.get("output")
54				.cloned()
55				.ok_or_else(|| anyhow!("action response missing output"))
56		}
57		EncodingKind::Cbor => {
58			let value: JsonValue = serde_cbor::from_slice(payload)?;
59			value
60				.get("output")
61				.cloned()
62				.ok_or_else(|| anyhow!("action response missing output"))
63		}
64		EncodingKind::Bare => {
65			let response =
66                <wire::versioned::HttpActionResponse as OwnedVersionedData>::deserialize_with_embedded_version(
67                    payload,
68                )
69                .context("decode bare action response")?;
70			Ok(serde_cbor::from_slice(&response.output)?)
71		}
72	}
73}
74
75pub fn encode_http_queue_request<T: Serialize>(
76	encoding: EncodingKind,
77	name: &str,
78	body: &T,
79	wait: bool,
80	timeout: Option<u64>,
81) -> Result<Vec<u8>> {
82	#[derive(Serialize)]
83	struct JsonQueueRequest<'a, T: Serialize + ?Sized> {
84		name: &'a str,
85		body: &'a T,
86		wait: bool,
87		#[serde(skip_serializing_if = "Option::is_none")]
88		timeout: Option<u64>,
89	}
90
91	let request = JsonQueueRequest {
92		name,
93		body,
94		wait,
95		timeout,
96	};
97
98	match encoding {
99		EncodingKind::Json => Ok(serde_json::to_vec(&request)?),
100		EncodingKind::Cbor => Ok(serde_cbor::to_vec(&request)?),
101		EncodingKind::Bare => {
102			wire::versioned::HttpQueueSendRequest::wrap_latest(wire::HttpQueueSendRequest {
103				body: serde_cbor::to_vec(body)?,
104				name: Some(name.to_owned()),
105				wait: Some(wait),
106				timeout,
107			})
108			.serialize_with_embedded_version(wire::PROTOCOL_VERSION)
109		}
110	}
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum QueueSendStatus {
115	Completed,
116	TimedOut,
117	Other(String),
118}
119
120#[derive(Debug, Clone)]
121pub struct QueueSendResult {
122	pub status: QueueSendStatus,
123	pub response: Option<JsonValue>,
124}
125
126pub fn decode_http_queue_response(
127	encoding: EncodingKind,
128	payload: &[u8],
129) -> Result<QueueSendResult> {
130	let (status, response) = match encoding {
131		EncodingKind::Json => {
132			let value: JsonValue = serde_json::from_slice(payload)?;
133			let status = value
134				.get("status")
135				.and_then(JsonValue::as_str)
136				.ok_or_else(|| anyhow!("queue response missing status"))?
137				.to_owned();
138			let response = value.get("response").cloned();
139			(status, response)
140		}
141		EncodingKind::Cbor => {
142			let value: JsonValue = serde_cbor::from_slice(payload)?;
143			let status = value
144				.get("status")
145				.and_then(JsonValue::as_str)
146				.ok_or_else(|| anyhow!("queue response missing status"))?
147				.to_owned();
148			let response = value.get("response").cloned();
149			(status, response)
150		}
151		EncodingKind::Bare => {
152			let response =
153                <wire::versioned::HttpQueueSendResponse as OwnedVersionedData>::deserialize_with_embedded_version(
154                    payload,
155                )
156                .context("decode bare queue response")?;
157			let body = response
158				.response
159				.map(|payload| serde_cbor::from_slice(&payload))
160				.transpose()?;
161			(response.status, body)
162		}
163	};
164
165	let status = match status.as_str() {
166		"completed" => QueueSendStatus::Completed,
167		"timedOut" => QueueSendStatus::TimedOut,
168		_ => QueueSendStatus::Other(status),
169	};
170
171	Ok(QueueSendResult { status, response })
172}
173
174pub fn decode_http_error(
175	encoding: EncodingKind,
176	payload: &[u8],
177) -> Result<(String, String, String, Option<JsonValue>)> {
178	match encoding {
179		EncodingKind::Json => {
180			let value: JsonValue = serde_json::from_slice(payload)?;
181			error_from_json_value(&value)
182		}
183		EncodingKind::Cbor => {
184			let value: JsonValue = serde_cbor::from_slice(payload)?;
185			error_from_json_value(&value)
186		}
187		EncodingKind::Bare => {
188			// Routing-level errors from the gateway (e.g. actor not found) are
189			// emitted as JSON regardless of the client encoding, so fall back to
190			// parsing JSON when the payload is not valid BARE.
191			match <wire::versioned::HttpResponseError as OwnedVersionedData>::deserialize_with_embedded_version(
192				payload,
193			) {
194				Ok(error) => {
195					let metadata = error
196						.metadata
197						.map(|payload| serde_cbor::from_slice(&payload))
198						.transpose()?;
199					Ok((error.group, error.code, error.message, metadata))
200				}
201				Err(bare_err) => serde_json::from_slice::<JsonValue>(payload)
202					.ok()
203					.and_then(|value| error_from_json_value(&value).ok())
204					.ok_or(bare_err)
205					.context("decode bare http error"),
206			}
207		}
208	}
209}
210
211fn to_server_json_value(value: &to_server::ToServer) -> Result<JsonValue> {
212	let body = match &value.body {
213		to_server::ToServerBody::ActionRequest(request) => json!({
214			"tag": "ActionRequest",
215			"val": {
216				"id": request.id,
217				"name": request.name,
218				"args": serde_cbor::from_slice::<JsonValue>(&request.args)
219					.context("decode websocket action args for json/cbor transport")?,
220			},
221		}),
222		to_server::ToServerBody::SubscriptionRequest(request) => json!({
223			"tag": "SubscriptionRequest",
224			"val": {
225				"eventName": request.event_name,
226				"subscribe": request.subscribe,
227			},
228		}),
229	};
230	Ok(json!({ "body": body }))
231}
232
233fn to_client_from_json_value(value: &JsonValue) -> Result<to_client::ToClient> {
234	let body = value
235		.get("body")
236		.and_then(JsonValue::as_object)
237		.ok_or_else(|| anyhow!("actor websocket response missing body"))?;
238	let tag = body
239		.get("tag")
240		.and_then(JsonValue::as_str)
241		.ok_or_else(|| anyhow!("actor websocket response missing tag"))?;
242	let value = body
243		.get("val")
244		.and_then(JsonValue::as_object)
245		.ok_or_else(|| anyhow!("actor websocket response missing val"))?;
246
247	let body = match tag {
248		"Init" => to_client::ToClientBody::Init(to_client::Init {
249			actor_id: json_string(value, "actorId")?,
250			connection_id: json_string(value, "connectionId")?,
251			connection_token: value
252				.get("connectionToken")
253				.and_then(JsonValue::as_str)
254				.map(ToOwned::to_owned),
255		}),
256		"Error" => to_client::ToClientBody::Error(to_client::Error {
257			group: json_string(value, "group")?,
258			code: json_string(value, "code")?,
259			message: json_string(value, "message")?,
260			metadata: value.get("metadata").map(serde_cbor::to_vec).transpose()?,
261			action_id: value.get("actionId").map(parse_json_u64).transpose()?,
262		}),
263		"ActionResponse" => to_client::ToClientBody::ActionResponse(to_client::ActionResponse {
264			id: parse_json_u64(
265				value
266					.get("id")
267					.ok_or_else(|| anyhow!("action response missing id"))?,
268			)?,
269			output: serde_cbor::to_vec(
270				value
271					.get("output")
272					.ok_or_else(|| anyhow!("action response missing output"))?,
273			)?,
274		}),
275		"Event" => to_client::ToClientBody::Event(to_client::Event {
276			name: json_string(value, "name")?,
277			args: serde_cbor::to_vec(
278				value
279					.get("args")
280					.ok_or_else(|| anyhow!("event response missing args"))?,
281			)?,
282		}),
283		other => return Err(anyhow!("unknown actor websocket response tag `{other}`")),
284	};
285
286	Ok(to_client::ToClient { body })
287}
288
289fn encode_to_server_bare(value: &to_server::ToServer) -> Result<Vec<u8>> {
290	let body = match &value.body {
291		to_server::ToServerBody::ActionRequest(request) => {
292			wire::ToServerBody::ActionRequest(wire::ActionRequest {
293				id: serde_bare::Uint(request.id),
294				name: request.name.clone(),
295				args: request.args.clone(),
296			})
297		}
298		to_server::ToServerBody::SubscriptionRequest(request) => {
299			wire::ToServerBody::SubscriptionRequest(wire::SubscriptionRequest {
300				event_name: request.event_name.clone(),
301				subscribe: request.subscribe,
302			})
303		}
304	};
305
306	wire::versioned::ToServer::wrap_latest(wire::ToServer { body })
307		.serialize_with_embedded_version(wire::PROTOCOL_VERSION)
308}
309
310fn decode_to_client_bare(payload: &[u8]) -> Result<to_client::ToClient> {
311	let message =
312		<wire::versioned::ToClient as OwnedVersionedData>::deserialize_with_embedded_version(
313			payload,
314		)
315		.context("decode bare actor websocket response")?;
316
317	let body = match message.body {
318		wire::ToClientBody::Init(init) => to_client::ToClientBody::Init(to_client::Init {
319			actor_id: init.actor_id,
320			connection_id: init.connection_id,
321			connection_token: None,
322		}),
323		wire::ToClientBody::Error(error) => to_client::ToClientBody::Error(to_client::Error {
324			group: error.group,
325			code: error.code,
326			message: error.message,
327			metadata: error.metadata,
328			action_id: error.action_id.map(|id| id.0),
329		}),
330		wire::ToClientBody::ActionResponse(response) => {
331			to_client::ToClientBody::ActionResponse(to_client::ActionResponse {
332				id: response.id.0,
333				output: response.output,
334			})
335		}
336		wire::ToClientBody::Event(event) => to_client::ToClientBody::Event(to_client::Event {
337			name: event.name,
338			args: event.args,
339		}),
340	};
341
342	Ok(to_client::ToClient { body })
343}
344
345fn json_string(value: &serde_json::Map<String, JsonValue>, key: &str) -> Result<String> {
346	value
347		.get(key)
348		.and_then(JsonValue::as_str)
349		.map(ToOwned::to_owned)
350		.ok_or_else(|| anyhow!("json object missing string field `{key}`"))
351}
352
353fn parse_json_u64(value: &JsonValue) -> Result<u64> {
354	match value {
355		JsonValue::Number(number) => number
356			.as_u64()
357			.ok_or_else(|| anyhow!("json number is not an unsigned integer")),
358		JsonValue::Array(values) if values.len() == 2 => {
359			let tag = values[0]
360				.as_str()
361				.ok_or_else(|| anyhow!("json bigint tag is not a string"))?;
362			let raw = values[1]
363				.as_str()
364				.ok_or_else(|| anyhow!("json bigint value is not a string"))?;
365			if tag != "$BigInt" {
366				return Err(anyhow!("unsupported json bigint tag `{tag}`"));
367			}
368			raw.parse::<u64>().context("parse json bigint")
369		}
370		_ => Err(anyhow!("invalid json unsigned integer")),
371	}
372}
373
374fn error_from_json_value(value: &JsonValue) -> Result<(String, String, String, Option<JsonValue>)> {
375	let value = value
376		.as_object()
377		.ok_or_else(|| anyhow!("http error response is not an object"))?;
378	Ok((
379		json_string(value, "group")?,
380		json_string(value, "code")?,
381		json_string(value, "message")?,
382		value.get("metadata").cloned(),
383	))
384}
385
386#[cfg(test)]
387mod tests {
388	use serde_json::json;
389
390	use super::*;
391
392	#[test]
393	fn bare_action_response_round_trips() {
394		let payload = wire::versioned::HttpActionResponse::wrap_latest(wire::HttpActionResponse {
395			output: serde_cbor::to_vec(&json!({ "ok": true })).unwrap(),
396		})
397		.serialize_with_embedded_version(wire::PROTOCOL_VERSION)
398		.unwrap();
399
400		let output = decode_http_action_response(EncodingKind::Bare, &payload).unwrap();
401		assert_eq!(output, json!({ "ok": true }));
402	}
403
404	#[test]
405	fn bare_queue_request_has_embedded_version() {
406		let payload = encode_http_queue_request(
407			EncodingKind::Bare,
408			"jobs",
409			&json!({ "id": 1 }),
410			true,
411			Some(50),
412		)
413		.unwrap();
414		assert_eq!(
415			u16::from_le_bytes([payload[0], payload[1]]),
416			wire::PROTOCOL_VERSION
417		);
418	}
419}