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}
25
26impl ClientMessageType {
27 #[must_use]
29 #[allow(clippy::should_implement_trait)] pub fn from_str(s: &str) -> Option<Self> {
31 match s {
32 "connection_init" => Some(Self::ConnectionInit),
33 "ping" => Some(Self::Ping),
34 "pong" => Some(Self::Pong),
35 "subscribe" => Some(Self::Subscribe),
36 "complete" => Some(Self::Complete),
37 _ => None,
38 }
39 }
40
41 #[must_use]
43 pub const fn as_str(&self) -> &'static str {
44 match self {
45 Self::ConnectionInit => "connection_init",
46 Self::Ping => "ping",
47 Self::Pong => "pong",
48 Self::Subscribe => "subscribe",
49 Self::Complete => "complete",
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56#[non_exhaustive]
57pub enum ServerMessageType {
58 ConnectionAck,
60 Ping,
62 Pong,
64 Next,
66 Error,
68 Complete,
70}
71
72impl ServerMessageType {
73 #[must_use]
75 pub const fn as_str(&self) -> &'static str {
76 match self {
77 Self::ConnectionAck => "connection_ack",
78 Self::Ping => "ping",
79 Self::Pong => "pong",
80 Self::Next => "next",
81 Self::Error => "error",
82 Self::Complete => "complete",
83 }
84 }
85}
86
87#[derive(Debug, Clone, Deserialize)]
89pub struct ClientMessage {
90 #[serde(rename = "type")]
92 pub message_type: String,
93
94 #[serde(default)]
96 pub id: Option<String>,
97
98 #[serde(default)]
100 pub payload: Option<serde_json::Value>,
101}
102
103impl ClientMessage {
104 #[must_use]
106 pub fn parsed_type(&self) -> Option<ClientMessageType> {
107 ClientMessageType::from_str(&self.message_type)
108 }
109
110 #[must_use]
112 pub const fn connection_params(&self) -> Option<&serde_json::Value> {
113 self.payload.as_ref()
114 }
115
116 #[must_use]
118 pub fn subscription_payload(&self) -> Option<SubscribePayload> {
119 self.payload.as_ref().and_then(|p| serde_json::from_value(p.clone()).ok())
120 }
121}
122
123#[derive(Debug, Clone, Deserialize, Serialize)]
125pub struct SubscribePayload {
126 pub query: String,
128
129 #[serde(rename = "operationName")]
131 #[serde(default)]
132 pub operation_name: Option<String>,
133
134 #[serde(default)]
136 pub variables: HashMap<String, serde_json::Value>,
137
138 #[serde(default)]
140 pub extensions: HashMap<String, serde_json::Value>,
141}
142
143#[derive(Debug, Clone, Serialize)]
145pub struct ServerMessage {
146 #[serde(rename = "type")]
148 pub message_type: String,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub id: Option<String>,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub payload: Option<serde_json::Value>,
157}
158
159impl ServerMessage {
160 #[must_use]
162 pub fn connection_ack(payload: Option<serde_json::Value>) -> Self {
163 Self {
164 message_type: ServerMessageType::ConnectionAck.as_str().to_string(),
165 id: None,
166 payload,
167 }
168 }
169
170 #[must_use]
172 pub fn ping(payload: Option<serde_json::Value>) -> Self {
173 Self {
174 message_type: ServerMessageType::Ping.as_str().to_string(),
175 id: None,
176 payload,
177 }
178 }
179
180 #[must_use]
182 pub fn pong(payload: Option<serde_json::Value>) -> Self {
183 Self {
184 message_type: ServerMessageType::Pong.as_str().to_string(),
185 id: None,
186 payload,
187 }
188 }
189
190 #[must_use]
192 #[allow(clippy::needless_pass_by_value)] pub fn next(id: impl Into<String>, data: serde_json::Value) -> Self {
194 Self {
195 message_type: ServerMessageType::Next.as_str().to_string(),
196 id: Some(id.into()),
197 payload: Some(serde_json::json!({ "data": data })),
198 }
199 }
200
201 #[must_use]
208 #[allow(clippy::needless_pass_by_value)] pub fn next_with_extensions(
210 id: impl Into<String>,
211 data: serde_json::Value,
212 extensions: serde_json::Value,
213 ) -> Self {
214 Self {
215 message_type: ServerMessageType::Next.as_str().to_string(),
216 id: Some(id.into()),
217 payload: Some(serde_json::json!({ "data": data, "extensions": extensions })),
218 }
219 }
220
221 #[must_use]
223 #[allow(clippy::needless_pass_by_value)] pub fn error(id: impl Into<String>, errors: Vec<GraphQLError>) -> Self {
225 Self {
226 message_type: ServerMessageType::Error.as_str().to_string(),
227 id: Some(id.into()),
228 payload: Some(serde_json::to_value(errors).unwrap_or_default()),
229 }
230 }
231
232 #[must_use]
234 pub fn complete(id: impl Into<String>) -> Self {
235 Self {
236 message_type: ServerMessageType::Complete.as_str().to_string(),
237 id: Some(id.into()),
238 payload: None,
239 }
240 }
241
242 pub fn to_json(&self) -> Result<String, serde_json::Error> {
248 serde_json::to_string(self)
249 }
250}
251
252pub use fraiseql_error::{GraphQLError, GraphQLErrorLocation as ErrorLocation};
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
256#[non_exhaustive]
257pub enum CloseCode {
258 Normal = 1000,
260 ProtocolError = 1002,
262 InternalError = 1011,
264 ConnectionInitTimeout = 4408,
266 TooManyInitRequests = 4429,
268 SubscriberAlreadyExists = 4409,
270 Unauthorized = 4401,
272 SubscriptionNotFound = 4404,
274}
275
276impl CloseCode {
277 #[must_use]
279 pub const fn code(self) -> u16 {
280 self as u16
281 }
282
283 #[must_use]
285 pub const fn reason(self) -> &'static str {
286 match self {
287 Self::Normal => "Normal closure",
288 Self::ProtocolError => "Protocol error",
289 Self::InternalError => "Internal server error",
290 Self::ConnectionInitTimeout => "Connection initialization timeout",
291 Self::TooManyInitRequests => "Too many initialization requests",
292 Self::SubscriberAlreadyExists => "Subscriber already exists",
293 Self::Unauthorized => "Unauthorized",
294 Self::SubscriptionNotFound => "Subscription not found",
295 }
296 }
297}