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::{Result, anyhow};
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    /// Detect all possible link chains from the configuration (forward and reverse)
103    ///
104    /// Returns a list of chains like: (source_type, [(route_name, target_type), ...])
105    /// Example: (order, [("invoices", invoice), ("payments", payment)]) for the chain:
106    /// Order → Invoice → Payment
107    pub fn detect_link_chains(&self, max_depth: usize) -> Vec<LinkChain> {
108        let mut chains = Vec::new();
109
110        // Pour chaque type d'entité, trouver toutes les chaînes possibles (forward)
111        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        // Pour chaque type d'entité, trouver toutes les chaînes inverses (reverse)
126        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    /// Helper to recursively find chains from an entity (forward direction)
144    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        // Trouver tous les liens sortants de cette entité
157        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                // Éviter les cycles
162                if visited.contains(&edge) {
163                    continue;
164                }
165
166                visited.insert(edge.clone());
167
168                // Ajouter cette étape à la chaîne
169                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                // Si c'est une chaîne valide (au moins 2 steps), l'ajouter
178                if current_chain.len() >= 2 {
179                    chains.push(LinkChain {
180                        steps: current_chain.clone(),
181                        config: self.config.clone(),
182                    });
183                }
184
185                // Continuer récursivement
186                self.find_chains_from_entity(
187                    &link_def.target_type,
188                    current_chain,
189                    chains,
190                    remaining_depth - 1,
191                    visited,
192                );
193
194                // Retirer cette étape
195                visited.remove(&edge);
196                current_chain.pop();
197            }
198        }
199    }
200
201    /// Helper to recursively find chains from an entity (reverse direction)
202    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        // Trouver tous les liens entrants de cette entité
215        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                // Éviter les cycles
220                if visited.contains(&edge) {
221                    continue;
222                }
223
224                visited.insert(edge.clone());
225
226                // Ajouter cette étape à la chaîne (avec reverse route name)
227                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                // Si c'est une chaîne valide (au moins 2 steps), l'ajouter
236                if current_chain.len() >= 2 {
237                    chains.push(LinkChain {
238                        steps: current_chain.clone(),
239                        config: self.config.clone(),
240                    });
241                }
242
243                // Continuer récursivement
244                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                // Retirer cette étape
253                visited.remove(&edge);
254                current_chain.pop();
255            }
256        }
257    }
258}
259
260/// Une chaîne de liens détectée
261#[derive(Debug, Clone)]
262pub struct LinkChain {
263    pub steps: Vec<LinkChainStep>,
264    pub config: Arc<LinksConfig>,
265}
266
267/// Une étape dans une chaîne de liens
268#[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    /// Génère le pattern de route Axum pour cette chaîne
277    ///
278    /// Exemple forward: order → invoice → payment
279    ///   "/orders/{order_id}/invoices/{invoice_id}/payments"
280    ///
281    /// Exemple reverse: payment ← invoice ← order
282    ///   "/payments/{payment_id}/invoice/{invoice_id}/orders"
283    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                // Premier step: entité source
290                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                // Step intermédiaire avec route
295                // Pour le dernier step, utiliser le pluriel au lieu du route_name
296                let segment = if idx == steps_count - 1 {
297                    // Dernier step: utiliser le pluriel
298                    self.get_plural(&step.entity_type)
299                } else {
300                    // Step intermédiaire: utiliser le route_name
301                    route_name.clone()
302                };
303                pattern.push_str(&format!("/{segment}"));
304
305                // Ajouter le param ID pour ce step
306                // SAUF si c'est le dernier step (pour la route de liste)
307                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    /// Indique si cette chaîne est en sens inverse
318    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/// Information about a route available for an entity
336#[derive(Debug, Clone)]
337pub struct RouteInfo {
338    /// The route name (e.g., "cars-owned")
339    pub route_name: String,
340
341    /// The type of link (e.g., "owner")
342    pub link_type: String,
343
344    /// Direction of the relationship
345    pub direction: LinkDirection,
346
347    /// The entity type this route connects to
348    pub connected_to: String,
349
350    /// Optional description
351    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}