Skip to main content

ferro_inertia/
config.rs

1//! Configuration for Inertia.js integration.
2
3/// Configuration for Inertia.js responses.
4///
5/// # Example
6///
7/// ```rust
8/// use ferro_inertia::InertiaConfig;
9///
10/// // Development configuration (default)
11/// let config = InertiaConfig::default();
12///
13/// // Production configuration
14/// let config = InertiaConfig::new()
15///     .version("1.0.0")
16///     .production();
17///
18/// // Custom Vite dev server
19/// let config = InertiaConfig::new()
20///     .vite_dev_server("http://localhost:3000")
21///     .entry_point("src/app.tsx");
22/// ```
23#[derive(Debug, Clone)]
24pub struct InertiaConfig {
25    /// Application name used in the HTML title tag
26    pub app_name: String,
27    /// Vite dev server URL (e.g., "http://localhost:5173")
28    pub vite_dev_server: String,
29    /// Entry point for the frontend (e.g., "src/main.tsx")
30    pub entry_point: String,
31    /// Asset version for cache busting
32    pub version: String,
33    /// Whether we're in development mode (use Vite dev server)
34    pub development: bool,
35    /// Custom HTML template (if None, uses default)
36    pub html_template: Option<String>,
37    /// Path to Vite's manifest.json for resolving hashed asset filenames
38    pub manifest_path: String,
39    /// Optional page title. When `Some`, overrides `app_name` in `<title>`.
40    pub title: Option<String>,
41    /// Raw HTML injected into `<head>` before `</head>` (meta, favicon, font tags).
42    /// SECURITY: developer-controlled config only — never populate from request data.
43    /// Ignored when `html_template` is set (the custom template owns `<head>`).
44    pub head_extras: Option<String>,
45    /// id attribute of the mount node. Defaults to `"app"`.
46    pub mount_id: String,
47}
48
49impl InertiaConfig {
50    /// Build configuration from environment variables.
51    ///
52    /// Reads `APP_NAME`, `VITE_DEV_SERVER`, `VITE_ENTRY_POINT`, `INERTIA_VERSION`,
53    /// and `APP_ENV`. Mirrors the framework `from_env()` convention.
54    pub fn from_env() -> Self {
55        let vite_dev_server = std::env::var("VITE_DEV_SERVER")
56            .unwrap_or_else(|_| "http://localhost:5173".to_string());
57
58        let is_dev = !matches!(
59            std::env::var("APP_ENV").ok().as_deref(),
60            Some("production") | Some("staging")
61        );
62
63        let app_name = std::env::var("APP_NAME").unwrap_or_else(|_| "Ferro".to_string());
64
65        Self {
66            app_name,
67            vite_dev_server,
68            entry_point: std::env::var("VITE_ENTRY_POINT")
69                .unwrap_or_else(|_| "src/main.tsx".to_string()),
70            version: std::env::var("INERTIA_VERSION").unwrap_or_else(|_| "1.0".to_string()),
71            development: is_dev,
72            html_template: None,
73            manifest_path: "public/.vite/manifest.json".to_string(),
74            title: None,
75            head_extras: None,
76            mount_id: "app".to_string(),
77        }
78    }
79
80    /// Create a new configuration with default values.
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    /// Set the Vite dev server URL.
86    pub fn vite_dev_server(mut self, url: impl Into<String>) -> Self {
87        self.vite_dev_server = url.into();
88        self
89    }
90
91    /// Set the frontend entry point.
92    pub fn entry_point(mut self, entry: impl Into<String>) -> Self {
93        self.entry_point = entry.into();
94        self
95    }
96
97    /// Set the asset version for cache busting.
98    pub fn version(mut self, version: impl Into<String>) -> Self {
99        self.version = version.into();
100        self
101    }
102
103    /// Enable production mode (disables Vite dev server integration).
104    pub fn production(mut self) -> Self {
105        self.development = false;
106        self
107    }
108
109    /// Enable development mode (enables Vite dev server integration).
110    pub fn development(mut self) -> Self {
111        self.development = true;
112        self
113    }
114
115    /// Set the application name used in the HTML title tag.
116    pub fn app_name(mut self, name: impl Into<String>) -> Self {
117        self.app_name = name.into();
118        self
119    }
120
121    /// Set the path to Vite's manifest.json.
122    pub fn manifest_path(mut self, path: impl Into<String>) -> Self {
123        self.manifest_path = path.into();
124        self
125    }
126
127    /// Set a custom HTML template.
128    ///
129    /// The template should contain the following placeholders:
130    /// - `{page}` - The escaped JSON page data
131    /// - `{csrf}` - The CSRF token (optional)
132    ///
133    /// # Example
134    ///
135    /// ```rust
136    /// use ferro_inertia::InertiaConfig;
137    ///
138    /// let template = r#"
139    /// <!DOCTYPE html>
140    /// <html>
141    /// <head><title>My App</title></head>
142    /// <body>
143    ///     <div id="app" data-page="{page}"></div>
144    ///     <script src="/app.js"></script>
145    /// </body>
146    /// </html>
147    /// "#;
148    ///
149    /// let config = InertiaConfig::new()
150    ///     .html_template(template);
151    /// ```
152    pub fn html_template(mut self, template: impl Into<String>) -> Self {
153        self.html_template = Some(template.into());
154        self
155    }
156
157    /// Override the `<title>` tag. When `None`, falls back to `app_name`.
158    pub fn title(mut self, t: impl Into<String>) -> Self {
159        self.title = Some(t.into());
160        self
161    }
162
163    /// Raw HTML injected into `<head>` before `</head>`.
164    /// Ignored when `html_template` is set.
165    pub fn head_extras(mut self, h: impl Into<String>) -> Self {
166        self.head_extras = Some(h.into());
167        self
168    }
169
170    /// Set the mount node id (default `"app"`).
171    pub fn mount_id(mut self, id: impl Into<String>) -> Self {
172        self.mount_id = id.into();
173        self
174    }
175}
176
177impl Default for InertiaConfig {
178    fn default() -> Self {
179        Self::from_env()
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn from_env_reads_defaults() {
189        // CI does not set VITE_ENTRY_POINT / INERTIA_VERSION, so defaults apply.
190        let c = InertiaConfig::from_env();
191        assert_eq!(c.entry_point, "src/main.tsx");
192        assert_eq!(c.version, "1.0");
193        assert_eq!(c.mount_id, "app");
194    }
195
196    #[test]
197    fn new_fields_default_to_none() {
198        let c = InertiaConfig::from_env();
199        assert!(c.title.is_none());
200        assert!(c.head_extras.is_none());
201    }
202
203    #[test]
204    fn builders_set_new_fields() {
205        let c = InertiaConfig::new()
206            .title("My App")
207            .head_extras(r#"<link rel="icon" href="/favicon.ico">"#)
208            .mount_id("root");
209        assert_eq!(c.title.as_deref(), Some("My App"));
210        assert_eq!(
211            c.head_extras.as_deref(),
212            Some(r#"<link rel="icon" href="/favicon.ico">"#)
213        );
214        assert_eq!(c.mount_id, "root");
215    }
216
217    #[test]
218    fn default_equals_from_env_shape() {
219        let a = InertiaConfig::default();
220        let b = InertiaConfig::from_env();
221        assert_eq!(a.entry_point, b.entry_point);
222        assert_eq!(a.version, b.version);
223        assert_eq!(a.mount_id, b.mount_id);
224    }
225}