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}