Skip to main content

grpc_webnext/
json_frame.rs

1//! Native-JSON WebSocket frame format for the `+json` codec.
2//!
3//! JSON WebSocket messages are **text** frames carrying a flat object; you read
4//! which field is present to know the frame kind. One WebSocket carries exactly one
5//! stream, so the WS URL *is* the route — frames carry neither `method` nor a stream
6//! id (human-readable):
7//!
8//! ```jsonc
9//! { "metadata": {…}, "timeoutMillis": 5000 } // open (optional; metadata/deadline)
10//! { "message": {…} }                         // data message
11//! { "halfClose": true }                      // client done sending
12//! { "status": { "code": 0, "message": "" } } // terminal (trailer / reset)
13//! ```
14//!
15//! The application `message` is a *native* JSON value (not base64 bytes). Proto
16//! mode uses binary frames (`crate::frame`).
17
18use std::collections::BTreeMap;
19
20use serde::{Deserialize, Serialize};
21use serde_json::Value;
22
23/// Metadata as a JSON object (single value per key).
24pub type JsonMeta = BTreeMap<String, String>;
25
26#[derive(Debug, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct JsonStatus {
29    pub code: u32,
30    #[serde(default, skip_serializing_if = "String::is_empty")]
31    pub message: String,
32}
33
34/// One WebSocket text frame. The frame kind is chosen by which of `message` /
35/// `half_close` / `status` (or a bare `metadata` open) is present.
36#[derive(Debug, Default, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct JsonFrame {
39    /// Request metadata (on open) or initial response metadata (header) or trailing
40    /// metadata (with `status`).
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub metadata: Option<JsonMeta>,
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub timeout_millis: Option<u32>,
45    /// A data message (native JSON). On an open frame, the optional first message.
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub message: Option<Value>,
48    /// Set → the client is done sending.
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub half_close: Option<bool>,
51    /// Set → terminal status (trailer, or a reset/cancel).
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub status: Option<JsonStatus>,
54}
55
56/// Parse a WebSocket text frame.
57pub fn decode_json_frame(text: &str) -> Result<JsonFrame, serde_json::Error> {
58    serde_json::from_str(text)
59}
60
61/// Serialize a `JsonFrame` to a WebSocket text payload.
62pub fn encode_json_frame(frame: &JsonFrame) -> String {
63    serde_json::to_string(frame).expect("JsonFrame serializes")
64}
65
66// --- Conversions to/from the internal protobuf `Frame` (server-side only) -----
67
68use crate::pb::{frame::Kind, metadatum, Frame, HalfClose, Message, Metadatum, Reset, Subscribe};
69
70fn meta_vec_to_json(items: &[Metadatum]) -> Option<JsonMeta> {
71    let map: JsonMeta = items
72        .iter()
73        .filter_map(|m| match &m.value {
74            Some(metadatum::Value::AsciiValue(s)) => Some((m.key.clone(), s.clone())),
75            _ => None, // binary metadata is omitted from JSON frames
76        })
77        .collect();
78    (!map.is_empty()).then_some(map)
79}
80
81fn json_to_meta_vec(meta: &Option<JsonMeta>) -> Vec<Metadatum> {
82    meta.iter()
83        .flatten()
84        .map(|(k, v)| Metadatum {
85            key: k.clone(),
86            value: Some(metadatum::Value::AsciiValue(v.clone())),
87        })
88        .collect()
89}
90
91fn to_bytes(v: &Value) -> bytes::Bytes {
92    serde_json::to_vec(v).unwrap_or_default().into()
93}
94
95/// Build a `Subscribe` from the open frame, taking the method from the WS route
96/// (the frame itself carries no `method`).
97pub fn json_open_to_subscribe(f: JsonFrame, method: String) -> Subscribe {
98    Subscribe {
99        method,
100        headers: json_to_meta_vec(&f.metadata),
101        timeout_millis: f.timeout_millis.unwrap_or(0),
102        initial_payload: f.message.as_ref().map(to_bytes).unwrap_or_default(),
103        json: true,
104    }
105}
106
107/// Convert a post-open client `JsonFrame` (WS text) into the internal `Frame`. The
108/// frame kind is chosen by which field is present.
109pub fn json_frame_to_proto(f: JsonFrame) -> Frame {
110    let kind = if let Some(status) = f.status {
111        // A terminal status from the client is a cancel/reset.
112        Kind::Reset(Reset { status_code: status.code, status_message: status.message })
113    } else if f.half_close == Some(true) {
114        Kind::HalfClose(HalfClose {})
115    } else if let Some(message) = f.message {
116        Kind::Message(Message { payload: to_bytes(&message) })
117    } else {
118        // Bare frame is treated as half-close.
119        Kind::HalfClose(HalfClose {})
120    };
121    Frame { kind: Some(kind) }
122}
123
124/// Convert an internal server `Frame` into a `JsonFrame` for a WS text message.
125/// Returns `None` for kinds with no JSON form (client-only kinds).
126pub fn proto_frame_to_json(frame: &Frame) -> Option<JsonFrame> {
127    let from_bytes = |b: &[u8]| serde_json::from_slice::<Value>(b).unwrap_or(Value::Null);
128    Some(match frame.kind.as_ref()? {
129        Kind::Message(m) => JsonFrame {
130            message: Some(from_bytes(&m.payload)),
131            ..Default::default()
132        },
133        Kind::Header(h) => JsonFrame {
134            metadata: Some(meta_vec_to_json(&h.headers).unwrap_or_default()),
135            ..Default::default()
136        },
137        Kind::Trailer(t) => JsonFrame {
138            status: Some(JsonStatus { code: t.status_code, message: t.status_message.clone() }),
139            metadata: meta_vec_to_json(&t.trailers),
140            ..Default::default()
141        },
142        Kind::Reset(r) => JsonFrame {
143            status: Some(JsonStatus { code: r.status_code, message: r.status_message.clone() }),
144            ..Default::default()
145        },
146        _ => return None,
147    })
148}