fraiseql_core/runtime/subscription/
protocol.rs1use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum ClientMessageType {
14 ConnectionInit,
16 Ping,
18 Pong,
20 Subscribe,
22 Complete,
24 ConnectionTerminate,
28}
29
30impl ClientMessageType {
31 #[must_use]
33 #[allow(clippy::should_implement_trait)] pub fn from_str(s: &str) -> Option<Self> {
35 match s {
36 "connection_init" => Some(Self::ConnectionInit),
37 "ping" => Some(Self::Ping),
38 "pong" => Some(Self::Pong),
39 "subscribe" => Some(Self::Subscribe),
40 "complete" => Some(Self::Complete),
41 "connection_terminate" => Some(Self::ConnectionTerminate),
42 _ => None,
43 }
44 }
45
46 #[must_use]
48 pub const fn as_str(&self) -> &'static str {
49 match self {
50 Self::ConnectionInit => "connection_init",
51 Self::Ping => "ping",
52 Self::Pong => "pong",
53 Self::Subscribe => "subscribe",
54 Self::Complete => "complete",
55 Self::ConnectionTerminate => "connection_terminate",
56 }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62#[non_exhaustive]
63pub enum ServerMessageType {
64 ConnectionAck,
66 Ping,
68 Pong,
70 Next,
72 Error,
74 Complete,
76}
77
78impl ServerMessageType {
79 #[must_use]
81 pub const fn as_str(&self) -> &'static str {
82 match self {
83 Self::ConnectionAck => "connection_ack",
84 Self::Ping => "ping",
85 Self::Pong => "pong",
86 Self::Next => "next",
87 Self::Error => "error",
88 Self::Complete => "complete",
89 }
90 }
91}
92
93#[derive(Debug, Clone, Deserialize)]
95pub struct ClientMessage {
96 #[serde(rename = "type")]
98 pub message_type: String,
99
100 #[serde(default)]
102 pub id: Option<String>,
103
104 #[serde(default)]
106 pub payload: Option<serde_json::Value>,
107}
108
109impl ClientMessage {
110 #[must_use]
112 pub fn parsed_type(&self) -> Option<ClientMessageType> {
113 ClientMessageType::from_str(&self.message_type)
114 }
115
116 #[must_use]
118 pub const fn connection_params(&self) -> Option<&serde_json::Value> {
119 self.payload.as_ref()
120 }
121
122 #[must_use]
124 pub fn subscription_payload(&self) -> Option<SubscribePayload> {
125 self.payload.as_ref().and_then(|p| serde_json::from_value(p.clone()).ok())
126 }
127}
128
129#[derive(Debug, Clone, Deserialize, Serialize)]
131pub struct SubscribePayload {
132 pub query: String,
134
135 #[serde(rename = "operationName")]
137 #[serde(default)]
138 pub operation_name: Option<String>,
139
140 #[serde(default)]
142 pub variables: HashMap<String, serde_json::Value>,
143
144 #[serde(default)]
146 pub extensions: HashMap<String, serde_json::Value>,
147}
148
149#[derive(Debug, Clone, Serialize)]
151pub struct ServerMessage {
152 #[serde(rename = "type")]
154 pub message_type: String,
155
156 #[serde(skip_serializing_if = "Option::is_none")]
158 pub id: Option<String>,
159
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub payload: Option<serde_json::Value>,
163}
164
165impl ServerMessage {
166 #[must_use]
168 pub fn connection_ack(payload: Option<serde_json::Value>) -> Self {
169 Self {
170 message_type: ServerMessageType::ConnectionAck.as_str().to_string(),
171 id: None,
172 payload,
173 }
174 }
175
176 #[must_use]
178 pub fn ping(payload: Option<serde_json::Value>) -> Self {
179 Self {
180 message_type: ServerMessageType::Ping.as_str().to_string(),
181 id: None,
182 payload,
183 }
184 }
185
186 #[must_use]
188 pub fn pong(payload: Option<serde_json::Value>) -> Self {
189 Self {
190 message_type: ServerMessageType::Pong.as_str().to_string(),
191 id: None,
192 payload,
193 }
194 }
195
196 #[must_use]
198 #[allow(clippy::needless_pass_by_value)] pub fn next(id: impl Into<String>, data: serde_json::Value) -> Self {
200 Self {
201 message_type: ServerMessageType::Next.as_str().to_string(),
202 id: Some(id.into()),
203 payload: Some(serde_json::json!({ "data": data })),
204 }
205 }
206
207 #[must_use]
214 #[allow(clippy::needless_pass_by_value)] pub fn next_with_extensions(
216 id: impl Into<String>,
217 data: serde_json::Value,
218 extensions: serde_json::Value,
219 ) -> Self {
220 Self {
221 message_type: ServerMessageType::Next.as_str().to_string(),
222 id: Some(id.into()),
223 payload: Some(serde_json::json!({ "data": data, "extensions": extensions })),
224 }
225 }
226
227 #[must_use]
229 #[allow(clippy::needless_pass_by_value)] pub fn error(id: impl Into<String>, errors: Vec<GraphQLError>) -> Self {
231 Self {
232 message_type: ServerMessageType::Error.as_str().to_string(),
233 id: Some(id.into()),
234 payload: Some(serde_json::to_value(errors).unwrap_or_default()),
235 }
236 }
237
238 #[must_use]
240 pub fn complete(id: impl Into<String>) -> Self {
241 Self {
242 message_type: ServerMessageType::Complete.as_str().to_string(),
243 id: Some(id.into()),
244 payload: None,
245 }
246 }
247
248 pub fn to_json(&self) -> Result<String, serde_json::Error> {
254 serde_json::to_string(self)
255 }
256}
257
258pub use fraiseql_error::{GraphQLError, GraphQLErrorLocation as ErrorLocation};
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
262#[non_exhaustive]
263pub enum CloseCode {
264 Normal = 1000,
266 GoingAway = 1001,
268 ProtocolError = 1002,
270 BadRequest = 4400,
272 InternalError = 1011,
274 ConnectionInitTimeout = 4408,
276 TooManyInitRequests = 4429,
278 SubscriberAlreadyExists = 4409,
280 Unauthorized = 4401,
282 SubscriptionNotFound = 4404,
284}
285
286impl CloseCode {
287 #[must_use]
289 pub const fn code(self) -> u16 {
290 self as u16
291 }
292
293 #[must_use]
295 pub const fn reason(self) -> &'static str {
296 match self {
297 Self::Normal => "Normal closure",
298 Self::GoingAway => "Server shutting down",
299 Self::ProtocolError => "Protocol error",
300 Self::BadRequest => "Bad Request",
301 Self::InternalError => "Internal server error",
302 Self::ConnectionInitTimeout => "Connection initialization timeout",
303 Self::TooManyInitRequests => "Too many initialization requests",
304 Self::SubscriberAlreadyExists => "Subscriber already exists",
305 Self::Unauthorized => "Unauthorized",
306 Self::SubscriptionNotFound => "Subscription not found",
307 }
308 }
309}