Skip to main content

standout_render/
embedded.rs

1//! Embedded resource source types for compile-time embedding with debug hot-reload.
2//!
3//! This module provides types that hold both embedded content (for release builds)
4//! and source paths (for debug hot-reload). The macros `embed_templates!` and
5//! `embed_styles!` return these types, and `App::builder()` consumes them.
6//!
7//! # Design
8//!
9//! The key insight is that we want:
10//! - Release builds: Use embedded content, zero file I/O
11//! - Debug builds: Hot-reload from disk if source path exists
12//!
13//! By storing both the embedded content AND the source path, we can make this
14//! decision at runtime based on `cfg!(debug_assertions)` and path existence.
15//!
16//! # Example
17//!
18//! ```rust,ignore
19//! use standout_render::{EmbeddedSource, TemplateResource, TemplateRegistry};
20//!
21//! // Create from macro output (typically done by embed_templates!/embed_styles!)
22//! static ENTRIES: &[(&str, &str)] = &[("list.jinja", "{{ items }}")];
23//! let source: EmbeddedSource<TemplateResource> = EmbeddedSource::new(ENTRIES, "src/templates");
24//!
25//! // Convert to registry
26//! let registry: TemplateRegistry = source.into();
27//! ```
28
29use std::marker::PhantomData;
30use std::path::Path;
31
32use crate::file_loader::{build_embedded_registry, walk_dir};
33use crate::style::{parse_theme_content, StylesheetRegistry, STYLESHEET_EXTENSIONS};
34use crate::template::{walk_template_dir, TemplateRegistry};
35
36/// Marker type for template resources.
37#[derive(Debug, Clone, Copy)]
38pub struct TemplateResource;
39
40/// Marker type for stylesheet resources.
41#[derive(Debug, Clone, Copy)]
42pub struct StylesheetResource;
43
44/// Embedded resource source with optional debug hot-reload.
45///
46/// This type holds:
47/// - Embedded entries (name, content) pairs baked in at compile time
48/// - The source path for debug hot-reload
49///
50/// The type parameter `R` is a marker indicating the resource type
51/// (templates or stylesheets).
52#[derive(Debug, Clone)]
53pub struct EmbeddedSource<R> {
54    /// The embedded entries as (name_with_extension, content) pairs.
55    /// This is `'static` because it's baked into the binary at compile time.
56    pub entries: &'static [(&'static str, &'static str)],
57
58    /// The source path used for embedding.
59    /// In debug mode, if this path exists, files are read from disk instead.
60    pub source_path: &'static str,
61
62    /// Marker for the resource type.
63    _marker: PhantomData<R>,
64}
65
66impl<R> EmbeddedSource<R> {
67    /// Creates a new embedded source.
68    ///
69    /// This is typically called by the `embed_templates!` and `embed_styles!` macros.
70    #[doc(hidden)]
71    pub const fn new(
72        entries: &'static [(&'static str, &'static str)],
73        source_path: &'static str,
74    ) -> Self {
75        Self {
76            entries,
77            source_path,
78            _marker: PhantomData,
79        }
80    }
81
82    /// Returns the embedded entries.
83    pub fn entries(&self) -> &'static [(&'static str, &'static str)] {
84        self.entries
85    }
86
87    /// Returns the source path.
88    pub fn source_path(&self) -> &'static str {
89        self.source_path
90    }
91
92    /// Returns true if hot-reload should be used.
93    ///
94    /// Hot-reload is enabled when:
95    /// - We're in debug mode (`debug_assertions` enabled)
96    /// - The source path exists on disk
97    pub fn should_hot_reload(&self) -> bool {
98        cfg!(debug_assertions) && std::path::Path::new(self.source_path).exists()
99    }
100}
101
102/// Type alias for embedded templates.
103pub type EmbeddedTemplates = EmbeddedSource<TemplateResource>;
104
105/// Type alias for embedded stylesheets.
106pub type EmbeddedStyles = EmbeddedSource<StylesheetResource>;
107
108impl From<EmbeddedTemplates> for TemplateRegistry {
109    /// Converts embedded templates into a TemplateRegistry.
110    ///
111    /// In debug mode, if the source path exists, templates are loaded from disk
112    /// (enabling hot-reload). Otherwise, embedded content is used.
113    fn from(source: EmbeddedTemplates) -> Self {
114        if source.should_hot_reload() {
115            // Debug mode with existing source path: load from filesystem
116            // Use walk_template_dir + add_from_files for immediate loading
117            // (add_template_dir uses lazy loading which doesn't work well here)
118            let files = match walk_template_dir(source.source_path) {
119                Ok(files) => files,
120                Err(e) => {
121                    eprintln!(
122                        "Warning: Failed to walk templates directory '{}', using embedded: {}",
123                        source.source_path, e
124                    );
125                    return TemplateRegistry::from_embedded_entries(source.entries);
126                }
127            };
128
129            let mut registry = TemplateRegistry::new();
130            if let Err(e) = registry.add_from_files(files) {
131                eprintln!(
132                    "Warning: Failed to register templates from '{}', using embedded: {}",
133                    source.source_path, e
134                );
135                return TemplateRegistry::from_embedded_entries(source.entries);
136            }
137            registry
138        } else {
139            // Release mode or missing source: use embedded content
140            TemplateRegistry::from_embedded_entries(source.entries)
141        }
142    }
143}
144
145impl From<EmbeddedStyles> for StylesheetRegistry {
146    /// Converts embedded styles into a StylesheetRegistry.
147    ///
148    /// In debug mode, if the source path exists, styles are loaded from disk
149    /// (enabling hot-reload). Otherwise, embedded content is used.
150    ///
151    /// # Panics
152    ///
153    /// Panics if embedded stylesheet content (CSS or YAML) fails to parse
154    /// (should be caught in dev).
155    fn from(source: EmbeddedStyles) -> Self {
156        if source.should_hot_reload() {
157            // Debug mode with existing source path: load from filesystem
158            // Walk directory and load immediately (add_dir uses lazy loading which
159            // doesn't work well for names() iteration)
160            let files = match walk_dir(Path::new(source.source_path), STYLESHEET_EXTENSIONS) {
161                Ok(files) => files,
162                Err(e) => {
163                    eprintln!(
164                        "Warning: Failed to walk styles directory '{}', using embedded: {}",
165                        source.source_path, e
166                    );
167                    return StylesheetRegistry::from_embedded_entries(source.entries)
168                        .expect("embedded stylesheets should parse");
169                }
170            };
171
172            // Read file contents into (name_with_ext, content) pairs
173            let entries: Vec<(String, String)> = files
174                .into_iter()
175                .filter_map(|file| match std::fs::read_to_string(&file.path) {
176                    Ok(content) => Some((file.name_with_ext, content)),
177                    Err(e) => {
178                        eprintln!(
179                            "Warning: Failed to read stylesheet '{}': {}",
180                            file.path.display(),
181                            e
182                        );
183                        None
184                    }
185                })
186                .collect();
187
188            // Build registry with extension priority handling
189            let entries_refs: Vec<(&str, &str)> = entries
190                .iter()
191                .map(|(n, c)| (n.as_str(), c.as_str()))
192                .collect();
193
194            let inline =
195                match build_embedded_registry(&entries_refs, STYLESHEET_EXTENSIONS, |content| {
196                    parse_theme_content(content)
197                }) {
198                    Ok(map) => map,
199                    Err(e) => {
200                        eprintln!(
201                            "Warning: Failed to parse stylesheets from '{}', using embedded: {}",
202                            source.source_path, e
203                        );
204                        return StylesheetRegistry::from_embedded_entries(source.entries)
205                            .expect("embedded stylesheets should parse");
206                    }
207                };
208
209            let mut registry = StylesheetRegistry::new();
210            registry.add_embedded(inline);
211            registry
212        } else {
213            // Release mode or missing source: use embedded content
214            StylesheetRegistry::from_embedded_entries(source.entries)
215                .expect("embedded stylesheets should parse")
216        }
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_embedded_source_new() {
226        static ENTRIES: &[(&str, &str)] = &[("test.jinja", "content")];
227        let source: EmbeddedTemplates = EmbeddedSource::new(ENTRIES, "src/templates");
228
229        assert_eq!(source.entries().len(), 1);
230        assert_eq!(source.source_path(), "src/templates");
231    }
232
233    #[test]
234    fn test_should_hot_reload_nonexistent_path() {
235        static ENTRIES: &[(&str, &str)] = &[];
236        let source: EmbeddedTemplates = EmbeddedSource::new(ENTRIES, "/nonexistent/path");
237
238        // Should be false because path doesn't exist
239        assert!(!source.should_hot_reload());
240    }
241}