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 pub fn detect_link_chains(&self, max_depth: usize) -> Vec<LinkChain> {
108 let mut chains = Vec::new();
109
110 for entity_config in &self.config.entities {
112 self.find_chains_from_entity(
113 &entity_config.singular,
114 &mut vec![LinkChainStep {
115 entity_type: entity_config.singular.clone(),
116 route_name: None,
117 direction: LinkDirection::Forward,
118 }],
119 &mut chains,
120 max_depth,
121 &mut std::collections::HashSet::new(),
122 );
123 }
124
125 for entity_config in &self.config.entities {
127 self.find_reverse_chains_from_entity(
128 &entity_config.singular,
129 &mut vec![LinkChainStep {
130 entity_type: entity_config.singular.clone(),
131 route_name: None,
132 direction: LinkDirection::Reverse,
133 }],
134 &mut chains,
135 max_depth,
136 &mut std::collections::HashSet::new(),
137 );
138 }
139
140 chains
141 }
142
143 fn find_chains_from_entity(
145 &self,
146 entity_type: &str,
147 current_chain: &mut Vec<LinkChainStep>,
148 chains: &mut Vec<LinkChain>,
149 remaining_depth: usize,
150 visited: &mut std::collections::HashSet<String>,
151 ) {
152 if remaining_depth == 0 {
153 return;
154 }
155
156 for link_def in &self.config.links {
158 if link_def.source_type == entity_type {
159 let edge = format!("{}->{}", link_def.source_type, link_def.target_type);
160
161 if visited.contains(&edge) {
163 continue;
164 }
165
166 visited.insert(edge.clone());
167
168 let route_name = Some(link_def.forward_route_name.clone());
170
171 current_chain.push(LinkChainStep {
172 entity_type: link_def.target_type.clone(),
173 route_name,
174 direction: LinkDirection::Forward,
175 });
176
177 if current_chain.len() >= 2 {
179 chains.push(LinkChain {
180 steps: current_chain.clone(),
181 config: self.config.clone(),
182 });
183 }
184
185 self.find_chains_from_entity(
187 &link_def.target_type,
188 current_chain,
189 chains,
190 remaining_depth - 1,
191 visited,
192 );
193
194 visited.remove(&edge);
196 current_chain.pop();
197 }
198 }
199 }
200
201 fn find_reverse_chains_from_entity(
203 &self,
204 entity_type: &str,
205 current_chain: &mut Vec<LinkChainStep>,
206 chains: &mut Vec<LinkChain>,
207 remaining_depth: usize,
208 visited: &mut std::collections::HashSet<String>,
209 ) {
210 if remaining_depth == 0 {
211 return;
212 }
213
214 for link_def in &self.config.links {
216 if link_def.target_type == entity_type {
217 let edge = format!("{}<-{}", link_def.source_type, link_def.target_type);
218
219 if visited.contains(&edge) {
221 continue;
222 }
223
224 visited.insert(edge.clone());
225
226 let route_name = Some(link_def.reverse_route_name.clone());
228
229 current_chain.push(LinkChainStep {
230 entity_type: link_def.source_type.clone(),
231 route_name,
232 direction: LinkDirection::Reverse,
233 });
234
235 if current_chain.len() >= 2 {
237 chains.push(LinkChain {
238 steps: current_chain.clone(),
239 config: self.config.clone(),
240 });
241 }
242
243 self.find_reverse_chains_from_entity(
245 &link_def.source_type,
246 current_chain,
247 chains,
248 remaining_depth - 1,
249 visited,
250 );
251
252 visited.remove(&edge);
254 current_chain.pop();
255 }
256 }
257 }
258}
259
260#[derive(Debug, Clone)]
262pub struct LinkChain {
263 pub steps: Vec<LinkChainStep>,
264 pub config: Arc<LinksConfig>,
265}
266
267#[derive(Debug, Clone)]
269pub struct LinkChainStep {
270 pub entity_type: String,
271 pub route_name: Option<String>,
272 pub direction: LinkDirection,
273}
274
275impl LinkChain {
276 pub fn to_route_pattern(&self) -> String {
284 let mut pattern = String::new();
285 let steps_count = self.steps.len();
286
287 for (idx, step) in self.steps.iter().enumerate() {
288 if step.route_name.is_none() {
289 let plural = self.get_plural(&step.entity_type);
291 let param_name = format!("{}_id", step.entity_type);
292 pattern.push_str(&format!("/{plural}/{{{}}}", param_name));
293 } else if let Some(route_name) = &step.route_name {
294 let segment = if idx == steps_count - 1 {
297 self.get_plural(&step.entity_type)
299 } else {
300 route_name.clone()
302 };
303 pattern.push_str(&format!("/{segment}"));
304
305 if idx < steps_count - 1 {
308 let param_name = format!("{}_id", step.entity_type);
309 pattern.push_str(&format!("/{{{}}}", param_name));
310 }
311 }
312 }
313
314 pattern
315 }
316
317 pub fn is_reverse(&self) -> bool {
319 self.steps
320 .first()
321 .map(|s| s.direction == LinkDirection::Reverse)
322 .unwrap_or(false)
323 }
324
325 fn get_plural(&self, singular: &str) -> String {
326 self.config
327 .entities
328 .iter()
329 .find(|e| e.singular == singular)
330 .map(|e| e.plural.clone())
331 .unwrap_or_else(|| format!("{}s", singular))
332 }
333}
334
335#[derive(Debug, Clone)]
337pub struct RouteInfo {
338 pub route_name: String,
340
341 pub link_type: String,
343
344 pub direction: LinkDirection,
346
347 pub connected_to: String,
349
350 pub description: Option<String>,
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use crate::config::EntityConfig;
358
359 fn create_test_config() -> LinksConfig {
360 LinksConfig {
361 entities: vec![
362 EntityConfig {
363 singular: "user".to_string(),
364 plural: "users".to_string(),
365 auth: crate::config::EntityAuthConfig::default(),
366 },
367 EntityConfig {
368 singular: "car".to_string(),
369 plural: "cars".to_string(),
370 auth: crate::config::EntityAuthConfig::default(),
371 },
372 ],
373 links: vec![
374 LinkDefinition {
375 link_type: "owner".to_string(),
376 source_type: "user".to_string(),
377 target_type: "car".to_string(),
378 forward_route_name: "cars-owned".to_string(),
379 reverse_route_name: "users-owners".to_string(),
380 description: Some("User owns a car".to_string()),
381 required_fields: None,
382 auth: None,
383 },
384 LinkDefinition {
385 link_type: "driver".to_string(),
386 source_type: "user".to_string(),
387 target_type: "car".to_string(),
388 forward_route_name: "cars-driven".to_string(),
389 reverse_route_name: "users-drivers".to_string(),
390 description: Some("User drives a car".to_string()),
391 required_fields: None,
392 auth: None,
393 },
394 ],
395 validation_rules: None,
396 }
397 }
398
399 #[test]
400 fn test_resolve_forward_route() {
401 let config = Arc::new(create_test_config());
402 let registry = LinkRouteRegistry::new(config);
403
404 let (def, direction) = registry.resolve_route("user", "cars-owned").unwrap();
405
406 assert_eq!(def.link_type, "owner");
407 assert_eq!(def.source_type, "user");
408 assert_eq!(def.target_type, "car");
409 assert_eq!(direction, LinkDirection::Forward);
410 }
411
412 #[test]
413 fn test_resolve_reverse_route() {
414 let config = Arc::new(create_test_config());
415 let registry = LinkRouteRegistry::new(config);
416
417 let (def, direction) = registry.resolve_route("car", "users-owners").unwrap();
418
419 assert_eq!(def.link_type, "owner");
420 assert_eq!(def.source_type, "user");
421 assert_eq!(def.target_type, "car");
422 assert_eq!(direction, LinkDirection::Reverse);
423 }
424
425 #[test]
426 fn test_list_routes_for_entity() {
427 let config = Arc::new(create_test_config());
428 let registry = LinkRouteRegistry::new(config);
429
430 let routes = registry.list_routes_for_entity("user");
431
432 assert_eq!(routes.len(), 2);
433
434 let route_names: Vec<_> = routes.iter().map(|r| r.route_name.as_str()).collect();
435 assert!(route_names.contains(&"cars-owned"));
436 assert!(route_names.contains(&"cars-driven"));
437 }
438
439 #[test]
440 fn test_no_route_conflicts() {
441 let config = Arc::new(create_test_config());
442 let registry = LinkRouteRegistry::new(config);
443
444 let user_routes = registry.list_routes_for_entity("user");
445 let route_names: Vec<_> = user_routes.iter().map(|r| &r.route_name).collect();
446
447 let unique_names: std::collections::HashSet<_> = route_names.iter().collect();
448 assert_eq!(
449 route_names.len(),
450 unique_names.len(),
451 "Route names must be unique"
452 );
453 }
454}