ferro_json_ui/config.rs
1//! Configuration for JSON-UI rendering.
2//!
3//! Controls rendering behavior such as Tailwind CDN inclusion,
4//! custom head content injection, default body CSS classes, and
5//! stylesheet URLs emitted as `<link>` tags in the HTML `<head>`.
6
7/// Configuration for JSON-UI HTML rendering.
8///
9/// # Example
10///
11/// ```rust
12/// use ferro_json_ui::JsonUiConfig;
13///
14/// // Default: framework-served base CSS with cache-busting version, no Tailwind CDN.
15/// let default_config = JsonUiConfig::new();
16/// assert!(default_config.stylesheet_urls[0].starts_with("/_ferro/ferro-base.css"));
17/// assert!(!default_config.tailwind_cdn);
18///
19/// // Opt into CDN for dev, and override stylesheet list:
20/// let dev_config = JsonUiConfig::new()
21/// .tailwind_cdn(true)
22/// .stylesheet_urls(vec!["/custom.css".to_string()]);
23/// ```
24#[derive(Debug, Clone, schemars::JsonSchema)]
25pub struct JsonUiConfig {
26 /// Include Tailwind CDN link in rendered HTML (dev convenience).
27 ///
28 /// Default: `false`. Set to `true` to load the Tailwind v4 browser runtime
29 /// from cdn.jsdelivr.net. The runtime is a development convenience per
30 /// Tailwind's docs — it does not work reliably on Safari/WebKit. Production
31 /// apps rely on the pre-built base CSS served via `stylesheet_urls`.
32 pub tailwind_cdn: bool,
33
34 /// Stylesheet URLs emitted as `<link rel="stylesheet" href="...">` in `<head>`,
35 /// in order.
36 ///
37 /// Default: `vec!["/_ferro/ferro-base.css".to_string()]` — the framework-served
38 /// pre-built base CSS. Apps override this list to inject additional stylesheets
39 /// (e.g., app-level theme token files) or to drop the default entirely.
40 pub stylesheet_urls: Vec<String>,
41
42 /// Custom content to inject into the `<head>` element.
43 pub custom_head: Option<String>,
44 /// Default CSS classes for the `<body>` element.
45 pub body_class: String,
46}
47
48impl Default for JsonUiConfig {
49 fn default() -> Self {
50 Self {
51 tailwind_cdn: false,
52 stylesheet_urls: vec![format!(
53 "/_ferro/ferro-base.css?v={}",
54 env!("CARGO_PKG_VERSION")
55 )],
56 custom_head: None,
57 body_class: "bg-background text-text font-sans".to_string(),
58 }
59 }
60}
61
62impl JsonUiConfig {
63 /// Create a new configuration with default values.
64 pub fn new() -> Self {
65 Self::default()
66 }
67
68 /// Enable or disable Tailwind CDN inclusion.
69 pub fn tailwind_cdn(mut self, enabled: bool) -> Self {
70 self.tailwind_cdn = enabled;
71 self
72 }
73
74 /// Replace the stylesheet URL list.
75 ///
76 /// Each URL emits a `<link rel="stylesheet" href="...">` in `<head>`, in order.
77 /// Default: `["/_ferro/ferro-base.css"]`.
78 ///
79 /// Pass an empty `Vec` to disable the framework-served base CSS.
80 pub fn stylesheet_urls(mut self, urls: Vec<String>) -> Self {
81 self.stylesheet_urls = urls;
82 self
83 }
84
85 /// Set custom content to inject into the `<head>` element.
86 pub fn custom_head(mut self, head: impl Into<String>) -> Self {
87 self.custom_head = Some(head.into());
88 self
89 }
90
91 /// Set the default CSS classes for the `<body>` element.
92 pub fn body_class(mut self, class: impl Into<String>) -> Self {
93 self.body_class = class.into();
94 self
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn default_has_tailwind_cdn_false_and_default_stylesheet_urls() {
104 let c = JsonUiConfig::default();
105 assert!(!c.tailwind_cdn, "tailwind_cdn default must be false");
106 assert_eq!(c.stylesheet_urls.len(), 1);
107 assert!(
108 c.stylesheet_urls[0].starts_with("/_ferro/ferro-base.css?v="),
109 "default stylesheet_urls must be versioned /_ferro/ferro-base.css?v=...; got {:?}",
110 c.stylesheet_urls
111 );
112 }
113
114 #[test]
115 fn stylesheet_urls_builder_replaces_entire_list() {
116 let c =
117 JsonUiConfig::new().stylesheet_urls(vec!["/a.css".to_string(), "/b.css".to_string()]);
118 assert_eq!(c.stylesheet_urls, vec!["/a.css", "/b.css"]);
119 assert!(
120 !c.stylesheet_urls
121 .contains(&"/_ferro/ferro-base.css".to_string()),
122 "builder must replace the default, not append"
123 );
124 }
125
126 #[test]
127 fn stylesheet_urls_builder_accepts_empty_vec() {
128 let c = JsonUiConfig::new().stylesheet_urls(vec![]);
129 assert!(c.stylesheet_urls.is_empty());
130 }
131
132 #[test]
133 fn json_schema_derives_with_new_field() {
134 // Proves schemars::JsonSchema derive still compiles after adding Vec<String>.
135 let _schema = schemars::schema_for!(JsonUiConfig);
136 }
137}