Skip to main content

pocopine_core/
server.rs

1//! Server-function error types.
2//!
3//! `#[server]` functions return [`Result<T>`] where the error type is
4//! always [`ServerError`]. That keeps the wire protocol uniform and
5//! lets the client cleanly distinguish "the server returned an error"
6//! from "the network failed or the response didn't deserialize."
7
8use std::fmt;
9
10use serde::{Deserialize, Serialize};
11
12/// All failures a client stub can return.
13///
14/// * [`ServerError::App`] is serialized by the server as part of a
15///   `Result::Err` and decoded verbatim on the client.
16/// * [`ServerError::Network`] is synthesized locally when the fetch
17///   never reached the server, or the body didn't parse as JSON.
18#[derive(Clone, Debug, Serialize, Deserialize)]
19pub enum ServerError {
20    /// An application-level error produced on the server side.
21    App(String),
22    /// Authentication is required but missing or invalid.
23    Unauthorized(String),
24    /// Authentication succeeded, but this caller cannot perform the action.
25    Forbidden(String),
26    /// The request payload was malformed or exceeded the framework body limit.
27    BadRequest(String),
28    /// Transport / deserialization failure on the client side.
29    Network(String),
30}
31
32impl fmt::Display for ServerError {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            ServerError::App(msg) => write!(f, "server error: {msg}"),
36            ServerError::Unauthorized(msg) => write!(f, "unauthorized: {msg}"),
37            ServerError::Forbidden(msg) => write!(f, "forbidden: {msg}"),
38            ServerError::BadRequest(msg) => write!(f, "bad request: {msg}"),
39            ServerError::Network(msg) => write!(f, "network error: {msg}"),
40        }
41    }
42}
43
44impl std::error::Error for ServerError {}
45
46impl ServerError {
47    /// Build an authentication failure.
48    pub fn unauthorized(msg: impl Into<String>) -> Self {
49        ServerError::Unauthorized(msg.into())
50    }
51
52    /// Build an authorization failure.
53    pub fn forbidden(msg: impl Into<String>) -> Self {
54        ServerError::Forbidden(msg.into())
55    }
56
57    /// Build a malformed request failure.
58    pub fn bad_request(msg: impl Into<String>) -> Self {
59        ServerError::BadRequest(msg.into())
60    }
61}
62
63impl From<String> for ServerError {
64    fn from(s: String) -> Self {
65        ServerError::App(s)
66    }
67}
68
69impl From<&str> for ServerError {
70    fn from(s: &str) -> Self {
71        ServerError::App(s.to_owned())
72    }
73}
74
75/// Canonical `Result` alias for `#[server]` functions.
76pub type Result<T> = core::result::Result<T, ServerError>;
77
78const SERVER_FUNCTION_HASH_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
79const SERVER_FUNCTION_HASH_PRIME: u64 = 0x0000_0100_0000_01b3;
80
81fn fnv1a64(bytes: &[u8]) -> u64 {
82    let mut hash = SERVER_FUNCTION_HASH_OFFSET;
83    for byte in bytes {
84        hash ^= u64::from(*byte);
85        hash = hash.wrapping_mul(SERVER_FUNCTION_HASH_PRIME);
86    }
87    hash
88}
89
90/// Returns the compiler-generated default route path for a server function.
91///
92/// The suffix is based on the Rust module path and function name so two
93/// modules can expose the same function name without colliding.
94#[doc(hidden)]
95pub fn server_function_default_path(module_path: &str, function: &str) -> String {
96    let mut key = String::with_capacity(module_path.len() + function.len() + 2);
97    key.push_str(module_path);
98    key.push_str("::");
99    key.push_str(function);
100    let hash = fnv1a64(key.as_bytes());
101
102    format!("/_pocopine/{function}_{hash:016x}")
103}
104
105#[cfg(test)]
106mod tests {
107    use super::server_function_default_path;
108
109    #[test]
110    fn default_paths_are_scoped_by_module_path() {
111        let first = server_function_default_path("app::filters", "get_filter");
112        let second = server_function_default_path("app::admin", "get_filter");
113
114        assert_ne!(first, second);
115        assert!(first.starts_with("/_pocopine/get_filter_"));
116        assert!(second.starts_with("/_pocopine/get_filter_"));
117    }
118
119    #[test]
120    fn default_paths_are_stable() {
121        assert_eq!(
122            server_function_default_path("app::filters", "get_filter"),
123            "/_pocopine/get_filter_b0e2ae5de9927498"
124        );
125    }
126}