konnektoren_core/
asset_loader.rs

1use std::path::PathBuf;
2
3#[cfg(feature = "csr")]
4use gloo::net::http::Request;
5
6#[derive(Debug, Clone)]
7pub enum AssetLoader {
8    /// Load assets from URLs (used in CSR)
9    Url { base_url: String },
10    /// Load assets from the filesystem (used in SSR)
11    File { base_dirs: Vec<PathBuf> },
12}
13
14impl Default for AssetLoader {
15    fn default() -> Self {
16        #[cfg(feature = "csr")]
17        {
18            // Default for CSR is to use URL loader with /assets/ base
19            AssetLoader::Url {
20                base_url: "/assets/".to_string(),
21            }
22        }
23
24        #[cfg(all(feature = "ssr", not(feature = "csr")))]
25        {
26            // Default for SSR is to use File loader with BUILD_DIR or current directory
27            let mut base_dirs = Vec::new();
28
29            // Try BUILD_DIR environment variable if set
30            if let Ok(build_dir) = std::env::var("BUILD_DIR") {
31                base_dirs.push(PathBuf::from(build_dir));
32            }
33
34            // Add current directory and public directory as fallbacks
35            base_dirs.push(PathBuf::from("./"));
36            base_dirs.push(PathBuf::from("assets"));
37
38            AssetLoader::File { base_dirs }
39        }
40
41        #[cfg(not(any(feature = "csr", feature = "ssr")))]
42        {
43            // Default when neither feature is enabled
44            AssetLoader::File {
45                base_dirs: vec![PathBuf::from("./")],
46            }
47        }
48    }
49}
50
51impl AssetLoader {
52    /// Create a URL-based asset loader
53    #[cfg(feature = "csr")]
54    pub fn new_url(base_url: impl Into<String>) -> Self {
55        AssetLoader::Url {
56            base_url: base_url.into(),
57        }
58    }
59
60    /// Create a file-based asset loader
61    pub fn new_file(base_dirs: Vec<PathBuf>) -> Self {
62        AssetLoader::File { base_dirs }
63    }
64
65    /// Create a file-based asset loader from a single directory
66    pub fn from_dir(dir: impl Into<PathBuf>) -> Self {
67        AssetLoader::File {
68            base_dirs: vec![dir.into()],
69        }
70    }
71
72    /// Load a binary asset (like a zip file)
73    pub async fn load_binary(&self, path: &str) -> Result<Vec<u8>, String> {
74        match self {
75            #[cfg(feature = "csr")]
76            AssetLoader::Url { base_url } => {
77                // Normalize URL by ensuring base_url ends with / and path doesn't start with /
78                let normalized_path = path.trim_start_matches('/');
79                let base_url = if base_url.ends_with('/') {
80                    base_url.to_string()
81                } else {
82                    format!("{}/", base_url)
83                };
84
85                let url = format!("{}{}", base_url, normalized_path);
86
87                let response = Request::get(&url)
88                    .send()
89                    .await
90                    .map_err(|e| format!("Failed to send request to {}: {}", url, e))?;
91
92                if response.status() != 200 {
93                    return Err(format!(
94                        "Failed to load asset {}: status {}",
95                        url,
96                        response.status()
97                    ));
98                }
99
100                response
101                    .binary()
102                    .await
103                    .map_err(|e| format!("Failed to read response from {}: {}", url, e))
104            }
105
106            AssetLoader::File { base_dirs } => {
107                // Try each base directory
108                for base_dir in base_dirs {
109                    let file_path = base_dir.join(path);
110                    if file_path.exists() {
111                        return std::fs::read(&file_path).map_err(|e| {
112                            format!("Failed to read file {}: {}", file_path.display(), e)
113                        });
114                    }
115                }
116
117                // If path is absolute, try it directly
118                let path_buf = PathBuf::from(path);
119                if path_buf.is_absolute() && path_buf.exists() {
120                    return std::fs::read(&path_buf)
121                        .map_err(|e| format!("Failed to read file {}: {}", path_buf.display(), e));
122                }
123
124                Err(format!("File not found: {}", path))
125            }
126
127            #[allow(unreachable_patterns)]
128            _ => Err("Asset loading is not available in this configuration".to_string()),
129        }
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    #[cfg(feature = "csr")]
136    use crate::asset_loader::AssetLoader;
137
138    #[cfg(feature = "csr")]
139    use wasm_bindgen_test::*;
140
141    #[cfg(feature = "csr")]
142    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
143
144    #[cfg(all(test, feature = "ssr", not(feature = "csr")))]
145    const KONNEKTOREN_YAML_CONTENT: &str = include_str!("../assets/konnektoren.yml");
146
147    // Test for SSR environment
148    #[cfg(all(test, feature = "ssr", not(feature = "csr")))]
149    #[tokio::test]
150    async fn test_load_konnektoren_yaml_ssr() {
151        use std::io::Write;
152        use tempfile::TempDir;
153        // Create a temporary directory for testing
154        let temp_dir = TempDir::new().unwrap();
155
156        // Create the assets directory inside the temp dir
157        let assets_dir = temp_dir.path().join("assets");
158        std::fs::create_dir_all(&assets_dir).unwrap();
159
160        // Write the konnektoren.yml file to the assets directory
161        let yaml_path = assets_dir.join("konnektoren.yml");
162        let mut file = std::fs::File::create(&yaml_path).unwrap();
163        file.write_all(KONNEKTOREN_YAML_CONTENT.as_bytes()).unwrap();
164
165        // Set the BUILD_DIR environment variable to the temp directory
166        std::env::set_var("BUILD_DIR", temp_dir.path().to_str().unwrap());
167
168        // Create asset loader
169        let asset_loader = AssetLoader::default();
170
171        // Load the file
172        let content = asset_loader
173            .load_binary("assets/konnektoren.yml")
174            .await
175            .unwrap();
176
177        // Verify content
178        let content_str = String::from_utf8(content).unwrap();
179        assert!(content_str.contains("id: \"konnektoren\""));
180        assert!(content_str.contains("name: \"Konnektoren\""));
181        assert!(content_str.contains("lang: \"de\""));
182    }
183
184    // Test with explicit file loader
185    #[cfg(all(test, feature = "ssr", not(feature = "csr")))]
186    #[tokio::test]
187    async fn test_load_konnektoren_yaml_file_loader() {
188        use std::io::Write;
189        use tempfile::TempDir;
190
191        // Create a temporary directory for testing
192        let temp_dir = TempDir::new().unwrap();
193
194        // Create the assets directory inside the temp dir
195        let assets_dir = temp_dir.path().join("assets");
196        std::fs::create_dir_all(&assets_dir).unwrap();
197
198        // Write the konnektoren.yml file to the assets directory
199        let yaml_path = assets_dir.join("konnektoren.yml");
200        let mut file = std::fs::File::create(&yaml_path).unwrap();
201        file.write_all(KONNEKTOREN_YAML_CONTENT.as_bytes()).unwrap();
202
203        // Create asset loader with specific directory
204        let asset_loader = AssetLoader::from_dir(temp_dir.path());
205
206        // Load the file
207        let content = asset_loader
208            .load_binary("assets/konnektoren.yml")
209            .await
210            .unwrap();
211
212        // Verify content
213        let content_str = String::from_utf8(content).unwrap();
214        assert!(content_str.contains("id: \"konnektoren\""));
215        assert!(content_str.contains("name: \"Konnektoren\""));
216        assert!(content_str.contains("lang: \"de\""));
217    }
218
219    // Test for CSR environment - we need to mock fetch API in a browser environment
220    #[cfg(feature = "csr")]
221    #[wasm_bindgen_test]
222    async fn test_load_konnektoren_yaml_csr() {
223        // In a real browser environment with proper server setup,
224        // we would test against a real file, but for a unit test
225        // we'll just check that the code doesn't panic
226        let asset_loader = AssetLoader::default();
227
228        // This will typically fail in a test environment as there's no server,
229        // but we want to make sure the URL is constructed correctly
230        let result = asset_loader.load_binary("assets/konnektoren.yml").await;
231
232        // In an actual integration test, you'd set up a mock server and assert:
233        // assert!(result.is_ok());
234        // For unit test, we'll just ensure our code runs without panicking
235        match result {
236            Ok(_) => {
237                println!("Successfully loaded the file (integration environment)");
238            }
239            Err(e) => {
240                println!("Expected error in test environment: {}", e);
241                // Make sure it tried to load from the correct URL
242                assert!(e.contains("assets/konnektoren.yml"));
243            }
244        }
245    }
246
247    // Test for custom URL
248    #[cfg(feature = "csr")]
249    #[wasm_bindgen_test]
250    async fn test_custom_url_base_csr() {
251        let asset_loader = AssetLoader::new_url("https://example.com/static");
252
253        // Check that the URL is constructed correctly with the custom base
254        let result = asset_loader.load_binary("assets/konnektoren.yml").await;
255
256        match result {
257            Ok(_) => {
258                println!("Successfully loaded the file (integration environment)");
259            }
260            Err(e) => {
261                println!("Expected error in test environment: {}", e);
262                // Make sure it tried to load from the correct URL with custom base
263                assert!(e.contains("https://example.com/static/assets/konnektoren.yml"));
264            }
265        }
266    }
267}