Skip to main content

reinhardt_pages/server_fn/
server_fn_trait.rs

1//! Server Function Trait and Error Types
2//!
3//! This module defines the core trait and error types for server functions.
4
5use serde::{Deserialize, Serialize};
6
7/// Common trait for all server functions
8///
9/// This trait is implemented automatically by the `#[server_fn]` macro.
10/// Users typically don't need to implement this manually.
11pub trait ServerFn {
12	/// The input type (function arguments)
13	type Input: Serialize + for<'de> Deserialize<'de>;
14
15	/// The output type (function return value)
16	type Output: Serialize + for<'de> Deserialize<'de>;
17
18	/// The endpoint path for this server function
19	fn endpoint() -> &'static str;
20
21	/// The codec name ("json", "url", "msgpack")
22	fn codec() -> &'static str {
23		"json"
24	}
25}
26
27/// Unified error type for server functions
28///
29/// This error type covers all possible error conditions when calling
30/// a server function from the client side.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub enum ServerFnError {
33	/// Network error (connection failed, timeout, etc.)
34	Network(String),
35
36	/// Serialization error (failed to serialize arguments)
37	Serialization(String),
38
39	/// Deserialization error (failed to deserialize response)
40	Deserialization(String),
41
42	/// Server-side error (HTTP 4xx, 5xx)
43	Server {
44		/// HTTP status code
45		status: u16,
46		/// Error message
47		message: String,
48	},
49
50	/// Application error (custom error from server function)
51	Application(String),
52}
53
54impl ServerFnError {
55	/// Create a network error
56	pub fn network(msg: impl Into<String>) -> Self {
57		Self::Network(msg.into())
58	}
59
60	/// Create a serialization error
61	pub fn serialization(msg: impl Into<String>) -> Self {
62		Self::Serialization(msg.into())
63	}
64
65	/// Create a deserialization error
66	pub fn deserialization(msg: impl Into<String>) -> Self {
67		Self::Deserialization(msg.into())
68	}
69
70	/// Create a server error
71	pub fn server(status: u16, message: impl Into<String>) -> Self {
72		Self::Server {
73			status,
74			message: message.into(),
75		}
76	}
77
78	/// Create an application error
79	pub fn application(msg: impl Into<String>) -> Self {
80		Self::Application(msg.into())
81	}
82
83	/// Returns the human-readable message without the variant prefix.
84	///
85	/// Use this when surfacing the error text directly to end users;
86	/// use `to_string()` (`Display`) for the developer-facing form
87	/// that includes the variant tag.
88	///
89	/// # Examples
90	///
91	/// ```
92	/// use reinhardt_pages::ServerFnError;
93	///
94	/// let err = ServerFnError::application("Invalid choice_id");
95	/// assert_eq!(err.message(), "Invalid choice_id");
96	/// assert_eq!(err.to_string(), "Application error: Invalid choice_id");
97	/// ```
98	pub fn message(&self) -> &str {
99		match self {
100			Self::Network(msg)
101			| Self::Serialization(msg)
102			| Self::Deserialization(msg)
103			| Self::Application(msg) => msg,
104			Self::Server { message, .. } => message,
105		}
106	}
107}
108
109impl std::fmt::Display for ServerFnError {
110	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111		match self {
112			Self::Network(msg) => write!(f, "Network error: {}", msg),
113			Self::Serialization(msg) => write!(f, "Serialization error: {}", msg),
114			Self::Deserialization(msg) => write!(f, "Deserialization error: {}", msg),
115			Self::Server { status, message } => {
116				write!(f, "Server error ({}): {}", status, message)
117			}
118			Self::Application(msg) => write!(f, "Application error: {}", msg),
119		}
120	}
121}
122
123impl std::error::Error for ServerFnError {}
124
125/// Extract the human-readable message from a `ServerFnError` string,
126/// regardless of format.
127///
128/// Accepts three representations:
129///
130/// 1. **JSON wire format** — serde's externally-tagged envelope
131///    (e.g., `{"Application":"Invalid choice_id"}`).
132/// 2. **`Display` format** — the variant-prefixed string produced by
133///    `ServerFnError::to_string()` (e.g., `"Application error: msg"`).
134/// 3. **Plain text** — returned unchanged as a fallback.
135///
136/// # Examples
137///
138/// ```
139/// use reinhardt_pages::parse_server_error_message;
140///
141/// // JSON wire format
142/// let msg = parse_server_error_message(r#"{"Application":"Invalid choice_id"}"#);
143/// assert_eq!(msg, "Invalid choice_id");
144///
145/// // Display format (from .to_string())
146/// let msg = parse_server_error_message("Application error: Invalid choice_id");
147/// assert_eq!(msg, "Invalid choice_id");
148///
149/// // Plain text fallback
150/// let msg = parse_server_error_message("plain error text");
151/// assert_eq!(msg, "plain error text");
152/// ```
153pub fn parse_server_error_message(raw: &str) -> String {
154	// 1. Try JSON deserialization (wire format)
155	if let Ok(e) = serde_json::from_str::<ServerFnError>(raw) {
156		return unwrap_nested_or_raw(e.message());
157	}
158	// 2. Try stripping known Display prefixes
159	for prefix in [
160		"Network error: ",
161		"Serialization error: ",
162		"Deserialization error: ",
163		"Application error: ",
164	] {
165		if let Some(msg) = raw.strip_prefix(prefix) {
166			return unwrap_nested_or_raw(msg);
167		}
168	}
169	// 2b. Handle "Server error (NNN): " format
170	if let Some(rest) = raw.strip_prefix("Server error (")
171		&& let Some(idx) = rest.find("): ")
172	{
173		return unwrap_nested_or_raw(&rest[idx + 3..]);
174	}
175	// 3. Fallback: return unchanged
176	raw.to_string()
177}
178
179/// If `msg` is itself a JSON-serialized `ServerFnError` (nested envelope),
180/// unwrap it; otherwise return the string as-is.
181fn unwrap_nested_or_raw(msg: &str) -> String {
182	serde_json::from_str::<ServerFnError>(msg)
183		.map(|e| e.message().to_string())
184		.unwrap_or_else(|_| msg.to_string())
185}
186
187#[cfg(test)]
188mod tests {
189	use rstest::rstest;
190
191	use super::*;
192
193	#[rstest]
194	fn test_server_fn_error_creation() {
195		// Arrange & Act
196		let network_err = ServerFnError::network("Connection timeout");
197		let server_err = ServerFnError::server(404, "Not found");
198
199		// Assert
200		assert!(matches!(network_err, ServerFnError::Network(_)));
201		assert!(matches!(
202			server_err,
203			ServerFnError::Server { status: 404, .. }
204		));
205	}
206
207	#[rstest]
208	fn test_server_fn_error_display() {
209		// Arrange
210		let network_err = ServerFnError::network("Connection timeout");
211		let server_err = ServerFnError::server(500, "Internal error");
212
213		// Act & Assert
214		assert_eq!(network_err.to_string(), "Network error: Connection timeout");
215		assert_eq!(server_err.to_string(), "Server error (500): Internal error");
216	}
217
218	#[rstest]
219	#[case::network(ServerFnError::network("timeout"), "timeout")]
220	#[case::serialization(ServerFnError::serialization("bad input"), "bad input")]
221	#[case::deserialization(ServerFnError::deserialization("bad json"), "bad json")]
222	#[case::server(ServerFnError::server(403, "Forbidden"), "Forbidden")]
223	#[case::application(ServerFnError::application("Invalid choice_id"), "Invalid choice_id")]
224	fn test_message_returns_inner_text(#[case] err: ServerFnError, #[case] expected: &str) {
225		// Act
226		let msg = err.message();
227
228		// Assert
229		assert_eq!(msg, expected);
230	}
231
232	#[rstest]
233	fn test_message_returns_empty_string_when_inner_is_empty() {
234		// Arrange
235		let err = ServerFnError::application("");
236
237		// Act & Assert
238		assert_eq!(err.message(), "");
239	}
240
241	#[rstest]
242	fn test_message_differs_from_display() {
243		// Arrange
244		let err = ServerFnError::application("Invalid choice_id");
245
246		// Act
247		let message = err.message();
248		let display = err.to_string();
249
250		// Assert
251		assert_ne!(message, display);
252		assert_eq!(message, "Invalid choice_id");
253		assert_eq!(display, "Application error: Invalid choice_id");
254	}
255
256	#[rstest]
257	#[case::application(r#"{"Application":"Invalid choice_id"}"#, "Invalid choice_id")]
258	#[case::server(r#"{"Server":{"status":403,"message":"Forbidden"}}"#, "Forbidden")]
259	#[case::network(r#"{"Network":"Connection timeout"}"#, "Connection timeout")]
260	fn test_parse_server_error_message_from_json(#[case] json: &str, #[case] expected: &str) {
261		// Act
262		let msg = parse_server_error_message(json);
263
264		// Assert
265		assert_eq!(msg, expected);
266	}
267
268	#[rstest]
269	#[case::application("Application error: Invalid choice_id", "Invalid choice_id")]
270	#[case::network("Network error: Connection timeout", "Connection timeout")]
271	#[case::serialization("Serialization error: bad input", "bad input")]
272	#[case::deserialization("Deserialization error: bad json", "bad json")]
273	#[case::server("Server error (403): Forbidden", "Forbidden")]
274	#[case::server_500("Server error (500): Internal error", "Internal error")]
275	fn test_parse_server_error_message_from_display(#[case] display: &str, #[case] expected: &str) {
276		// Act
277		let msg = parse_server_error_message(display);
278
279		// Assert
280		assert_eq!(msg, expected);
281	}
282
283	#[rstest]
284	#[case::server_wrapping_application(
285		r#"Server error (500): {"Application":"Invalid choice_id"}"#,
286		"Invalid choice_id"
287	)]
288	#[case::server_wrapping_network(
289		r#"Server error (500): {"Network":"Connection lost"}"#,
290		"Connection lost"
291	)]
292	#[case::json_server_wrapping_application(
293		r#"{"Server":{"status":500,"message":"{\"Application\":\"Invalid choice_id\"}"}}"#,
294		"Invalid choice_id"
295	)]
296	fn test_parse_server_error_message_unwraps_nested_json(
297		#[case] input: &str,
298		#[case] expected: &str,
299	) {
300		// Act
301		let msg = parse_server_error_message(input);
302
303		// Assert
304		assert_eq!(msg, expected);
305	}
306
307	#[rstest]
308	fn test_parse_server_error_message_falls_back_for_invalid_json() {
309		// Arrange
310		let raw = "plain error text";
311
312		// Act
313		let msg = parse_server_error_message(raw);
314
315		// Assert
316		assert_eq!(msg, "plain error text");
317	}
318
319	#[rstest]
320	fn test_parse_server_error_message_falls_back_for_empty_string() {
321		// Act
322		let msg = parse_server_error_message("");
323
324		// Assert
325		assert_eq!(msg, "");
326	}
327
328	#[rstest]
329	fn test_parse_server_error_message_falls_back_for_non_server_fn_error_json() {
330		// Arrange
331		let raw = r#"{"foo":"bar"}"#;
332
333		// Act
334		let msg = parse_server_error_message(raw);
335
336		// Assert
337		assert_eq!(msg, raw);
338	}
339}