1use std::path::PathBuf;
4use std::time::Duration;
5
6use crate::error::{SsrError, SsrResult};
7
8#[derive(Debug, Clone)]
10pub struct SsrConfig {
11 pub bundle_path: PathBuf,
13
14 pub pool_size: usize,
16
17 pub queue_capacity: usize,
19
20 pub pin_threads: bool,
22
23 pub cache_size: usize,
25
26 pub cache_ttl: Option<Duration>,
28
29 pub request_timeout: Option<Duration>,
31
32 pub render_function: String,
34
35 pub html_template_path: Option<PathBuf>,
44
45 pub assets_manifest_path: Option<PathBuf>,
50}
51
52impl Default for SsrConfig {
53 fn default() -> Self {
54 Self {
55 bundle_path: PathBuf::from("ssr-bundle.js"),
56 pool_size: num_cpus::get(),
57 queue_capacity: 512,
58 pin_threads: false,
59 cache_size: 300,
60 cache_ttl: Some(Duration::from_secs(300)), request_timeout: Some(Duration::from_secs(30)),
62 render_function: "renderPage".to_string(),
63 html_template_path: None,
64 assets_manifest_path: None,
65 }
66 }
67}
68
69impl SsrConfig {
70 pub fn builder() -> SsrConfigBuilder {
72 SsrConfigBuilder::default()
73 }
74}
75
76#[derive(Debug, Default)]
78pub struct SsrConfigBuilder {
79 bundle_path: Option<PathBuf>,
80 pool_size: Option<usize>,
81 queue_capacity: Option<usize>,
82 pin_threads: Option<bool>,
83 cache_size: Option<usize>,
84 cache_ttl: Option<Option<Duration>>,
85 request_timeout: Option<Option<Duration>>,
86 render_function: Option<String>,
87 html_template_path: Option<PathBuf>,
88 assets_manifest_path: Option<PathBuf>,
89}
90
91impl SsrConfigBuilder {
92 pub fn bundle_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
103 self.bundle_path = Some(path.into());
104 self
105 }
106
107 pub fn pool_size(mut self, size: usize) -> Self {
111 self.pool_size = Some(size);
112 self
113 }
114
115 pub fn queue_capacity(mut self, capacity: usize) -> Self {
119 self.queue_capacity = Some(capacity);
120 self
121 }
122
123 pub fn pin_threads(mut self, pin: bool) -> Self {
127 self.pin_threads = Some(pin);
128 self
129 }
130
131 pub fn cache_size(mut self, size: usize) -> Self {
135 self.cache_size = Some(size);
136 self
137 }
138
139 pub fn cache_ttl(mut self, ttl: Option<Duration>) -> Self {
143 self.cache_ttl = Some(ttl);
144 self
145 }
146
147 pub fn cache_ttl_secs(mut self, secs: u64) -> Self {
151 self.cache_ttl = Some(if secs > 0 {
152 Some(Duration::from_secs(secs))
153 } else {
154 None
155 });
156 self
157 }
158
159 pub fn request_timeout(mut self, timeout: Option<Duration>) -> Self {
163 self.request_timeout = Some(timeout);
164 self
165 }
166
167 pub fn html_template<P: Into<PathBuf>>(mut self, path: P) -> Self {
184 self.html_template_path = Some(path.into());
185 self
186 }
187
188 pub fn assets_manifest<P: Into<PathBuf>>(mut self, path: P) -> Self {
192 self.assets_manifest_path = Some(path.into());
193 self
194 }
195
196 pub fn render_function<S: Into<String>>(mut self, name: S) -> Self {
202 self.render_function = Some(name.into());
203 self
204 }
205
206 pub fn build(self) -> SsrResult<SsrConfig> {
215 let default = SsrConfig::default();
216
217 let config = SsrConfig {
218 bundle_path: self.bundle_path.unwrap_or(default.bundle_path),
219 pool_size: self.pool_size.unwrap_or(default.pool_size),
220 queue_capacity: self.queue_capacity.unwrap_or(default.queue_capacity),
221 pin_threads: self.pin_threads.unwrap_or(default.pin_threads),
222 cache_size: self.cache_size.unwrap_or(default.cache_size),
223 cache_ttl: self.cache_ttl.unwrap_or(default.cache_ttl),
224 request_timeout: self.request_timeout.unwrap_or(default.request_timeout),
225 render_function: self.render_function.unwrap_or(default.render_function),
226 html_template_path: self.html_template_path,
227 assets_manifest_path: self.assets_manifest_path,
228 };
229
230 if config.pool_size == 0 {
231 return Err(SsrError::Config("pool_size must be > 0".into()));
232 }
233 if config.cache_size == 0 {
234 return Err(SsrError::Config("cache_size must be > 0".into()));
235 }
236 if config.queue_capacity == 0 {
237 return Err(SsrError::Config("queue_capacity must be > 0".into()));
238 }
239 if config.render_function.is_empty()
240 || !config
241 .render_function
242 .chars()
243 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.')
244 {
245 return Err(SsrError::Config(format!(
246 "render_function must be a valid JS identifier, got: {:?}",
247 config.render_function
248 )));
249 }
250
251 Ok(config)
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_default_config() {
261 let config = SsrConfig::default();
262 assert_eq!(config.pool_size, num_cpus::get());
263 assert_eq!(config.cache_size, 300);
264 assert!(!config.pin_threads);
265 }
266
267 #[test]
268 fn test_builder() {
269 let config = SsrConfig::builder()
270 .bundle_path("custom.js")
271 .pool_size(4)
272 .cache_size(100)
273 .pin_threads(true)
274 .build()
275 .unwrap();
276
277 assert_eq!(config.bundle_path, PathBuf::from("custom.js"));
278 assert_eq!(config.pool_size, 4);
279 assert_eq!(config.cache_size, 100);
280 assert!(config.pin_threads);
281 }
282
283 #[test]
284 fn test_zero_pool_size_rejected() {
285 let result = SsrConfig::builder().pool_size(0).build();
286 assert!(result.is_err());
287 }
288
289 #[test]
290 fn test_zero_cache_size_rejected() {
291 let result = SsrConfig::builder().cache_size(0).build();
292 assert!(result.is_err());
293 }
294
295 #[test]
296 fn test_empty_render_function_rejected() {
297 let result = SsrConfig::builder().render_function("").build();
298 assert!(result.is_err());
299 }
300
301 #[test]
302 fn test_invalid_render_function_rejected() {
303 let result = SsrConfig::builder()
304 .render_function("foo; evil()")
305 .build();
306 assert!(result.is_err());
307 }
308
309 #[test]
310 fn test_dotted_render_function_ok() {
311 let config = SsrConfig::builder()
312 .render_function("module.renderPage")
313 .build()
314 .unwrap();
315 assert_eq!(config.render_function, "module.renderPage");
316 }
317}