Skip to main content

openscenario_rs/catalog/
resolver.rs

1//! Catalog reference resolution functionality
2//!
3//! This module handles:
4//! - Resolving catalog references to actual entities
5//! - Dependency tracking and circular reference detection
6//! - Parameter substitution during resolution
7//! - Integration of resolved content into scenarios
8
9use crate::catalog::parameters::ParameterSubstitutionEngine;
10use crate::error::{Error, Result};
11use crate::types::catalogs::controllers::{CatalogController, ControllerCatalog};
12use crate::types::catalogs::environments::{CatalogEnvironment, EnvironmentCatalog};
13use crate::types::catalogs::routes::{CatalogRoute, RouteCatalog};
14use crate::types::catalogs::trajectories::{CatalogTrajectory, TrajectoryCatalog};
15use std::collections::{HashMap, HashSet};
16
17/// Represents a resolved catalog entity
18pub struct ResolvedCatalog<T> {
19    /// The resolved entity content
20    pub entity: T,
21    /// Additional metadata about the resolution
22    pub metadata: ResolutionMetadata,
23}
24
25/// Metadata about how a catalog reference was resolved
26#[derive(Debug, Clone)]
27pub struct ResolutionMetadata {
28    /// Path to the catalog file where the entity was found
29    pub catalog_path: String,
30    /// Name of the entity in the catalog
31    pub entity_name: String,
32    /// Parameters that were substituted during resolution
33    pub parameter_substitutions: HashMap<String, String>,
34}
35
36/// Trait for catalog resolution
37pub trait CatalogResolvable<T> {
38    /// Resolve a catalog reference to the actual entity
39    fn resolve(&self, entry_name: &str, catalogs: &CatalogManager) -> Result<ResolvedCatalog<T>>;
40}
41
42/// Central catalog manager for all catalog types
43pub struct CatalogManager {
44    /// Controller catalogs indexed by catalog name
45    pub controller_catalogs: HashMap<String, ControllerCatalog>,
46    /// Trajectory catalogs indexed by catalog name
47    pub trajectory_catalogs: HashMap<String, TrajectoryCatalog>,
48    /// Route catalogs indexed by catalog name
49    pub route_catalogs: HashMap<String, RouteCatalog>,
50    /// Environment catalogs indexed by catalog name
51    pub environment_catalogs: HashMap<String, EnvironmentCatalog>,
52    /// Parameter resolver for handling parameter substitution
53    pub parameter_resolver: ParameterSubstitutionEngine,
54}
55
56/// Catalog reference resolver
57pub struct CatalogResolver {
58    /// Track resolved references to detect circular dependencies
59    resolution_stack: HashSet<String>,
60    /// Catalog manager containing all loaded catalogs
61    catalog_manager: Option<CatalogManager>,
62}
63
64impl Default for CatalogManager {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70impl CatalogManager {
71    /// Create a new empty catalog manager
72    pub fn new() -> Self {
73        Self {
74            controller_catalogs: HashMap::new(),
75            trajectory_catalogs: HashMap::new(),
76            route_catalogs: HashMap::new(),
77            environment_catalogs: HashMap::new(),
78            parameter_resolver: ParameterSubstitutionEngine::new(),
79        }
80    }
81
82    /// Add a controller catalog
83    pub fn add_controller_catalog(&mut self, name: String, catalog: ControllerCatalog) {
84        self.controller_catalogs.insert(name, catalog);
85    }
86
87    /// Add a trajectory catalog
88    pub fn add_trajectory_catalog(&mut self, name: String, catalog: TrajectoryCatalog) {
89        self.trajectory_catalogs.insert(name, catalog);
90    }
91
92    /// Add a route catalog
93    pub fn add_route_catalog(&mut self, name: String, catalog: RouteCatalog) {
94        self.route_catalogs.insert(name, catalog);
95    }
96
97    /// Add an environment catalog
98    pub fn add_environment_catalog(&mut self, name: String, catalog: EnvironmentCatalog) {
99        self.environment_catalogs.insert(name, catalog);
100    }
101
102    /// Set global parameter values that apply to all catalog resolutions
103    pub fn set_parameters(&mut self, parameters: HashMap<String, String>) -> Result<()> {
104        self.parameter_resolver.set_parameters(parameters)
105    }
106
107    /// Set a single global parameter value
108    pub fn set_parameter(&mut self, name: String, value: String) -> Result<()> {
109        self.parameter_resolver.set_parameter(name, value)
110    }
111
112    /// Get the current parameter context
113    pub fn parameter_context(&self) -> &HashMap<String, String> {
114        self.parameter_resolver.context()
115    }
116
117    /// Resolve controller reference from controller catalogs
118    pub fn resolve_controller_reference(
119        &self,
120        catalog_name: &str,
121        entry_name: &str,
122    ) -> Result<ResolvedCatalog<CatalogController>> {
123        self.resolve_controller_reference_with_params(catalog_name, entry_name, &HashMap::new())
124    }
125
126    /// Resolve controller reference with parameter overrides
127    pub fn resolve_controller_reference_with_params(
128        &self,
129        catalog_name: &str,
130        entry_name: &str,
131        params: &HashMap<String, String>,
132    ) -> Result<ResolvedCatalog<CatalogController>> {
133        if let Some(catalog) = self.controller_catalogs.get(catalog_name) {
134            for controller in &catalog.controllers {
135                if controller.name == entry_name {
136                    let resolved_controller =
137                        if !params.is_empty() || !self.parameter_resolver.context().is_empty() {
138                            let _param_engine = self
139                                .parameter_resolver
140                                .with_additional_context(params.clone());
141                            controller.clone()
142                        } else {
143                            controller.clone()
144                        };
145
146                    return Ok(ResolvedCatalog::with_parameters(
147                        resolved_controller,
148                        format!("controller_catalog:{}", catalog_name),
149                        entry_name.to_string(),
150                        params.clone(),
151                    ));
152                }
153            }
154            let available: Vec<String> =
155                catalog.controllers.iter().map(|c| c.name.clone()).collect();
156            return Err(Error::catalog_entry_not_found(catalog_name, entry_name)
157                .with_context(&format!("Available controllers: {}", available.join(", "))));
158        }
159        let available: Vec<String> = self.controller_catalogs.keys().cloned().collect();
160        Err(Error::catalog_not_found(catalog_name, &available))
161    }
162
163    /// Resolve trajectory reference from trajectory catalogs
164    pub fn resolve_trajectory_reference(
165        &self,
166        catalog_name: &str,
167        entry_name: &str,
168    ) -> Result<ResolvedCatalog<CatalogTrajectory>> {
169        self.resolve_trajectory_reference_with_params(catalog_name, entry_name, &HashMap::new())
170    }
171
172    /// Resolve trajectory reference with parameter overrides
173    pub fn resolve_trajectory_reference_with_params(
174        &self,
175        catalog_name: &str,
176        entry_name: &str,
177        params: &HashMap<String, String>,
178    ) -> Result<ResolvedCatalog<CatalogTrajectory>> {
179        if let Some(catalog) = self.trajectory_catalogs.get(catalog_name) {
180            for trajectory in &catalog.trajectories {
181                if trajectory.name == entry_name {
182                    return Ok(ResolvedCatalog::with_parameters(
183                        trajectory.clone(),
184                        format!("trajectory_catalog:{}", catalog_name),
185                        entry_name.to_string(),
186                        params.clone(),
187                    ));
188                }
189            }
190            let available: Vec<String> = catalog
191                .trajectories
192                .iter()
193                .map(|t| t.name.clone())
194                .collect();
195            return Err(Error::catalog_entry_not_found(catalog_name, entry_name)
196                .with_context(&format!("Available trajectories: {}", available.join(", "))));
197        }
198        let available: Vec<String> = self.trajectory_catalogs.keys().cloned().collect();
199        Err(Error::catalog_not_found(catalog_name, &available))
200    }
201
202    /// Resolve route reference from route catalogs
203    pub fn resolve_route_reference(
204        &self,
205        catalog_name: &str,
206        entry_name: &str,
207    ) -> Result<ResolvedCatalog<CatalogRoute>> {
208        self.resolve_route_reference_with_params(catalog_name, entry_name, &HashMap::new())
209    }
210
211    /// Resolve route reference with parameter overrides
212    pub fn resolve_route_reference_with_params(
213        &self,
214        catalog_name: &str,
215        entry_name: &str,
216        params: &HashMap<String, String>,
217    ) -> Result<ResolvedCatalog<CatalogRoute>> {
218        if let Some(catalog) = self.route_catalogs.get(catalog_name) {
219            for route in &catalog.routes {
220                if route.name == entry_name {
221                    return Ok(ResolvedCatalog::with_parameters(
222                        route.clone(),
223                        format!("route_catalog:{}", catalog_name),
224                        entry_name.to_string(),
225                        params.clone(),
226                    ));
227                }
228            }
229            let available: Vec<String> = catalog.routes.iter().map(|r| r.name.clone()).collect();
230            return Err(Error::catalog_entry_not_found(catalog_name, entry_name)
231                .with_context(&format!("Available routes: {}", available.join(", "))));
232        }
233        let available: Vec<String> = self.route_catalogs.keys().cloned().collect();
234        Err(Error::catalog_not_found(catalog_name, &available))
235    }
236
237    /// Resolve environment reference from environment catalogs
238    pub fn resolve_environment_reference(
239        &self,
240        catalog_name: &str,
241        entry_name: &str,
242    ) -> Result<ResolvedCatalog<CatalogEnvironment>> {
243        self.resolve_environment_reference_with_params(catalog_name, entry_name, &HashMap::new())
244    }
245
246    /// Resolve environment reference with parameter overrides
247    pub fn resolve_environment_reference_with_params(
248        &self,
249        catalog_name: &str,
250        entry_name: &str,
251        params: &HashMap<String, String>,
252    ) -> Result<ResolvedCatalog<CatalogEnvironment>> {
253        if let Some(catalog) = self.environment_catalogs.get(catalog_name) {
254            for environment in &catalog.environments {
255                if environment.name == entry_name {
256                    return Ok(ResolvedCatalog::with_parameters(
257                        environment.clone(),
258                        format!("environment_catalog:{}", catalog_name),
259                        entry_name.to_string(),
260                        params.clone(),
261                    ));
262                }
263            }
264            let available: Vec<String> = catalog
265                .environments
266                .iter()
267                .map(|e| e.name.clone())
268                .collect();
269            return Err(Error::catalog_entry_not_found(catalog_name, entry_name)
270                .with_context(&format!("Available environments: {}", available.join(", "))));
271        }
272        let available: Vec<String> = self.environment_catalogs.keys().cloned().collect();
273        Err(Error::catalog_not_found(catalog_name, &available))
274    }
275}
276
277impl CatalogResolver {
278    /// Create a new catalog resolver
279    pub fn new() -> Self {
280        Self {
281            resolution_stack: HashSet::new(),
282            catalog_manager: None,
283        }
284    }
285
286    /// Create a new catalog resolver with a catalog manager
287    pub fn with_catalog_manager(catalog_manager: CatalogManager) -> Self {
288        Self {
289            resolution_stack: HashSet::new(),
290            catalog_manager: Some(catalog_manager),
291        }
292    }
293
294    /// Set the catalog manager
295    pub fn set_catalog_manager(&mut self, catalog_manager: CatalogManager) {
296        self.catalog_manager = Some(catalog_manager);
297    }
298
299    /// Get a reference to the catalog manager
300    pub fn catalog_manager(&self) -> Option<&CatalogManager> {
301        self.catalog_manager.as_ref()
302    }
303
304    /// Begin resolving a reference (for circular dependency detection)
305    pub fn begin_resolution(&mut self, reference_key: &str) -> Result<()> {
306        if self.resolution_stack.contains(reference_key) {
307            return Err(Error::circular_dependency(reference_key));
308        }
309        self.resolution_stack.insert(reference_key.to_string());
310        Ok(())
311    }
312
313    /// End resolving a reference
314    pub fn end_resolution(&mut self, reference_key: &str) {
315        self.resolution_stack.remove(reference_key);
316    }
317
318    /// Check if we're currently resolving a reference
319    pub fn is_resolving(&self, reference_key: &str) -> bool {
320        self.resolution_stack.contains(reference_key)
321    }
322
323    /// Clear the resolution stack
324    pub fn clear(&mut self) {
325        self.resolution_stack.clear();
326    }
327}
328
329impl Default for CatalogResolver {
330    fn default() -> Self {
331        Self::new()
332    }
333}
334
335impl<T> ResolvedCatalog<T> {
336    /// Create a new resolved catalog entry
337    pub fn new(entity: T, catalog_path: String, entity_name: String) -> Self {
338        Self {
339            entity,
340            metadata: ResolutionMetadata {
341                catalog_path,
342                entity_name,
343                parameter_substitutions: HashMap::new(),
344            },
345        }
346    }
347
348    /// Create a resolved catalog entry with parameter substitutions
349    pub fn with_parameters(
350        entity: T,
351        catalog_path: String,
352        entity_name: String,
353        parameters: HashMap<String, String>,
354    ) -> Self {
355        Self {
356            entity,
357            metadata: ResolutionMetadata {
358                catalog_path,
359                entity_name,
360                parameter_substitutions: parameters,
361            },
362        }
363    }
364
365    /// Get the resolved entity
366    pub fn into_entity(self) -> T {
367        self.entity
368    }
369
370    /// Get a reference to the resolved entity
371    pub fn entity(&self) -> &T {
372        &self.entity
373    }
374
375    /// Get the resolution metadata
376    pub fn metadata(&self) -> &ResolutionMetadata {
377        &self.metadata
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn test_resolver_creation() {
387        let resolver = CatalogResolver::new();
388        assert!(!resolver.is_resolving("test"));
389    }
390
391    #[test]
392    fn test_circular_dependency_detection() {
393        let mut resolver = CatalogResolver::new();
394
395        // Start resolving a reference
396        assert!(resolver.begin_resolution("vehicle1").is_ok());
397        assert!(resolver.is_resolving("vehicle1"));
398
399        // Try to resolve the same reference again - should detect circular dependency
400        assert!(resolver.begin_resolution("vehicle1").is_err());
401
402        // End the resolution
403        resolver.end_resolution("vehicle1");
404        assert!(!resolver.is_resolving("vehicle1"));
405
406        // Now we can start resolving it again
407        assert!(resolver.begin_resolution("vehicle1").is_ok());
408        resolver.end_resolution("vehicle1");
409    }
410
411    #[test]
412    fn test_resolved_catalog() {
413        let entity = "test_vehicle".to_string();
414        let resolved = ResolvedCatalog::new(
415            entity.clone(),
416            "/path/to/catalog.xosc".to_string(),
417            "TestVehicle".to_string(),
418        );
419
420        assert_eq!(resolved.entity(), &entity);
421        assert_eq!(resolved.metadata().catalog_path, "/path/to/catalog.xosc");
422        assert_eq!(resolved.metadata().entity_name, "TestVehicle");
423        assert!(resolved.metadata().parameter_substitutions.is_empty());
424    }
425
426    #[test]
427    fn test_resolved_catalog_with_parameters() {
428        let mut params = HashMap::new();
429        params.insert("MaxSpeed".to_string(), "60.0".to_string());
430        params.insert("Color".to_string(), "Red".to_string());
431
432        let resolved = ResolvedCatalog::with_parameters(
433            42u32,
434            "/catalogs/vehicles.xosc".to_string(),
435            "SportsCar".to_string(),
436            params.clone(),
437        );
438
439        assert_eq!(*resolved.entity(), 42u32);
440        assert_eq!(resolved.metadata().parameter_substitutions, params);
441    }
442}