Skip to main content

openscenario_rs/catalog/
loader.rs

1//! Catalog loading and caching functionality
2//!
3//! This module handles:
4//! - Loading catalog files from directory paths
5//! - Parsing XML catalog content
6//! - Caching loaded catalogs for performance
7//! - File system operations for catalog discovery
8
9use crate::error::{Error, Result};
10use crate::parser::xml::{parse_catalog_from_file, parse_catalog_from_str};
11use crate::types::basic::Directory;
12use crate::types::catalogs::entities::{CatalogController, CatalogPedestrian, CatalogVehicle};
13use crate::types::catalogs::files::CatalogFile;
14use std::fs;
15use std::path::{Path, PathBuf};
16
17/// Catalog file loader that handles file system operations
18pub struct CatalogLoader {
19    /// Base path for resolving relative catalog paths
20    base_path: Option<PathBuf>,
21}
22
23impl CatalogLoader {
24    /// Create a new catalog loader
25    pub fn new() -> Self {
26        Self { base_path: None }
27    }
28
29    /// Create a catalog loader with a specific base path
30    pub fn with_base_path<P: AsRef<Path>>(base_path: P) -> Self {
31        Self {
32            base_path: Some(base_path.as_ref().to_path_buf()),
33        }
34    }
35
36    /// Set the base path for resolving relative paths
37    pub fn set_base_path<P: AsRef<Path>>(&mut self, base_path: P) {
38        self.base_path = Some(base_path.as_ref().to_path_buf());
39    }
40
41    /// Discover all .xosc files in a directory
42    pub fn discover_catalog_files(&self, directory: &Directory) -> Result<Vec<PathBuf>> {
43        let path_str = directory.path.as_literal().ok_or_else(|| {
44            Error::invalid_value(
45                "directory.path",
46                "parameterized path",
47                "literal path required",
48            )
49        })?;
50
51        let dir_path = self.resolve_path(path_str)?;
52
53        if !dir_path.exists() {
54            return Err(Error::directory_not_found(&dir_path.to_string_lossy()));
55        }
56
57        if !dir_path.is_dir() {
58            return Err(Error::invalid_value(
59                "directory.path",
60                &dir_path.to_string_lossy(),
61                "path must be a directory",
62            ));
63        }
64
65        let mut catalog_files = Vec::new();
66
67        for entry in fs::read_dir(&dir_path)
68            .map_err(|e| Error::file_read_error(&dir_path.to_string_lossy(), &e.to_string()))?
69        {
70            let entry = entry
71                .map_err(|e| Error::file_read_error(&dir_path.to_string_lossy(), &e.to_string()))?;
72
73            let path = entry.path();
74
75            if path.is_file() {
76                if let Some(extension) = path.extension() {
77                    if extension == "xosc" {
78                        catalog_files.push(path);
79                    }
80                }
81            }
82        }
83
84        catalog_files.sort();
85        Ok(catalog_files)
86    }
87
88    /// Load and parse a catalog file
89    pub fn load_catalog_file<P: AsRef<Path>>(&self, file_path: P) -> Result<String> {
90        let path = file_path.as_ref();
91
92        if !path.exists() {
93            return Err(Error::file_not_found(&path.to_string_lossy()));
94        }
95
96        fs::read_to_string(path)
97            .map_err(|e| Error::file_read_error(&path.to_string_lossy(), &e.to_string()))
98    }
99
100    /// Load and parse a catalog file into a CatalogFile structure
101    pub fn load_and_parse_catalog_file<P: AsRef<Path>>(&self, file_path: P) -> Result<CatalogFile> {
102        let path = file_path.as_ref();
103
104        if !path.exists() {
105            return Err(Error::catalog_error(&format!(
106                "Catalog file does not exist: {}",
107                path.display()
108            )));
109        }
110
111        parse_catalog_from_file(path).map_err(|e| {
112            e.with_context(&format!("Failed to parse catalog file: {}", path.display()))
113        })
114    }
115
116    /// Load and parse a catalog from XML string
117    pub fn parse_catalog_from_string(&self, xml: &str) -> Result<CatalogFile> {
118        parse_catalog_from_str(xml)
119            .map_err(|e| e.with_context("Failed to parse catalog from string"))
120    }
121
122    /// Load all vehicle catalogs from a directory
123    pub fn load_vehicle_catalogs(&self, directory: &Directory) -> Result<Vec<CatalogVehicle>> {
124        let catalog_files = self.discover_catalog_files(directory)?;
125        let mut vehicles = Vec::new();
126
127        for file_path in catalog_files {
128            let catalog = self.load_and_parse_catalog_file(&file_path)?;
129            vehicles.extend(catalog.vehicles().iter().cloned());
130        }
131
132        Ok(vehicles)
133    }
134
135    /// Load all controller catalogs from a directory
136    pub fn load_controller_catalogs(
137        &self,
138        directory: &Directory,
139    ) -> Result<Vec<CatalogController>> {
140        let catalog_files = self.discover_catalog_files(directory)?;
141        let mut controllers = Vec::new();
142
143        for file_path in catalog_files {
144            let catalog = self.load_and_parse_catalog_file(&file_path)?;
145            controllers.extend(catalog.controllers().iter().cloned());
146        }
147
148        Ok(controllers)
149    }
150
151    /// Load all pedestrian catalogs from a directory
152    pub fn load_pedestrian_catalogs(
153        &self,
154        directory: &Directory,
155    ) -> Result<Vec<CatalogPedestrian>> {
156        let catalog_files = self.discover_catalog_files(directory)?;
157        let mut pedestrians = Vec::new();
158
159        for file_path in catalog_files {
160            let catalog = self.load_and_parse_catalog_file(&file_path)?;
161            pedestrians.extend(catalog.pedestrians().iter().cloned());
162        }
163
164        Ok(pedestrians)
165    }
166
167    /// Find a specific entity in a catalog file
168    pub fn find_entity_in_catalog<P: AsRef<Path>>(
169        &self,
170        file_path: P,
171        entity_name: &str,
172    ) -> Result<Option<String>> {
173        let catalog = self.load_and_parse_catalog_file(file_path)?;
174
175        // Check if any entity with the given name exists
176        let entity_names = catalog.catalog.entity_names();
177        if entity_names.contains(&entity_name.to_string()) {
178            Ok(Some(entity_name.to_string()))
179        } else {
180            Ok(None)
181        }
182    }
183
184    /// Load a specific controller catalog from a file
185    pub fn load_controller_catalog<P: AsRef<Path>>(
186        &self,
187        file_path: P,
188    ) -> Result<crate::types::catalogs::controllers::ControllerCatalog> {
189        let path = file_path.as_ref();
190        if !path.exists() {
191            return Err(Error::file_not_found(&path.to_string_lossy()));
192        }
193
194        let xml_content = fs::read_to_string(path)
195            .map_err(|e| Error::file_read_error(&path.to_string_lossy(), &e.to_string()))?;
196
197        quick_xml::de::from_str(&xml_content)
198            .map_err(|e| Error::parse_error(&path.to_string_lossy(), &e.to_string()))
199    }
200
201    /// Load a specific trajectory catalog from a file
202    pub fn load_trajectory_catalog<P: AsRef<Path>>(
203        &self,
204        file_path: P,
205    ) -> Result<crate::types::catalogs::trajectories::TrajectoryCatalog> {
206        let path = file_path.as_ref();
207        if !path.exists() {
208            return Err(Error::file_not_found(&path.to_string_lossy()));
209        }
210
211        let xml_content = fs::read_to_string(path)
212            .map_err(|e| Error::file_read_error(&path.to_string_lossy(), &e.to_string()))?;
213
214        quick_xml::de::from_str(&xml_content)
215            .map_err(|e| Error::parse_error(&path.to_string_lossy(), &e.to_string()))
216    }
217
218    /// Load a specific route catalog from a file
219    pub fn load_route_catalog<P: AsRef<Path>>(
220        &self,
221        file_path: P,
222    ) -> Result<crate::types::catalogs::routes::RouteCatalog> {
223        let path = file_path.as_ref();
224        if !path.exists() {
225            return Err(Error::file_not_found(&path.to_string_lossy()));
226        }
227
228        let xml_content = fs::read_to_string(path)
229            .map_err(|e| Error::file_read_error(&path.to_string_lossy(), &e.to_string()))?;
230
231        quick_xml::de::from_str(&xml_content)
232            .map_err(|e| Error::parse_error(&path.to_string_lossy(), &e.to_string()))
233    }
234
235    /// Load a specific environment catalog from a file
236    pub fn load_environment_catalog<P: AsRef<Path>>(
237        &self,
238        file_path: P,
239    ) -> Result<crate::types::catalogs::environments::EnvironmentCatalog> {
240        let path = file_path.as_ref();
241        if !path.exists() {
242            return Err(Error::file_not_found(&path.to_string_lossy()));
243        }
244
245        let xml_content = fs::read_to_string(path)
246            .map_err(|e| Error::file_read_error(&path.to_string_lossy(), &e.to_string()))?;
247
248        quick_xml::de::from_str(&xml_content)
249            .map_err(|e| Error::parse_error(&path.to_string_lossy(), &e.to_string()))
250    }
251
252    /// Load all controller catalogs from a directory and return them as a hashmap
253    pub fn load_controller_catalogs_from_directory(
254        &self,
255        directory: &Directory,
256    ) -> Result<
257        std::collections::HashMap<String, crate::types::catalogs::controllers::ControllerCatalog>,
258    > {
259        let catalog_files = self.discover_catalog_files(directory)?;
260        let mut catalogs = std::collections::HashMap::new();
261
262        for file_path in catalog_files {
263            if let Ok(catalog) = self.load_controller_catalog(&file_path) {
264                let catalog_name = file_path
265                    .file_stem()
266                    .and_then(|s| s.to_str())
267                    .unwrap_or("unknown")
268                    .to_string();
269                catalogs.insert(catalog_name, catalog);
270            }
271        }
272
273        Ok(catalogs)
274    }
275
276    /// Load all trajectory catalogs from a directory and return them as a hashmap
277    pub fn load_trajectory_catalogs_from_directory(
278        &self,
279        directory: &Directory,
280    ) -> Result<
281        std::collections::HashMap<String, crate::types::catalogs::trajectories::TrajectoryCatalog>,
282    > {
283        let catalog_files = self.discover_catalog_files(directory)?;
284        let mut catalogs = std::collections::HashMap::new();
285
286        for file_path in catalog_files {
287            if let Ok(catalog) = self.load_trajectory_catalog(&file_path) {
288                let catalog_name = file_path
289                    .file_stem()
290                    .and_then(|s| s.to_str())
291                    .unwrap_or("unknown")
292                    .to_string();
293                catalogs.insert(catalog_name, catalog);
294            }
295        }
296
297        Ok(catalogs)
298    }
299
300    /// Load all route catalogs from a directory and return them as a hashmap
301    pub fn load_route_catalogs_from_directory(
302        &self,
303        directory: &Directory,
304    ) -> Result<std::collections::HashMap<String, crate::types::catalogs::routes::RouteCatalog>>
305    {
306        let catalog_files = self.discover_catalog_files(directory)?;
307        let mut catalogs = std::collections::HashMap::new();
308
309        for file_path in catalog_files {
310            if let Ok(catalog) = self.load_route_catalog(&file_path) {
311                let catalog_name = file_path
312                    .file_stem()
313                    .and_then(|s| s.to_str())
314                    .unwrap_or("unknown")
315                    .to_string();
316                catalogs.insert(catalog_name, catalog);
317            }
318        }
319
320        Ok(catalogs)
321    }
322
323    /// Load all environment catalogs from a directory and return them as a hashmap
324    pub fn load_environment_catalogs_from_directory(
325        &self,
326        directory: &Directory,
327    ) -> Result<
328        std::collections::HashMap<String, crate::types::catalogs::environments::EnvironmentCatalog>,
329    > {
330        let catalog_files = self.discover_catalog_files(directory)?;
331        let mut catalogs = std::collections::HashMap::new();
332
333        for file_path in catalog_files {
334            if let Ok(catalog) = self.load_environment_catalog(&file_path) {
335                let catalog_name = file_path
336                    .file_stem()
337                    .and_then(|s| s.to_str())
338                    .unwrap_or("unknown")
339                    .to_string();
340                catalogs.insert(catalog_name, catalog);
341            }
342        }
343
344        Ok(catalogs)
345    }
346
347    /// Resolve a path relative to the base path if needed
348    fn resolve_path(&self, path: &str) -> Result<PathBuf> {
349        let path = Path::new(path);
350
351        if path.is_absolute() {
352            Ok(path.to_path_buf())
353        } else if let Some(base) = &self.base_path {
354            Ok(base.join(path))
355        } else {
356            // Use current directory as base
357            Ok(std::env::current_dir()?.join(path))
358        }
359    }
360}
361
362impl Default for CatalogLoader {
363    fn default() -> Self {
364        Self::new()
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    use std::fs;
373    use tempfile::TempDir;
374
375    #[test]
376    fn test_catalog_loader_creation() {
377        let loader = CatalogLoader::new();
378        assert!(loader.base_path.is_none());
379
380        let loader_with_path = CatalogLoader::with_base_path("/tmp");
381        assert_eq!(
382            loader_with_path.base_path.as_ref().unwrap(),
383            Path::new("/tmp")
384        );
385    }
386
387    #[test]
388    fn test_discover_catalog_files() -> Result<()> {
389        // Create a temporary directory with some test files
390        let temp_dir = TempDir::new().unwrap();
391        let dir_path = temp_dir.path();
392
393        // Create test files
394        fs::write(dir_path.join("catalog1.xosc"), "catalog1 content")?;
395        fs::write(dir_path.join("catalog2.xosc"), "catalog2 content")?;
396        fs::write(dir_path.join("not_catalog.txt"), "not a catalog")?;
397
398        let directory = Directory::new(dir_path.to_string_lossy().to_string());
399        let loader = CatalogLoader::new();
400
401        let files = loader.discover_catalog_files(&directory)?;
402        assert_eq!(files.len(), 2);
403
404        // Files should be sorted
405        assert!(files[0]
406            .file_name()
407            .unwrap()
408            .to_string_lossy()
409            .starts_with("catalog"));
410        assert!(files[1]
411            .file_name()
412            .unwrap()
413            .to_string_lossy()
414            .starts_with("catalog"));
415
416        Ok(())
417    }
418
419    #[test]
420    fn test_load_catalog_file() -> Result<()> {
421        let temp_dir = TempDir::new().unwrap();
422        let file_path = temp_dir.path().join("test_catalog.xosc");
423        let content = "<?xml version=\"1.0\"?>\n<OpenSCENARIO>test</OpenSCENARIO>";
424
425        fs::write(&file_path, &content).unwrap();
426
427        let loader = CatalogLoader::new();
428        let loaded_content = loader.load_catalog_file(&file_path)?;
429        assert_eq!(loaded_content, content);
430
431        Ok(())
432    }
433
434    #[test]
435    fn test_parse_catalog_from_string() {
436        let loader = CatalogLoader::new();
437
438        // Test with minimal valid catalog XML
439        let catalog_xml = r#"<?xml version="1.0"?>
440        <OpenSCENARIO>
441            <FileHeader author="Test" date="2024-01-01T00:00:00" description="Test" revMajor="1" revMinor="3"/>
442            <Catalog name="TestCatalog">
443            </Catalog>
444        </OpenSCENARIO>"#;
445
446        let catalog = loader.parse_catalog_from_string(catalog_xml).unwrap();
447        assert_eq!(catalog.catalog_name().as_literal().unwrap(), "TestCatalog");
448        assert_eq!(catalog.file_header.author.as_literal().unwrap(), "Test");
449        assert_eq!(catalog.catalog.entity_count(), 0);
450    }
451}