Skip to main content

wscall_server/
server_types.rs

1use std::net::SocketAddr;
2use std::sync::Arc;
3
4use serde::de::DeserializeOwned;
5use serde_json::{Value, json};
6use thiserror::Error;
7use tokio::sync::{RwLock, mpsc};
8use validator::Validate;
9use wscall_protocol::{
10    EncryptionKind, ErrorPayload, FileAttachment, PacketEnvelope, ProtocolError,
11};
12
13use crate::validation;
14
15pub(crate) enum ServerOutbound {
16    Packet(PacketEnvelope),
17    Ping(Vec<u8>),
18    Pong(Vec<u8>),
19    Close,
20}
21
22pub(crate) struct ServerState {
23    pub clients: RwLock<std::collections::HashMap<String, mpsc::Sender<ServerOutbound>>>,
24}
25
26/// Handle that can be cloned into request and event handlers for server push operations.
27#[derive(Clone)]
28pub struct ServerHandle {
29    pub(crate) state: Arc<ServerState>,
30    pub(crate) default_encryption: EncryptionKind,
31}
32
33/// Request context passed to API route handlers.
34#[derive(Clone)]
35pub struct ApiContext {
36    pub(crate) connection_id: String,
37    pub(crate) peer_addr: Option<SocketAddr>,
38    pub(crate) request_id: String,
39    pub(crate) route: String,
40    pub(crate) params: Value,
41    pub(crate) attachments: Vec<FileAttachment>,
42    pub(crate) metadata: Value,
43    pub(crate) server: ServerHandle,
44}
45
46/// Trait for custom parameter validation after JSON binding.
47pub trait ValidateParams {
48    fn validate(&self) -> Result<(), ApiError>;
49}
50
51impl ApiContext {
52    /// Returns the logical connection id for the current client.
53    pub fn connection_id(&self) -> &str {
54        &self.connection_id
55    }
56
57    /// Returns the peer socket address when available.
58    pub fn peer_addr(&self) -> Option<SocketAddr> {
59        self.peer_addr
60    }
61
62    /// Returns the peer IP as a string when available.
63    pub fn peer_ip(&self) -> Option<String> {
64        self.peer_addr.map(|addr| addr.ip().to_string())
65    }
66
67    /// Returns the request correlation id.
68    pub fn request_id(&self) -> &str {
69        &self.request_id
70    }
71
72    /// Returns the matched route name.
73    pub fn route(&self) -> &str {
74        &self.route
75    }
76
77    /// Returns the raw JSON params payload.
78    pub fn params(&self) -> &Value {
79        &self.params
80    }
81
82    /// Looks up a single parameter by key.
83    pub fn param(&self, key: &str) -> Option<&Value> {
84        self.params.as_object()?.get(key)
85    }
86
87    /// Looks up a required parameter or returns a bad request error.
88    pub fn require_param(&self, key: &str) -> Result<&Value, ApiError> {
89        self.param(key)
90            .ok_or_else(|| ApiError::bad_request(format!("missing required param: {key}")))
91    }
92
93    /// Binds the raw params payload into a strongly typed value.
94    pub fn bind<T>(&self) -> Result<T, ApiError>
95    where
96        T: DeserializeOwned,
97    {
98        serde_json::from_value(self.params.clone())
99            .map_err(|source| ApiError::bad_request(format!("invalid params: {source}")))
100    }
101
102    /// Binds params and runs `ValidateParams` on the result.
103    pub fn bind_and_validate<T>(&self) -> Result<T, ApiError>
104    where
105        T: DeserializeOwned + ValidateParams,
106    {
107        let params: T = self.bind()?;
108        params.validate()?;
109        Ok(params)
110    }
111
112    /// Binds params and runs `validator::Validate` on the result.
113    pub fn bind_validated<T>(&self) -> Result<T, ApiError>
114    where
115        T: DeserializeOwned + Validate,
116    {
117        let params: T = self.bind()?;
118        params.validate().map_err(|source| {
119            ApiError::bad_request("params validation failed").with_details(json!({
120                "validation_errors": validation::errors_to_details(&source),
121            }))
122        })?;
123        Ok(params)
124    }
125
126    /// Returns all attachments that accompanied the request.
127    pub fn attachments(&self) -> &[FileAttachment] {
128        &self.attachments
129    }
130
131    /// Returns the raw metadata payload.
132    pub fn metadata(&self) -> &Value {
133        &self.metadata
134    }
135
136    /// Returns a server handle for outbound event operations.
137    pub fn server(&self) -> &ServerHandle {
138        &self.server
139    }
140
141    /// Returns a simplified JSON view of incoming attachments.
142    pub fn attachment_summaries(&self) -> Vec<Value> {
143        self.attachments
144            .iter()
145            .map(|attachment| {
146                json!({
147                    "id": attachment.id,
148                    "name": attachment.name,
149                    "content_type": attachment.content_type,
150                    "size": attachment.size,
151                })
152            })
153            .collect()
154    }
155}
156
157/// Event context passed to event handlers.
158#[derive(Clone)]
159pub struct EventContext {
160    pub(crate) connection_id: String,
161    pub(crate) peer_addr: Option<SocketAddr>,
162    pub(crate) event_id: String,
163    pub(crate) name: String,
164    pub(crate) data: Value,
165    pub(crate) attachments: Vec<FileAttachment>,
166    pub(crate) metadata: Value,
167    pub(crate) server: ServerHandle,
168}
169
170impl EventContext {
171    /// Returns the logical connection id for the current client.
172    pub fn connection_id(&self) -> &str {
173        &self.connection_id
174    }
175
176    /// Returns the peer socket address when available.
177    pub fn peer_addr(&self) -> Option<SocketAddr> {
178        self.peer_addr
179    }
180
181    /// Returns the peer IP as a string when available.
182    pub fn peer_ip(&self) -> Option<String> {
183        self.peer_addr.map(|addr| addr.ip().to_string())
184    }
185
186    /// Returns the event correlation id.
187    pub fn event_id(&self) -> &str {
188        &self.event_id
189    }
190
191    /// Returns the event name.
192    pub fn name(&self) -> &str {
193        &self.name
194    }
195
196    /// Returns the raw JSON event data.
197    pub fn data(&self) -> &Value {
198        &self.data
199    }
200
201    /// Returns attachments that accompanied the event.
202    pub fn attachments(&self) -> &[FileAttachment] {
203        &self.attachments
204    }
205
206    /// Returns the raw metadata payload.
207    pub fn metadata(&self) -> &Value {
208        &self.metadata
209    }
210
211    /// Returns a server handle for outbound event operations.
212    pub fn server(&self) -> &ServerHandle {
213        &self.server
214    }
215}
216
217/// Context passed to the global exception handler.
218#[derive(Clone)]
219pub struct ExceptionContext {
220    pub connection_id: String,
221    pub request_id: Option<String>,
222    pub target: String,
223    pub message_kind: &'static str,
224    pub error: ApiError,
225}
226
227/// Application-level error returned from handlers.
228#[derive(Debug, Clone, Error)]
229#[error("{code}: {message}")]
230pub struct ApiError {
231    pub code: String,
232    pub message: String,
233    pub status: u16,
234    pub details: Option<Value>,
235}
236
237impl ApiError {
238    /// Constructs a 400 bad request error.
239    pub fn bad_request(message: impl Into<String>) -> Self {
240        Self::new("bad_request", message, 400)
241    }
242
243    /// Constructs a 404 not found error.
244    pub fn not_found(message: impl Into<String>) -> Self {
245        Self::new("not_found", message, 404)
246    }
247
248    /// Constructs a 500 internal error.
249    pub fn internal(message: impl Into<String>) -> Self {
250        Self::new("internal_error", message, 500)
251    }
252
253    /// Constructs an error with an explicit code and status.
254    pub fn new(code: impl Into<String>, message: impl Into<String>, status: u16) -> Self {
255        Self {
256            code: code.into(),
257            message: message.into(),
258            status,
259            details: None,
260        }
261    }
262
263    /// Attaches structured details to the error.
264    pub fn with_details(mut self, details: Value) -> Self {
265        self.details = Some(details);
266        self
267    }
268
269    /// Converts the error into a transport payload.
270    pub fn into_payload(self) -> ErrorPayload {
271        ErrorPayload {
272            code: self.code,
273            message: self.message,
274            status: self.status,
275            details: self.details,
276        }
277    }
278}
279
280/// Errors produced by the reusable server runtime.
281#[derive(Debug, Error)]
282pub enum ServerError {
283    #[error("io error: {0}")]
284    Io(#[from] std::io::Error),
285    #[error("websocket error: {0}")]
286    WebSocket(#[from] tokio_tungstenite::tungstenite::Error),
287    #[error("protocol error: {0}")]
288    Protocol(#[from] ProtocolError),
289    #[error("api error: {0:?}")]
290    Api(#[from] ApiError),
291    #[error("connection idle timeout: {0}")]
292    IdleTimeout(String),
293    #[error("outbound queue is full for connection {0}")]
294    OutboundQueueFull(String),
295}