Skip to main content

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}