this/core/
extractors.rs

1//! Axum extractors for entities and links
2//!
3//! This module provides HTTP extractors that automatically:
4//! - Deserialize and validate entities from request bodies
5//! - Parse link routes and resolve definitions
6
7use axum::Json;
8use axum::http::StatusCode;
9use axum::response::{IntoResponse, Response};
10use uuid::Uuid;
11
12use crate::config::LinksConfig;
13use crate::core::LinkDefinition;
14use crate::links::registry::{LinkDirection, LinkRouteRegistry};
15
16/// Errors that can occur during extraction
17#[derive(Debug, Clone)]
18pub enum ExtractorError {
19    InvalidPath,
20    InvalidEntityId,
21    RouteNotFound(String),
22    LinkNotFound,
23    JsonError(String),
24}
25
26impl std::fmt::Display for ExtractorError {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            ExtractorError::InvalidPath => write!(f, "Invalid path format"),
30            ExtractorError::InvalidEntityId => write!(f, "Invalid entity ID format"),
31            ExtractorError::RouteNotFound(route) => write!(f, "Route not found: {}", route),
32            ExtractorError::LinkNotFound => write!(f, "Link not found"),
33            ExtractorError::JsonError(msg) => write!(f, "JSON error: {}", msg),
34        }
35    }
36}
37
38impl std::error::Error for ExtractorError {}
39
40impl IntoResponse for ExtractorError {
41    fn into_response(self) -> Response {
42        let (status, message) = match self {
43            ExtractorError::InvalidPath => (StatusCode::BAD_REQUEST, self.to_string()),
44            ExtractorError::InvalidEntityId => (StatusCode::BAD_REQUEST, self.to_string()),
45            ExtractorError::RouteNotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
46            ExtractorError::LinkNotFound => (StatusCode::NOT_FOUND, self.to_string()),
47            ExtractorError::JsonError(_) => (StatusCode::BAD_REQUEST, self.to_string()),
48        };
49
50        (status, Json(serde_json::json!({ "error": message }))).into_response()
51    }
52}
53
54/// Extractor for link information from path
55///
56/// Automatically parses the path and resolves link definitions.
57/// Supports both forward and reverse navigation.
58#[derive(Debug, Clone)]
59pub struct LinkExtractor {
60    pub entity_id: Uuid,
61    pub entity_type: String,
62    pub link_definition: LinkDefinition,
63    pub direction: LinkDirection,
64}
65
66impl LinkExtractor {
67    /// Parse a link route path
68    ///
69    /// Expected format: `/{entity_type}/{entity_id}/{route_name}`
70    /// Example: `/users/123.../cars-owned`
71    pub fn from_path_and_registry(
72        path_parts: (String, Uuid, String),
73        registry: &LinkRouteRegistry,
74        config: &LinksConfig,
75    ) -> Result<Self, ExtractorError> {
76        let (entity_type_plural, entity_id, route_name) = path_parts;
77
78        // Convert plural to singular
79        let entity_type = config
80            .entities
81            .iter()
82            .find(|e| e.plural == entity_type_plural)
83            .map(|e| e.singular.clone())
84            .unwrap_or(entity_type_plural);
85
86        // Resolve the route
87        let (link_definition, direction) = registry
88            .resolve_route(&entity_type, &route_name)
89            .map_err(|_| ExtractorError::RouteNotFound(route_name.clone()))?;
90
91        Ok(Self {
92            entity_id,
93            entity_type,
94            link_definition,
95            direction,
96        })
97    }
98}
99
100/// Extractor for direct link creation/deletion/update
101///
102/// Format: `/{source_type}/{source_id}/{route_name}/{target_id}`
103/// Example: `/users/123.../cars-owned/456...`
104///
105/// This uses the route_name (e.g., "cars-owned") instead of link_type (e.g., "owner")
106/// to provide more semantic and RESTful URLs.
107#[derive(Debug, Clone)]
108pub struct DirectLinkExtractor {
109    pub source_id: Uuid,
110    pub source_type: String,
111    pub target_id: Uuid,
112    pub target_type: String,
113    pub link_definition: LinkDefinition,
114    pub direction: LinkDirection,
115}
116
117impl DirectLinkExtractor {
118    /// Parse a direct link path using route_name
119    ///
120    /// path_parts = (source_type_plural, source_id, route_name, target_id)
121    ///
122    /// The route_name is resolved to a link definition using the LinkRouteRegistry,
123    /// which handles both forward and reverse navigation automatically.
124    pub fn from_path(
125        path_parts: (String, Uuid, String, Uuid),
126        registry: &LinkRouteRegistry,
127        config: &LinksConfig,
128    ) -> Result<Self, ExtractorError> {
129        let (source_type_plural, source_id, route_name, target_id) = path_parts;
130
131        // Convert plural to singular
132        let source_type = config
133            .entities
134            .iter()
135            .find(|e| e.plural == source_type_plural)
136            .map(|e| e.singular.clone())
137            .unwrap_or(source_type_plural);
138
139        // Resolve the route to get link definition and direction
140        let (link_definition, direction) = registry
141            .resolve_route(&source_type, &route_name)
142            .map_err(|_| ExtractorError::RouteNotFound(route_name.clone()))?;
143
144        // Determine target type based on direction
145        let target_type = match direction {
146            LinkDirection::Forward => link_definition.target_type.clone(),
147            LinkDirection::Reverse => link_definition.source_type.clone(),
148        };
149
150        Ok(Self {
151            source_id,
152            source_type,
153            target_id,
154            target_type,
155            link_definition,
156            direction,
157        })
158    }
159}
160
161/// Segment d'une chaîne de liens imbriqués
162#[derive(Debug, Clone, serde::Serialize)]
163pub struct LinkPathSegment {
164    /// Type d'entité (singulier)
165    pub entity_type: String,
166    /// ID de l'entité
167    pub entity_id: Uuid,
168    /// Nom de la route (si présent)
169    pub route_name: Option<String>,
170    /// Définition du lien (si présent)
171    pub link_definition: Option<LinkDefinition>,
172    /// Direction du lien (Forward ou Reverse)
173    #[serde(skip_serializing)]
174    pub link_direction: Option<LinkDirection>,
175}
176
177/// Extractor pour chemins imbriqués de profondeur illimitée
178///
179/// Parse dynamiquement des chemins comme:
180/// - /users/123/invoices/456/orders
181/// - /users/123/invoices/456/orders/789/payments/101
182#[derive(Debug, Clone)]
183pub struct RecursiveLinkExtractor {
184    pub chain: Vec<LinkPathSegment>,
185    /// True si le chemin se termine par une route (liste)
186    /// False si le chemin se termine par un ID (item spécifique)
187    pub is_list: bool,
188}
189
190impl RecursiveLinkExtractor {
191    /// Parse un chemin complet dynamiquement
192    pub fn from_segments(
193        segments: Vec<String>,
194        registry: &LinkRouteRegistry,
195        config: &LinksConfig,
196    ) -> Result<Self, ExtractorError> {
197        if segments.len() < 2 {
198            return Err(ExtractorError::InvalidPath);
199        }
200
201        let mut chain = Vec::new();
202        let mut i = 0;
203        let mut current_entity_type: Option<String> = None;
204
205        // Pattern attendu: type/id/route/id/route/id...
206        // Premier segment: toujours un type d'entité
207        while i < segments.len() {
208            // 1. Type d'entité (soit depuis URL pour le 1er, soit depuis link_def pour les suivants)
209            let entity_type_singular = if let Some(ref entity_type) = current_entity_type {
210                // Type connu depuis la résolution précédente
211                entity_type.clone()
212            } else {
213                // Premier segment: lire le type depuis l'URL
214                let entity_type_plural = &segments[i];
215                let singular = config
216                    .entities
217                    .iter()
218                    .find(|e| e.plural == *entity_type_plural)
219                    .map(|e| e.singular.clone())
220                    .ok_or(ExtractorError::InvalidPath)?;
221                i += 1;
222                singular
223            };
224
225            // Reset pour la prochaine itération
226            current_entity_type = None;
227
228            // 2. ID de l'entité (peut ne pas exister si fin du chemin)
229            let entity_id = if i < segments.len() {
230                segments[i]
231                    .parse::<Uuid>()
232                    .map_err(|_| ExtractorError::InvalidEntityId)?
233            } else {
234                // Pas d'ID = liste finale
235                chain.push(LinkPathSegment {
236                    entity_type: entity_type_singular,
237                    entity_id: Uuid::nil(),
238                    route_name: None,
239                    link_definition: None,
240                    link_direction: None,
241                });
242                break;
243            };
244            i += 1;
245
246            // 3. Nom de route (peut ne pas exister si fin du chemin)
247            let route_name = if i < segments.len() {
248                Some(segments[i].clone())
249            } else {
250                None
251            };
252
253            if route_name.is_some() {
254                i += 1;
255            }
256
257            // Résoudre la définition du lien si on a une route
258            let (link_def, link_dir) = if let Some(route_name) = &route_name {
259                let (link_def, direction) = registry
260                    .resolve_route(&entity_type_singular, route_name)
261                    .map_err(|_| ExtractorError::RouteNotFound(route_name.clone()))?;
262
263                // Préparer le type pour la prochaine itération
264                // Pour Forward: on va vers target_type
265                // Pour Reverse: on va vers source_type (car on remonte la chaîne)
266                current_entity_type = Some(match direction {
267                    crate::links::registry::LinkDirection::Forward => link_def.target_type.clone(),
268                    crate::links::registry::LinkDirection::Reverse => link_def.source_type.clone(),
269                });
270
271                (Some(link_def), Some(direction))
272            } else {
273                (None, None)
274            };
275
276            chain.push(LinkPathSegment {
277                entity_type: entity_type_singular,
278                entity_id,
279                route_name,
280                link_definition: link_def,
281                link_direction: link_dir,
282            });
283        }
284
285        // Si current_entity_type est défini, cela signifie que le chemin se termine par une route
286        // et qu'on doit ajouter un segment final pour l'entité cible (liste)
287        if let Some(final_entity_type) = current_entity_type {
288            chain.push(LinkPathSegment {
289                entity_type: final_entity_type,
290                entity_id: Uuid::nil(), // Pas d'ID spécifique = liste
291                route_name: None,
292                link_definition: None,
293                link_direction: None,
294            });
295        }
296
297        // Déterminer si c'est une liste ou un item spécifique
298        // Format: type/id/route/id/route → 5 segments → liste
299        // Format: type/id/route/id/route/id → 6 segments → item
300        // Si impair ≥ 5: liste, si pair ≥ 6: item spécifique
301        let is_list = (segments.len() % 2 == 1) && (segments.len() >= 5);
302
303        Ok(Self { chain, is_list })
304    }
305
306    /// Obtenir l'ID final et le type pour la requête finale
307    pub fn final_target(&self) -> (Uuid, String) {
308        let last = self.chain.last().unwrap();
309        (last.entity_id, last.entity_type.clone())
310    }
311
312    /// Obtenir la définition du dernier lien
313    pub fn final_link_def(&self) -> Option<&LinkDefinition> {
314        // Le dernier segment n'a pas de link_def, le pénultième oui
315        if self.chain.len() >= 2 {
316            self.chain
317                .get(self.chain.len() - 2)
318                .and_then(|s| s.link_definition.as_ref())
319        } else {
320            None
321        }
322    }
323
324    /// Obtenir l'avant-dernier segment (celui qui a le lien)
325    pub fn penultimate_segment(&self) -> Option<&LinkPathSegment> {
326        if self.chain.len() >= 2 {
327            self.chain.get(self.chain.len() - 2)
328        } else {
329            None
330        }
331    }
332}