1use crate::config::LinksConfig;
7use crate::core::LinkDefinition;
8use anyhow::{Result, anyhow};
9use std::collections::HashMap;
10use std::sync::Arc;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum LinkDirection {
15 Forward,
17 Reverse,
19}
20
21pub struct LinkRouteRegistry {
26 config: Arc<LinksConfig>,
27 routes: HashMap<(String, String), (LinkDefinition, LinkDirection)>,
29}
30
31impl LinkRouteRegistry {
32 pub fn new(config: Arc<LinksConfig>) -> Self {
34 let mut routes = HashMap::new();
35
36 for link_def in &config.links {
38 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 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 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 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 pub fn config(&self) -> &LinksConfig {
99 &self.config
100 }
101}
102
103#[derive(Debug, Clone)]
105pub struct RouteInfo {
106 pub route_name: String,
108
109 pub link_type: String,
111
112 pub direction: LinkDirection,
114
115 pub connected_to: String,
117
118 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}