1use std::fmt;
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor, ser::Error as _};
4use serde_json::{Map, Value};
5
6use crate::ErrorInfo;
7
8pub const MAX_SAFE_INTEGER_ID: u64 = crate::MAX_SAFE_INTEGER;
9
10#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
12pub enum JsonRpcVersion {
13 #[serde(rename = "2.0")]
14 V2,
15}
16
17#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
18#[cfg_attr(feature = "schema", schemars(with = "RpcIdSchema"))]
19#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub enum RpcId {
21 String(String),
22 Number(u64),
23}
24
25#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
29#[serde(transparent)]
30pub struct RequestTimeoutMs(
31 #[cfg_attr(
32 feature = "schema",
33 schemars(range(min = 1_u64, max = 9_007_199_254_740_991_u64))
34 )]
35 u64,
36);
37
38impl RequestTimeoutMs {
39 pub const MIN: u64 = 1;
40 pub const MAX: u64 = crate::MAX_SAFE_INTEGER;
41
42 pub const fn new(value: u64) -> Option<Self> {
43 if value >= Self::MIN && value <= Self::MAX {
44 Some(Self(value))
45 } else {
46 None
47 }
48 }
49
50 pub const fn get(self) -> u64 {
51 self.0
52 }
53}
54
55impl<'de> Deserialize<'de> for RequestTimeoutMs {
56 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
57 where
58 D: Deserializer<'de>,
59 {
60 let value = u64::deserialize(deserializer)?;
61 Self::new(value).ok_or_else(|| {
62 serde::de::Error::custom(format!(
63 "timeout must be between {} and {} milliseconds",
64 Self::MIN,
65 Self::MAX
66 ))
67 })
68 }
69}
70
71pub(crate) fn deserialize_optional_timeout<'de, D>(
72 deserializer: D,
73) -> Result<Option<RequestTimeoutMs>, D::Error>
74where
75 D: Deserializer<'de>,
76{
77 RequestTimeoutMs::deserialize(deserializer).map(Some)
78}
79
80#[cfg(feature = "schema")]
81#[allow(dead_code)]
82#[derive(schemars::JsonSchema)]
83#[serde(untagged)]
84enum RpcIdSchema {
85 String(String),
86 Number(#[schemars(range(max = 9_007_199_254_740_991_u64))] u64),
87}
88
89impl Serialize for RpcId {
90 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
91 where
92 S: Serializer,
93 {
94 match self {
95 Self::String(value) => serializer.serialize_str(value),
96 Self::Number(value) if *value <= MAX_SAFE_INTEGER_ID => {
97 serializer.serialize_u64(*value)
98 }
99 Self::Number(_) => Err(S::Error::custom(
100 "numeric request id exceeds the JavaScript safe integer limit",
101 )),
102 }
103 }
104}
105
106impl<'de> Deserialize<'de> for RpcId {
107 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
108 where
109 D: Deserializer<'de>,
110 {
111 struct RpcIdVisitor;
112
113 impl<'de> Visitor<'de> for RpcIdVisitor {
114 type Value = RpcId;
115
116 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
117 write!(
118 formatter,
119 "a string or a non-negative integer no larger than {MAX_SAFE_INTEGER_ID}"
120 )
121 }
122
123 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
124 where
125 E: serde::de::Error,
126 {
127 Ok(RpcId::String(value.to_owned()))
128 }
129
130 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
131 where
132 E: serde::de::Error,
133 {
134 Ok(RpcId::String(value))
135 }
136
137 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
138 where
139 E: serde::de::Error,
140 {
141 if value <= MAX_SAFE_INTEGER_ID {
142 Ok(RpcId::Number(value))
143 } else {
144 Err(E::custom(
145 "numeric request id exceeds the JavaScript safe integer limit",
146 ))
147 }
148 }
149
150 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
151 where
152 E: serde::de::Error,
153 {
154 let value = u64::try_from(value)
155 .map_err(|_| E::custom("numeric request id must be non-negative"))?;
156 self.visit_u64(value)
157 }
158
159 fn visit_f64<E>(self, _value: f64) -> Result<Self::Value, E>
160 where
161 E: serde::de::Error,
162 {
163 Err(E::custom("numeric request id must be an integer"))
164 }
165 }
166
167 deserializer.deserialize_any(RpcIdVisitor)
168 }
169}
170
171#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
172#[derive(Clone, PartialEq, Deserialize, Serialize)]
173#[serde(untagged)]
174pub enum RpcParams {
175 Object(Map<String, Value>),
176 Array(Vec<Value>),
177}
178
179impl fmt::Debug for RpcParams {
180 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
181 match self {
182 Self::Object(values) => formatter
183 .debug_struct("RpcParams::Object")
184 .field("field_count", &values.len())
185 .finish(),
186 Self::Array(values) => formatter
187 .debug_struct("RpcParams::Array")
188 .field("item_count", &values.len())
189 .finish(),
190 }
191 }
192}
193
194impl RpcParams {
195 pub fn is_empty(&self) -> bool {
196 match self {
197 Self::Object(values) => values.is_empty(),
198 Self::Array(values) => values.is_empty(),
199 }
200 }
201
202 pub fn into_value(self) -> Value {
203 match self {
204 Self::Object(values) => Value::Object(values),
205 Self::Array(values) => Value::Array(values),
206 }
207 }
208}
209
210fn deserialize_optional_params<'de, D>(deserializer: D) -> Result<Option<RpcParams>, D::Error>
211where
212 D: Deserializer<'de>,
213{
214 let value = Value::deserialize(deserializer)?;
215 match value {
216 Value::Object(values) => Ok(Some(RpcParams::Object(values))),
217 Value::Array(values) => Ok(Some(RpcParams::Array(values))),
218 _ => Err(serde::de::Error::custom(
219 "params must be an object or array when present",
220 )),
221 }
222}
223
224#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
225#[derive(Clone, PartialEq, Deserialize, Serialize)]
226#[serde(rename_all = "camelCase", deny_unknown_fields)]
227pub struct RpcRequest {
228 pub jsonrpc: JsonRpcVersion,
229 pub id: RpcId,
230 pub method: String,
231 #[serde(
232 default,
233 deserialize_with = "deserialize_optional_timeout",
234 skip_serializing_if = "Option::is_none"
235 )]
236 #[cfg_attr(feature = "schema", schemars(with = "RequestTimeoutMs"))]
237 pub timeout_ms: Option<RequestTimeoutMs>,
238 #[serde(
239 default,
240 deserialize_with = "deserialize_optional_params",
241 skip_serializing_if = "Option::is_none"
242 )]
243 #[cfg_attr(feature = "schema", schemars(with = "RpcParams"))]
244 pub params: Option<RpcParams>,
245}
246
247impl fmt::Debug for RpcRequest {
248 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
249 formatter
250 .debug_struct("RpcRequest")
251 .field("jsonrpc", &self.jsonrpc)
252 .field("id", &self.id)
253 .field("method", &self.method)
254 .field("timeout_ms", &self.timeout_ms)
255 .field("has_params", &self.params.is_some())
256 .finish()
257 }
258}
259
260#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
261#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
262#[serde(rename_all = "camelCase", deny_unknown_fields)]
263pub struct RequestCancelParams {
264 pub request_id: RpcId,
265}
266
267#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
268#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
269#[serde(rename_all = "camelCase")]
270pub enum RequestCancelStatus {
271 Requested,
272 AlreadyRequested,
273 NotFound,
274}
275
276#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
277#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
278#[serde(rename_all = "camelCase", deny_unknown_fields)]
279pub struct RequestCancelResult {
280 pub request_id: RpcId,
281 pub status: RequestCancelStatus,
282}
283
284#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
285#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
286#[serde(rename_all = "camelCase", deny_unknown_fields)]
287pub struct RpcError {
288 #[cfg_attr(
289 feature = "schema",
290 schemars(range(min = -2_147_483_648_i64, max = 2_147_483_647_i64))
291 )]
292 pub code: i32,
293 pub message: String,
294 pub data: ErrorInfo,
295}
296
297#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
298#[cfg_attr(feature = "schema", schemars(with = "RpcResponseSchema"))]
299#[derive(Clone, PartialEq, Serialize)]
300#[serde(untagged)]
301pub enum RpcResponse {
302 Success {
303 jsonrpc: JsonRpcVersion,
304 id: RpcId,
305 result: Value,
306 },
307 Failure {
308 jsonrpc: JsonRpcVersion,
309 id: Option<RpcId>,
310 error: RpcError,
311 },
312}
313
314impl fmt::Debug for RpcResponse {
315 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
316 match self {
317 Self::Success { jsonrpc, id, .. } => formatter
318 .debug_struct("RpcResponse::Success")
319 .field("jsonrpc", jsonrpc)
320 .field("id", id)
321 .field("result", &"<redacted>")
322 .finish(),
323 Self::Failure { jsonrpc, id, error } => formatter
324 .debug_struct("RpcResponse::Failure")
325 .field("jsonrpc", jsonrpc)
326 .field("id", id)
327 .field("rpc_code", &error.code)
328 .field("error_code", &error.data.code)
329 .finish(),
330 }
331 }
332}
333
334#[cfg(feature = "schema")]
335#[allow(dead_code)]
336#[derive(schemars::JsonSchema)]
337#[serde(untagged)]
338enum RpcResponseSchema {
339 Success(RpcSuccessSchema),
340 Failure(RpcFailureSchema),
341}
342
343#[cfg(feature = "schema")]
344#[allow(dead_code)]
345#[derive(schemars::JsonSchema)]
346#[serde(rename_all = "camelCase", deny_unknown_fields)]
347struct RpcSuccessSchema {
348 jsonrpc: JsonRpcVersion,
349 id: RpcId,
350 result: Value,
351}
352
353#[cfg(feature = "schema")]
354#[allow(dead_code)]
355#[derive(schemars::JsonSchema)]
356#[serde(rename_all = "camelCase", deny_unknown_fields)]
357struct RpcFailureSchema {
358 jsonrpc: JsonRpcVersion,
359 id: NullableRpcIdSchema,
360 error: RpcError,
361}
362
363#[cfg(feature = "schema")]
364#[allow(dead_code)]
365#[derive(schemars::JsonSchema)]
366#[serde(untagged)]
367enum NullableRpcIdSchema {
368 Id(RpcId),
369 Null(()),
370}
371
372impl<'de> Deserialize<'de> for RpcResponse {
373 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
374 where
375 D: Deserializer<'de>,
376 {
377 #[derive(Deserialize)]
378 #[serde(rename_all = "camelCase", deny_unknown_fields)]
379 struct SuccessWire {
380 jsonrpc: JsonRpcVersion,
381 id: RpcId,
382 result: Value,
383 }
384
385 #[derive(Deserialize)]
386 #[serde(untagged)]
387 enum NullableRpcId {
388 Id(RpcId),
389 Null(()),
390 }
391
392 #[derive(Deserialize)]
393 #[serde(rename_all = "camelCase", deny_unknown_fields)]
394 struct FailureWire {
395 jsonrpc: JsonRpcVersion,
396 id: NullableRpcId,
397 error: RpcError,
398 }
399
400 #[derive(Deserialize)]
401 #[serde(untagged)]
402 enum ResponseWire {
403 Success(SuccessWire),
404 Failure(FailureWire),
405 }
406
407 match ResponseWire::deserialize(deserializer)? {
408 ResponseWire::Success(response) => Ok(Self::Success {
409 jsonrpc: response.jsonrpc,
410 id: response.id,
411 result: response.result,
412 }),
413 ResponseWire::Failure(response) => Ok(Self::Failure {
414 jsonrpc: response.jsonrpc,
415 id: match response.id {
416 NullableRpcId::Id(id) => Some(id),
417 NullableRpcId::Null(()) => None,
418 },
419 error: response.error,
420 }),
421 }
422 }
423}
424
425impl RpcResponse {
426 pub fn success(id: RpcId, result: Value) -> Self {
427 Self::Success {
428 jsonrpc: JsonRpcVersion::V2,
429 id,
430 result,
431 }
432 }
433
434 pub fn failure(id: Option<RpcId>, error: RpcError) -> Self {
435 Self::Failure {
436 jsonrpc: JsonRpcVersion::V2,
437 id,
438 error,
439 }
440 }
441
442 pub fn result(&self) -> Option<&Value> {
443 match self {
444 Self::Success { result, .. } => Some(result),
445 Self::Failure { .. } => None,
446 }
447 }
448
449 pub fn error(&self) -> Option<&RpcError> {
450 match self {
451 Self::Success { .. } => None,
452 Self::Failure { error, .. } => Some(error),
453 }
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use serde_json::{Value, json};
460
461 use super::{
462 JsonRpcVersion, MAX_SAFE_INTEGER_ID, RequestCancelParams, RequestCancelResult,
463 RequestCancelStatus, RequestTimeoutMs, RpcId, RpcParams, RpcRequest, RpcResponse,
464 };
465
466 #[test]
467 fn request_requires_json_rpc_two_and_a_safe_id() {
468 let request: RpcRequest = serde_json::from_value(json!({
469 "jsonrpc": "2.0",
470 "id": MAX_SAFE_INTEGER_ID,
471 "method": "system.hello"
472 }))
473 .expect("valid request");
474 assert_eq!(request.jsonrpc, JsonRpcVersion::V2);
475 assert_eq!(request.id, RpcId::Number(MAX_SAFE_INTEGER_ID));
476 assert!(request.params.is_none());
477
478 for invalid in [
479 json!({ "jsonrpc": "1.0", "id": 1, "method": "x" }),
480 json!({ "jsonrpc": "2.0", "id": null, "method": "x" }),
481 json!({ "jsonrpc": "2.0", "id": -1, "method": "x" }),
482 json!({ "jsonrpc": "2.0", "id": 1.5, "method": "x" }),
483 json!({ "jsonrpc": "2.0", "id": MAX_SAFE_INTEGER_ID + 1, "method": "x" }),
484 json!({ "jsonrpc": "2.0", "id": 1, "method": "x", "params": null }),
485 json!({ "jsonrpc": "2.0", "id": 1, "method": "x", "params": 42 }),
486 ] {
487 assert!(serde_json::from_value::<RpcRequest>(invalid).is_err());
488 }
489
490 assert!(serde_json::to_value(RpcId::Number(MAX_SAFE_INTEGER_ID + 1)).is_err());
491 }
492
493 #[test]
494 fn request_timeout_accepts_only_positive_safe_integers() {
495 let request: RpcRequest = serde_json::from_value(json!({
496 "jsonrpc": "2.0",
497 "id": "bounded",
498 "method": "device.observe",
499 "timeoutMs": RequestTimeoutMs::MAX
500 }))
501 .expect("maximum safe timeout");
502 assert_eq!(
503 request.timeout_ms.map(RequestTimeoutMs::get),
504 Some(RequestTimeoutMs::MAX)
505 );
506
507 for timeout in [json!(null), json!(0), json!(RequestTimeoutMs::MAX + 1)] {
508 let invalid = json!({
509 "jsonrpc": "2.0",
510 "id": "invalid-timeout",
511 "method": "device.observe",
512 "timeoutMs": timeout
513 });
514 assert!(serde_json::from_value::<RpcRequest>(invalid).is_err());
515 }
516 assert!(RequestTimeoutMs::new(0).is_none());
517 assert!(RequestTimeoutMs::new(RequestTimeoutMs::MAX + 1).is_none());
518 }
519
520 #[test]
521 fn old_request_json_round_trips_without_new_fields() {
522 let old = json!({
523 "jsonrpc": "2.0",
524 "id": "old-client",
525 "method": "device.execute",
526 "params": {
527 "id": "00000000-0000-0000-0000-000000000000",
528 "name": "tap",
529 "arguments": { "x": 10, "y": 20 }
530 }
531 });
532 let request: RpcRequest = serde_json::from_value(old.clone()).expect("old request");
533 assert!(request.timeout_ms.is_none());
534 assert_eq!(
535 serde_json::to_value(request).expect("serialize old request"),
536 old
537 );
538
539 let unknown = json!({
540 "jsonrpc": "2.0",
541 "id": "strict",
542 "method": "device.observe",
543 "deadlineMs": 100
544 });
545 assert!(serde_json::from_value::<RpcRequest>(unknown).is_err());
546 }
547
548 #[test]
549 fn cancellation_models_are_strict_and_use_camel_case_statuses() {
550 let params: RequestCancelParams = serde_json::from_value(json!({
551 "requestId": "execute-1"
552 }))
553 .expect("cancel params");
554 let result = RequestCancelResult {
555 request_id: params.request_id,
556 status: RequestCancelStatus::AlreadyRequested,
557 };
558 assert_eq!(
559 serde_json::to_value(result).expect("cancel result"),
560 json!({
561 "requestId": "execute-1",
562 "status": "alreadyRequested"
563 })
564 );
565 assert!(
566 serde_json::from_value::<RequestCancelParams>(json!({
567 "requestId": "execute-1",
568 "unknown": true
569 }))
570 .is_err()
571 );
572 assert!(
573 serde_json::from_value::<RequestCancelResult>(json!({
574 "requestId": "execute-1",
575 "status": "requested",
576 "unknown": true
577 }))
578 .is_err()
579 );
580 }
581
582 #[test]
583 fn response_shape_cannot_contain_both_result_and_error() {
584 let response = RpcResponse::success(RpcId::String("request-1".to_owned()), json!({}));
585 let value = serde_json::to_value(response).expect("serialize response");
586 assert!(value.get("result").is_some());
587 assert!(value.get("error").is_none());
588 assert_eq!(value["jsonrpc"], "2.0");
589
590 let restored: RpcResponse = serde_json::from_value(value).expect("deserialize response");
591 assert_eq!(restored.result(), Some(&Value::Object(Default::default())));
592
593 let invalid = json!({
594 "jsonrpc": "2.0",
595 "id": 1,
596 "result": {},
597 "error": {
598 "code": -32603,
599 "message": "internal",
600 "data": {
601 "code": "internal_error",
602 "message": "internal",
603 "retryable": false,
604 "details": null
605 }
606 }
607 });
608 assert!(serde_json::from_value::<RpcResponse>(invalid).is_err());
609 }
610
611 #[test]
612 fn rpc_debug_views_do_not_render_raw_params() {
613 const SENTINEL: &str = "DEVICERAIL_RPC_SECRET_SENTINEL";
614 let params = RpcParams::Object(serde_json::Map::from_iter([(
615 "arguments".to_owned(),
616 json!({ "text": SENTINEL }),
617 )]));
618 let request = RpcRequest {
619 jsonrpc: JsonRpcVersion::V2,
620 id: RpcId::Number(1),
621 method: "device.execute".to_owned(),
622 timeout_ms: None,
623 params: Some(params.clone()),
624 };
625 assert!(!format!("{params:?}").contains(SENTINEL));
626 assert!(!format!("{request:?}").contains(SENTINEL));
627 let response = RpcResponse::success(
628 RpcId::Number(1),
629 json!({ "endpoint": format!("ws://127.0.0.1/v/{SENTINEL}") }),
630 );
631 assert!(!format!("{response:?}").contains(SENTINEL));
632 }
633}