standout_dispatch/render.rs
1//! Render function abstraction.
2//!
3//! This module defines the contract between dispatch and renderers. The key design
4//! principle is that dispatch is render-agnostic: it doesn't know about templates,
5//! themes, output formats, or any rendering implementation details.
6//!
7//! # Design Rationale
8//!
9//! The render handler is a pluggable callback that the consuming framework provides.
10//! This separation exists because:
11//!
12//! 1. Flexibility: Different applications may use different renderers (or none at all)
13//! 2. Separation of concerns: Business logic (handlers) shouldn't know about presentation
14//! 3. Runtime configuration: Format/theme decisions happen at runtime (from CLI args),
15//! not at compile time
16//!
17//! # The Closure Pattern
18//!
19//! Render handlers capture their context (format, theme, etc.) in a closure:
20//!
21//! ```rust,ignore
22//! // At runtime, after parsing --output=json:
23//! let format = extract_output_mode(&matches);
24//! let theme = &app.theme;
25//! let templates = &app.templates;
26//!
27//! // Create render handler with context baked in
28//! let render_handler = from_fn(move |data| {
29//! render_with_format(templates, theme, format, data)
30//! });
31//! ```
32//!
33//! Dispatch calls `render_handler(data)` without knowing what's inside the closure.
34//! All format/theme/template logic lives in the closure, created by the framework layer.
35//!
36//! # Thread Safety
37//!
38//! Two variants are provided:
39//! - [`RenderFn`]: Thread-safe (`Send + Sync`), uses `Arc`
40//! - [`LocalRenderFn`]: Single-threaded, uses `Rc<RefCell>`, allows `FnMut`
41
42use std::sync::Arc;
43
44/// The render function signature.
45///
46/// Takes handler data (as JSON) and returns formatted output. The render function
47/// is a closure that captures all rendering context (format, theme, templates, etc.)
48/// so dispatch doesn't need to know about any of it.
49///
50/// # Example
51///
52/// ```rust,ignore
53/// // Framework creates render handler with context captured
54/// let render_handler = from_fn(move |data| {
55/// match format {
56/// Format::Json => serde_json::to_string_pretty(data),
57/// Format::Term => render_template(template, data, theme),
58/// // ...
59/// }
60/// });
61/// ```
62pub type RenderFn = Arc<dyn Fn(&serde_json::Value) -> Result<String, RenderError> + Send + Sync>;
63
64/// A local (non-Send) render function for single-threaded use.
65///
66/// Unlike [`RenderFn`], this uses `Rc<RefCell>` and allows `FnMut` closures,
67/// enabling mutable state in the render handler without `Send + Sync` overhead.
68pub type LocalRenderFn =
69 std::rc::Rc<std::cell::RefCell<dyn FnMut(&serde_json::Value) -> Result<String, RenderError>>>;
70
71/// Errors that can occur during rendering.
72#[derive(Debug, thiserror::Error)]
73pub enum RenderError {
74 /// Template rendering failed
75 #[error("render error: {0}")]
76 Render(String),
77
78 /// Data serialization failed
79 #[error("serialization error: {0}")]
80 Serialization(String),
81
82 /// Other error
83 #[error("{0}")]
84 Other(String),
85}
86
87impl From<serde_json::Error> for RenderError {
88 fn from(e: serde_json::Error) -> Self {
89 RenderError::Serialization(e.to_string())
90 }
91}
92
93/// Creates a render function from a closure.
94///
95/// This is the primary way to provide custom rendering logic. The closure
96/// should capture any context it needs (format, theme, templates, etc.).
97///
98/// # Example
99///
100/// ```rust
101/// use standout_dispatch::{from_fn, RenderError};
102///
103/// let render = from_fn(|data| {
104/// Ok(serde_json::to_string_pretty(data)?)
105/// });
106/// ```
107pub fn from_fn<F>(f: F) -> RenderFn
108where
109 F: Fn(&serde_json::Value) -> Result<String, RenderError> + Send + Sync + 'static,
110{
111 Arc::new(f)
112}
113
114/// Creates a local render function from a FnMut closure.
115///
116/// Use this when the render handler needs mutable state and doesn't need
117/// to be thread-safe.
118pub fn from_fn_mut<F>(f: F) -> LocalRenderFn
119where
120 F: FnMut(&serde_json::Value) -> Result<String, RenderError> + 'static,
121{
122 std::rc::Rc::new(std::cell::RefCell::new(f))
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use serde_json::json;
129
130 #[test]
131 fn test_from_fn_json() {
132 let render = from_fn(|data| {
133 serde_json::to_string_pretty(data)
134 .map_err(|e| RenderError::Serialization(e.to_string()))
135 });
136
137 let data = json!({"name": "test"});
138 let result = render(&data).unwrap();
139 assert!(result.contains("\"name\": \"test\""));
140 }
141
142 #[test]
143 fn test_from_fn_custom() {
144 let render = from_fn(|data| {
145 let name = data
146 .get("name")
147 .and_then(|v| v.as_str())
148 .unwrap_or("unknown");
149 Ok(format!("Hello, {}!", name))
150 });
151
152 let data = json!({"name": "world"});
153 let result = render(&data).unwrap();
154 assert_eq!(result, "Hello, world!");
155 }
156
157 #[test]
158 fn test_from_fn_mut() {
159 let render = from_fn_mut(|data| Ok(data.to_string()));
160
161 let data = json!({"key": "value"});
162 let result = render.borrow_mut()(&data).unwrap();
163 assert!(result.contains("key"));
164 }
165
166 #[test]
167 fn test_render_error_from_serde() {
168 let err: RenderError = serde_json::from_str::<serde_json::Value>("invalid")
169 .unwrap_err()
170 .into();
171 assert!(matches!(err, RenderError::Serialization(_)));
172 }
173}