this/links/
registry.rs

1//! Route registry for link navigation
2//!
3//! Provides resolution of route names to link definitions and handles
4//! bidirectional navigation (forward and reverse)
5
6use crate::config::LinksConfig;
7use crate::core::LinkDefinition;
8use anyhow::{anyhow, Result};
9use std::collections::HashMap;
10use std::sync::Arc;
11
12/// Direction of link navigation
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum LinkDirection {
15    /// From source to target
16    Forward,
17    /// From target to source
18    Reverse,
19}
20
21/// Registry for resolving route names to link definitions
22///
23/// This allows the framework to map URL paths like "/users/{id}/cars-owned"
24/// to the appropriate link definition and direction.
25pub struct LinkRouteRegistry {
26    config: Arc<LinksConfig>,
27    /// Maps (entity_type, route_name) -> (LinkDefinition, LinkDirection)
28    routes: HashMap<(String, String), (LinkDefinition, LinkDirection)>,
29}
30
31impl LinkRouteRegistry {
32    /// Create a new registry from a links configuration
33    pub fn new(config: Arc<LinksConfig>) -> Self {
34        let mut routes = HashMap::new();
35
36        // Build the routing table
37        for link_def in &config.links {
38            // Forward route: source -> target
39            let forward_key = (
40                link_def.source_type.clone(),
41                link_def.forward_route_name.clone(),
42            );
43            routes.insert(forward_key, (link_def.clone(), LinkDirection::Forward));
44
45            // Reverse route: target -> source
46            let reverse_key = (
47                link_def.target_type.clone(),
48                link_def.reverse_route_name.clone(),
49            );
50            routes.insert(reverse_key, (link_def.clone(), LinkDirection::Reverse));
51        }
52
53        Self { config, routes }
54    }
55
56    /// Resolve a route name for a given entity type
57    ///
58    /// Returns the link definition and the direction of navigation
59    pub fn resolve_route(
60        &self,
61        entity_type: &str,
62        route_name: &str,
63    ) -> Result<(LinkDefinition, LinkDirection)> {
64        let key = (entity_type.to_string(), route_name.to_string());
65
66        self.routes.get(&key).cloned().ok_or_else(|| {
67            anyhow!(
68                "No route '{}' found for entity type '{}'",
69                route_name,
70                entity_type
71            )
72        })
73    }
74
75    /// List all available routes for a given entity type
76    pub fn list_routes_for_entity(&self, entity_type: &str) -> Vec<RouteInfo> {
77        self.routes
78            .iter()
79            .filter(|((etype, _), _)| etype == entity_type)
80            .map(|((_, route_name), (link_def, direction))| {
81                let connected_to = match direction {
82                    LinkDirection::Forward => &link_def.target_type,
83                    LinkDirection::Reverse => &link_def.source_type,
84                };
85
86                RouteInfo {
87                    route_name: route_name.clone(),
88                    link_type: link_def.link_type.clone(),
89                    direction: *direction,
90                    connected_to: connected_to.clone(),
91                    description: link_def.description.clone(),
92                }
93            })
94            .collect()
95    }
96
97    /// Get the underlying configuration
98    pub fn config(&self) -> &LinksConfig {
99        &self.config
100    }
101}
102
103/// Information about a route available for an entity
104#[derive(Debug, Clone)]
105pub struct RouteInfo {
106    /// The route name (e.g., "cars-owned")
107    pub route_name: String,
108
109    /// The type of link (e.g., "owner")
110    pub link_type: String,
111
112    /// Direction of the relationship
113    pub direction: LinkDirection,
114
115    /// The entity type this route connects to
116    pub connected_to: String,
117
118    /// Optional description
119    pub description: Option<String>,
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::config::EntityConfig;
126
127    fn create_test_config() -> LinksConfig {
128        LinksConfig {
129            entities: vec![
130                EntityConfig {
131                    singular: "user".to_string(),
132                    plural: "users".to_string(),
133                    auth: crate::config::EntityAuthConfig::default(),
134                },
135                EntityConfig {
136                    singular: "car".to_string(),
137                    plural: "cars".to_string(),
138                    auth: crate::config::EntityAuthConfig::default(),
139                },
140            ],
141            links: vec![
142                LinkDefinition {
143                    link_type: "owner".to_string(),
144                    source_type: "user".to_string(),
145                    target_type: "car".to_string(),
146                    forward_route_name: "cars-owned".to_string(),
147                    reverse_route_name: "users-owners".to_string(),
148                    description: Some("User owns a car".to_string()),
149                    required_fields: None,
150                    auth: None,
151                },
152                LinkDefinition {
153                    link_type: "driver".to_string(),
154                    source_type: "user".to_string(),
155                    target_type: "car".to_string(),
156                    forward_route_name: "cars-driven".to_string(),
157                    reverse_route_name: "users-drivers".to_string(),
158                    description: Some("User drives a car".to_string()),
159                    required_fields: None,
160                    auth: None,
161                },
162            ],
163            validation_rules: None,
164        }
165    }
166
167    #[test]
168    fn test_resolve_forward_route() {
169        let config = Arc::new(create_test_config());
170        let registry = LinkRouteRegistry::new(config);
171
172        let (def, direction) = registry.resolve_route("user", "cars-owned").unwrap();
173
174        assert_eq!(def.link_type, "owner");
175        assert_eq!(def.source_type, "user");
176        assert_eq!(def.target_type, "car");
177        assert_eq!(direction, LinkDirection::Forward);
178    }
179
180    #[test]
181    fn test_resolve_reverse_route() {
182        let config = Arc::new(create_test_config());
183        let registry = LinkRouteRegistry::new(config);
184
185        let (def, direction) = registry.resolve_route("car", "users-owners").unwrap();
186
187        assert_eq!(def.link_type, "owner");
188        assert_eq!(def.source_type, "user");
189        assert_eq!(def.target_type, "car");
190        assert_eq!(direction, LinkDirection::Reverse);
191    }
192
193    #[test]
194    fn test_list_routes_for_entity() {
195        let config = Arc::new(create_test_config());
196        let registry = LinkRouteRegistry::new(config);
197
198        let routes = registry.list_routes_for_entity("user");
199
200        assert_eq!(routes.len(), 2);
201
202        let route_names: Vec<_> = routes.iter().map(|r| r.route_name.as_str()).collect();
203        assert!(route_names.contains(&"cars-owned"));
204        assert!(route_names.contains(&"cars-driven"));
205    }
206
207    #[test]
208    fn test_no_route_conflicts() {
209        let config = Arc::new(create_test_config());
210        let registry = LinkRouteRegistry::new(config);
211
212        let user_routes = registry.list_routes_for_entity("user");
213        let route_names: Vec<_> = user_routes.iter().map(|r| &r.route_name).collect();
214
215        let unique_names: std::collections::HashSet<_> = route_names.iter().collect();
216        assert_eq!(
217            route_names.len(),
218            unique_names.len(),
219            "Route names must be unique"
220        );
221    }
222}