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