this/links/
handlers.rs

1//! HTTP handlers for link operations
2//!
3//! This module provides generic handlers that work with any entity types.
4//! All handlers are completely entity-agnostic.
5
6use axum::{
7    Json,
8    extract::{Path, Query, State},
9    http::StatusCode,
10    response::{IntoResponse, Response},
11};
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use std::collections::HashMap;
16use std::sync::Arc;
17use uuid::Uuid;
18
19use crate::config::LinksConfig;
20use crate::core::extractors::{
21    DirectLinkExtractor, ExtractorError, LinkExtractor, RecursiveLinkExtractor,
22};
23use crate::core::{
24    EntityCreator, EntityFetcher, LinkDefinition, LinkService,
25    link::LinkEntity,
26    query::{PaginationMeta, QueryParams},
27};
28use crate::links::registry::{LinkDirection, LinkRouteRegistry};
29
30/// Application state shared across handlers
31#[derive(Clone)]
32pub struct AppState {
33    pub link_service: Arc<dyn LinkService>,
34    pub config: Arc<LinksConfig>,
35    pub registry: Arc<LinkRouteRegistry>,
36    /// Entity fetchers for enriching links with full entity data
37    pub entity_fetchers: Arc<HashMap<String, Arc<dyn EntityFetcher>>>,
38    /// Entity creators for creating new entities with automatic linking
39    pub entity_creators: Arc<HashMap<String, Arc<dyn EntityCreator>>>,
40}
41
42impl AppState {
43    /// Get the authorization policy for a link operation
44    pub fn get_link_auth_policy(
45        link_definition: &LinkDefinition,
46        operation: &str,
47    ) -> Option<String> {
48        link_definition.auth.as_ref().map(|auth| match operation {
49            "list" => auth.list.clone(),
50            "get" => auth.get.clone(),
51            "create" => auth.create.clone(),
52            "update" => auth.update.clone(),
53            "delete" => auth.delete.clone(),
54            _ => "authenticated".to_string(),
55        })
56    }
57}
58
59/// Response for list links endpoint
60#[derive(Debug, Serialize)]
61pub struct ListLinksResponse {
62    pub links: Vec<LinkEntity>,
63    pub count: usize,
64    pub link_type: String,
65    pub direction: String,
66    pub description: Option<String>,
67}
68
69/// Link with full entity data instead of just references
70#[derive(Debug, Serialize)]
71pub struct EnrichedLink {
72    /// Unique identifier for this link
73    pub id: Uuid,
74
75    /// Entity type
76    #[serde(rename = "type")]
77    pub entity_type: String,
78
79    /// The type of relationship (e.g., "has_invoice", "payment")
80    pub link_type: String,
81
82    /// Source entity ID
83    pub source_id: Uuid,
84
85    /// Target entity ID
86    pub target_id: Uuid,
87
88    /// Full source entity as JSON (omitted when querying from source)
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub source: Option<serde_json::Value>,
91
92    /// Full target entity as JSON (omitted when querying from target)
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub target: Option<serde_json::Value>,
95
96    /// Optional metadata for the relationship
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub metadata: Option<serde_json::Value>,
99
100    /// When this link was created
101    pub created_at: DateTime<Utc>,
102
103    /// When this link was last updated
104    pub updated_at: DateTime<Utc>,
105
106    /// Status
107    pub status: String,
108}
109
110/// Response for enriched list links endpoint (legacy, without pagination)
111#[derive(Debug, Serialize)]
112pub struct EnrichedListLinksResponse {
113    pub links: Vec<EnrichedLink>,
114    pub count: usize,
115    pub link_type: String,
116    pub direction: String,
117    pub description: Option<String>,
118}
119
120/// Paginated response for enriched list links endpoint
121#[derive(Debug, Serialize)]
122pub struct PaginatedEnrichedLinksResponse {
123    pub data: Vec<EnrichedLink>,
124    pub pagination: PaginationMeta,
125    pub link_type: String,
126    pub direction: String,
127    pub description: Option<String>,
128}
129
130/// Request body for creating a link between existing entities
131#[derive(Debug, Deserialize)]
132pub struct CreateLinkRequest {
133    pub metadata: Option<serde_json::Value>,
134}
135
136/// Request body for creating a new linked entity
137#[derive(Debug, Deserialize)]
138pub struct CreateLinkedEntityRequest {
139    pub entity: serde_json::Value,
140    pub metadata: Option<serde_json::Value>,
141}
142
143/// Context for link enrichment
144#[derive(Debug, Clone, Copy)]
145pub enum EnrichmentContext {
146    /// Query from source entity - only target entities are included
147    FromSource,
148    /// Query from target entity - only source entities are included
149    FromTarget,
150    /// Direct link access - both source and target entities are included
151    DirectLink,
152}
153
154/// List links using named routes (forward or reverse) - WITH PAGINATION
155///
156/// GET /{entity_type}/{entity_id}/{route_name}
157pub async fn list_links(
158    State(state): State<AppState>,
159    Path((entity_type_plural, entity_id, route_name)): Path<(String, Uuid, String)>,
160    Query(params): Query<QueryParams>,
161) -> Result<Json<PaginatedEnrichedLinksResponse>, ExtractorError> {
162    let extractor = LinkExtractor::from_path_and_registry(
163        (entity_type_plural, entity_id, route_name),
164        &state.registry,
165        &state.config,
166    )?;
167
168    // Query links based on direction
169    let links = match extractor.direction {
170        LinkDirection::Forward => state
171            .link_service
172            .find_by_source(
173                &extractor.entity_id,
174                Some(&extractor.link_definition.link_type),
175                Some(&extractor.link_definition.target_type),
176            )
177            .await
178            .map_err(|e| ExtractorError::JsonError(e.to_string()))?,
179        LinkDirection::Reverse => state
180            .link_service
181            .find_by_target(
182                &extractor.entity_id,
183                Some(&extractor.link_definition.link_type),
184                Some(&extractor.link_definition.source_type),
185            )
186            .await
187            .map_err(|e| ExtractorError::JsonError(e.to_string()))?,
188    };
189
190    // Determine enrichment context based on direction
191    let context = match extractor.direction {
192        LinkDirection::Forward => EnrichmentContext::FromSource,
193        LinkDirection::Reverse => EnrichmentContext::FromTarget,
194    };
195
196    // Enrich ALL links with full entity data first
197    let mut all_enriched =
198        enrich_links_with_entities(&state, links, context, &extractor.link_definition).await?;
199
200    // Apply filters if provided
201    if let Some(filter_value) = params.filter_value() {
202        all_enriched = apply_link_filters(all_enriched, &filter_value);
203    }
204
205    let total = all_enriched.len();
206
207    // Apply pagination (ALWAYS paginate for links)
208    let page = params.page();
209    let limit = params.limit();
210    let start = (page - 1) * limit;
211
212    let paginated_links: Vec<EnrichedLink> =
213        all_enriched.into_iter().skip(start).take(limit).collect();
214
215    Ok(Json(PaginatedEnrichedLinksResponse {
216        data: paginated_links,
217        pagination: PaginationMeta::new(page, limit, total),
218        link_type: extractor.link_definition.link_type,
219        direction: format!("{:?}", extractor.direction),
220        description: extractor.link_definition.description,
221    }))
222}
223
224/// Helper function to enrich links with full entity data
225async fn enrich_links_with_entities(
226    state: &AppState,
227    links: Vec<LinkEntity>,
228    context: EnrichmentContext,
229    link_definition: &LinkDefinition,
230) -> Result<Vec<EnrichedLink>, ExtractorError> {
231    let mut enriched = Vec::new();
232
233    for link in links {
234        // Fetch source entity only if needed
235        let source_entity = match context {
236            EnrichmentContext::FromSource => None,
237            EnrichmentContext::FromTarget | EnrichmentContext::DirectLink => {
238                // Fetch source entity using the type from link definition
239                fetch_entity_by_type(state, &link_definition.source_type, &link.source_id)
240                    .await
241                    .ok()
242            }
243        };
244
245        // Fetch target entity only if needed
246        let target_entity = match context {
247            EnrichmentContext::FromTarget => None,
248            EnrichmentContext::FromSource | EnrichmentContext::DirectLink => {
249                // Fetch target entity using the type from link definition
250                fetch_entity_by_type(state, &link_definition.target_type, &link.target_id)
251                    .await
252                    .ok()
253            }
254        };
255
256        enriched.push(EnrichedLink {
257            id: link.id,
258            entity_type: link.entity_type,
259            link_type: link.link_type,
260            source_id: link.source_id,
261            target_id: link.target_id,
262            source: source_entity,
263            target: target_entity,
264            metadata: link.metadata,
265            created_at: link.created_at,
266            updated_at: link.updated_at,
267            status: link.status,
268        });
269    }
270
271    Ok(enriched)
272}
273
274/// Fetch an entity dynamically by type
275async fn fetch_entity_by_type(
276    state: &AppState,
277    entity_type: &str,
278    entity_id: &Uuid,
279) -> Result<serde_json::Value, ExtractorError> {
280    let fetcher = state.entity_fetchers.get(entity_type).ok_or_else(|| {
281        ExtractorError::JsonError(format!(
282            "No entity fetcher registered for type: {}",
283            entity_type
284        ))
285    })?;
286
287    fetcher
288        .fetch_as_json(entity_id)
289        .await
290        .map_err(|e| ExtractorError::JsonError(format!("Failed to fetch entity: {}", e)))
291}
292
293/// Apply filtering to enriched links based on query parameters
294///
295/// Supports filtering on:
296/// - link fields (id, link_type, source_id, target_id, status, metadata)
297/// - nested entity fields (source.*, target.*)
298fn apply_link_filters(enriched_links: Vec<EnrichedLink>, filter: &Value) -> Vec<EnrichedLink> {
299    if filter.is_null() || !filter.is_object() {
300        return enriched_links;
301    }
302
303    let filter_obj = filter.as_object().unwrap();
304
305    enriched_links
306        .into_iter()
307        .filter(|link| {
308            let mut matches = true;
309
310            // Convert link to JSON for easy filtering
311            let link_json = match serde_json::to_value(link) {
312                Ok(v) => v,
313                Err(_) => return false,
314            };
315
316            for (key, value) in filter_obj.iter() {
317                // Check if the field exists in the link or in nested entities
318                let field_value = get_nested_value(&link_json, key);
319
320                match field_value {
321                    Some(field_val) => {
322                        // Simple equality match for now
323                        if field_val != *value {
324                            matches = false;
325                            break;
326                        }
327                    }
328                    None => {
329                        matches = false;
330                        break;
331                    }
332                }
333            }
334
335            matches
336        })
337        .collect()
338}
339
340/// Get a nested value from JSON using dot notation
341/// E.g., "source.name" or "target.amount"
342fn get_nested_value(json: &Value, key: &str) -> Option<Value> {
343    let parts: Vec<&str> = key.split('.').collect();
344
345    match parts.len() {
346        1 => json.get(key).cloned(),
347        2 => {
348            let (parent, child) = (parts[0], parts[1]);
349            json.get(parent).and_then(|v| v.get(child)).cloned()
350        }
351        _ => None,
352    }
353}
354
355/// Get a specific link by ID
356///
357/// GET /links/{link_id}
358pub async fn get_link(
359    State(state): State<AppState>,
360    Path(link_id): Path<Uuid>,
361) -> Result<Response, ExtractorError> {
362    let link = state
363        .link_service
364        .get(&link_id)
365        .await
366        .map_err(|e| ExtractorError::JsonError(e.to_string()))?
367        .ok_or(ExtractorError::LinkNotFound)?;
368
369    // Find the link definition from config
370    let link_definition = state
371        .config
372        .links
373        .iter()
374        .find(|def| def.link_type == link.link_type)
375        .ok_or_else(|| {
376            ExtractorError::JsonError(format!(
377                "No link definition found for link_type: {}",
378                link.link_type
379            ))
380        })?;
381
382    // Enrich with both source and target entities
383    let enriched_links = enrich_links_with_entities(
384        &state,
385        vec![link],
386        EnrichmentContext::DirectLink,
387        link_definition,
388    )
389    .await?;
390
391    let enriched_link = enriched_links
392        .into_iter()
393        .next()
394        .ok_or(ExtractorError::LinkNotFound)?;
395
396    Ok(Json(enriched_link).into_response())
397}
398
399/// Get a specific link by source, route_name, and target
400///
401/// GET /{source_type}/{source_id}/{route_name}/{target_id}
402pub async fn get_link_by_route(
403    State(state): State<AppState>,
404    Path((source_type_plural, source_id, route_name, target_id)): Path<(
405        String,
406        Uuid,
407        String,
408        Uuid,
409    )>,
410) -> Result<Response, ExtractorError> {
411    let extractor = DirectLinkExtractor::from_path(
412        (source_type_plural, source_id, route_name, target_id),
413        &state.registry,
414        &state.config,
415    )?;
416
417    // Find the specific link based on direction
418    let existing_links = match extractor.direction {
419        LinkDirection::Forward => {
420            // Forward: search by source_id in URL
421            state
422                .link_service
423                .find_by_source(
424                    &extractor.source_id,
425                    Some(&extractor.link_definition.link_type),
426                    Some(&extractor.target_type),
427                )
428                .await
429                .map_err(|e| ExtractorError::JsonError(e.to_string()))?
430        }
431        LinkDirection::Reverse => {
432            // Reverse: search by target_id in URL (which is the actual source in DB)
433            state
434                .link_service
435                .find_by_source(
436                    &extractor.target_id,
437                    Some(&extractor.link_definition.link_type),
438                    Some(&extractor.source_type),
439                )
440                .await
441                .map_err(|e| ExtractorError::JsonError(e.to_string()))?
442        }
443    };
444
445    let link = existing_links
446        .into_iter()
447        .find(|link| match extractor.direction {
448            LinkDirection::Forward => link.target_id == extractor.target_id,
449            LinkDirection::Reverse => link.target_id == extractor.source_id,
450        })
451        .ok_or(ExtractorError::LinkNotFound)?;
452
453    // Enrich with both source and target entities
454    let enriched_links = enrich_links_with_entities(
455        &state,
456        vec![link],
457        EnrichmentContext::DirectLink,
458        &extractor.link_definition,
459    )
460    .await?;
461
462    let enriched_link = enriched_links
463        .into_iter()
464        .next()
465        .ok_or(ExtractorError::LinkNotFound)?;
466
467    Ok(Json(enriched_link).into_response())
468}
469
470/// Create a link between two existing entities
471///
472/// POST /{source_type}/{source_id}/{route_name}/{target_id}
473/// Body: { "metadata": {...} }
474pub async fn create_link(
475    State(state): State<AppState>,
476    Path((source_type_plural, source_id, route_name, target_id)): Path<(
477        String,
478        Uuid,
479        String,
480        Uuid,
481    )>,
482    Json(payload): Json<CreateLinkRequest>,
483) -> Result<Response, ExtractorError> {
484    let extractor = DirectLinkExtractor::from_path(
485        (source_type_plural, source_id, route_name, target_id),
486        &state.registry,
487        &state.config,
488    )?;
489
490    // Create the link between existing entities
491    let link = LinkEntity::new(
492        extractor.link_definition.link_type,
493        extractor.source_id,
494        extractor.target_id,
495        payload.metadata,
496    );
497
498    let created_link = state
499        .link_service
500        .create(link)
501        .await
502        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
503
504    Ok((StatusCode::CREATED, Json(created_link)).into_response())
505}
506
507/// Create a new entity and link it to the source
508///
509/// POST /{source_type}/{source_id}/{route_name}
510/// Body: { "entity": {...entity fields...}, "metadata": {...link metadata...} }
511pub async fn create_linked_entity(
512    State(state): State<AppState>,
513    Path((source_type_plural, source_id, route_name)): Path<(String, Uuid, String)>,
514    Json(payload): Json<CreateLinkedEntityRequest>,
515) -> Result<Response, ExtractorError> {
516    let extractor = LinkExtractor::from_path_and_registry(
517        (source_type_plural.clone(), source_id, route_name.clone()),
518        &state.registry,
519        &state.config,
520    )?;
521
522    // Determine source and target based on direction
523    let (source_entity_id, target_entity_type) = match extractor.direction {
524        LinkDirection::Forward => {
525            // Forward: source is the entity in the URL, target is the new entity
526            (extractor.entity_id, &extractor.link_definition.target_type)
527        }
528        LinkDirection::Reverse => {
529            // Reverse: target is the entity in the URL, source is the new entity
530            (extractor.entity_id, &extractor.link_definition.source_type)
531        }
532    };
533
534    // Get the entity creator for the target type
535    let entity_creator = state
536        .entity_creators
537        .get(target_entity_type)
538        .ok_or_else(|| {
539            ExtractorError::JsonError(format!(
540                "No entity creator registered for type: {}",
541                target_entity_type
542            ))
543        })?;
544
545    // Create the new entity
546    let created_entity = entity_creator
547        .create_from_json(payload.entity)
548        .await
549        .map_err(|e| ExtractorError::JsonError(format!("Failed to create entity: {}", e)))?;
550
551    // Extract the ID from the created entity
552    let target_entity_id = created_entity["id"].as_str().ok_or_else(|| {
553        ExtractorError::JsonError("Created entity missing 'id' field".to_string())
554    })?;
555    let target_entity_id = Uuid::parse_str(target_entity_id)
556        .map_err(|e| ExtractorError::JsonError(format!("Invalid UUID in created entity: {}", e)))?;
557
558    // Create the link based on direction
559    let link = match extractor.direction {
560        LinkDirection::Forward => {
561            // Forward: source -> target (new entity)
562            LinkEntity::new(
563                extractor.link_definition.link_type,
564                source_entity_id,
565                target_entity_id,
566                payload.metadata,
567            )
568        }
569        LinkDirection::Reverse => {
570            // Reverse: source (new entity) -> target
571            LinkEntity::new(
572                extractor.link_definition.link_type,
573                target_entity_id,
574                source_entity_id,
575                payload.metadata,
576            )
577        }
578    };
579
580    let created_link = state
581        .link_service
582        .create(link)
583        .await
584        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
585
586    // Return both the created entity and the link
587    let response = serde_json::json!({
588        "entity": created_entity,
589        "link": created_link,
590    });
591
592    Ok((StatusCode::CREATED, Json(response)).into_response())
593}
594
595/// Update a link's metadata using route name
596///
597/// PUT/PATCH /{source_type}/{source_id}/{route_name}/{target_id}
598pub async fn update_link(
599    State(state): State<AppState>,
600    Path((source_type_plural, source_id, route_name, target_id)): Path<(
601        String,
602        Uuid,
603        String,
604        Uuid,
605    )>,
606    Json(payload): Json<CreateLinkRequest>,
607) -> Result<Response, ExtractorError> {
608    let extractor = DirectLinkExtractor::from_path(
609        (source_type_plural, source_id, route_name, target_id),
610        &state.registry,
611        &state.config,
612    )?;
613
614    // Find the existing link
615    let existing_links = state
616        .link_service
617        .find_by_source(
618            &extractor.source_id,
619            Some(&extractor.link_definition.link_type),
620            Some(&extractor.target_type),
621        )
622        .await
623        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
624
625    let mut existing_link = existing_links
626        .into_iter()
627        .find(|link| link.target_id == extractor.target_id)
628        .ok_or_else(|| ExtractorError::RouteNotFound("Link not found".to_string()))?;
629
630    // Update metadata
631    existing_link.metadata = payload.metadata;
632    existing_link.touch();
633
634    // Save the updated link
635    let link_id = existing_link.id;
636    let updated_link = state
637        .link_service
638        .update(&link_id, existing_link)
639        .await
640        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
641
642    Ok(Json(updated_link).into_response())
643}
644
645/// Delete a link using route name
646///
647/// DELETE /{source_type}/{source_id}/{route_name}/{target_id}
648pub async fn delete_link(
649    State(state): State<AppState>,
650    Path((source_type_plural, source_id, route_name, target_id)): Path<(
651        String,
652        Uuid,
653        String,
654        Uuid,
655    )>,
656) -> Result<Response, ExtractorError> {
657    let extractor = DirectLinkExtractor::from_path(
658        (source_type_plural, source_id, route_name, target_id),
659        &state.registry,
660        &state.config,
661    )?;
662
663    // Find the existing link first
664    let existing_links = state
665        .link_service
666        .find_by_source(
667            &extractor.source_id,
668            Some(&extractor.link_definition.link_type),
669            Some(&extractor.target_type),
670        )
671        .await
672        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
673
674    let existing_link = existing_links
675        .into_iter()
676        .find(|link| link.target_id == extractor.target_id)
677        .ok_or(ExtractorError::LinkNotFound)?;
678
679    // Delete the link by its ID
680    state
681        .link_service
682        .delete(&existing_link.id)
683        .await
684        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
685
686    Ok(StatusCode::NO_CONTENT.into_response())
687}
688
689/// Response for introspection endpoint
690#[derive(Debug, Serialize)]
691pub struct IntrospectionResponse {
692    pub entity_type: String,
693    pub entity_id: Uuid,
694    pub available_routes: Vec<RouteDescription>,
695}
696
697/// Description of an available route
698#[derive(Debug, Serialize)]
699pub struct RouteDescription {
700    pub path: String,
701    pub method: String,
702    pub link_type: String,
703    pub direction: String,
704    pub connected_to: String,
705    pub description: Option<String>,
706}
707
708/// Introspection: List all available link routes for an entity
709///
710/// GET /{entity_type}/{entity_id}/links
711pub async fn list_available_links(
712    State(state): State<AppState>,
713    Path((entity_type_plural, entity_id)): Path<(String, Uuid)>,
714) -> Result<Json<IntrospectionResponse>, ExtractorError> {
715    // Convert plural to singular
716    let entity_type = state
717        .config
718        .entities
719        .iter()
720        .find(|e| e.plural == entity_type_plural)
721        .map(|e| e.singular.clone())
722        .unwrap_or_else(|| entity_type_plural.clone());
723
724    // Get all routes for this entity type
725    let routes = state.registry.list_routes_for_entity(&entity_type);
726
727    let available_routes = routes
728        .iter()
729        .map(|r| RouteDescription {
730            path: format!("/{}/{}/{}", entity_type_plural, entity_id, r.route_name),
731            method: "GET".to_string(),
732            link_type: r.link_type.clone(),
733            direction: format!("{:?}", r.direction),
734            connected_to: r.connected_to.clone(),
735            description: r.description.clone(),
736        })
737        .collect();
738
739    Ok(Json(IntrospectionResponse {
740        entity_type,
741        entity_id,
742        available_routes,
743    }))
744}
745
746/// Handler générique pour GET sur chemins imbriqués illimités
747///
748/// Supporte des chemins comme:
749/// - GET /users/123/invoices/456/orders (liste les orders)
750/// - GET /users/123/invoices/456/orders/789 (get un order spécifique)
751pub async fn handle_nested_path_get(
752    State(state): State<AppState>,
753    Path(path): Path<String>,
754    Query(params): Query<QueryParams>,
755) -> Result<Json<serde_json::Value>, ExtractorError> {
756    // Parser le path en segments
757    let segments: Vec<String> = path
758        .trim_matches('/')
759        .split('/')
760        .map(|s| s.to_string())
761        .collect();
762
763    // Cette route ne gère QUE les chemins imbriqués à 3+ niveaux (5+ segments)
764    // Les chemins à 2 niveaux sont gérés par les routes spécifiques
765    if segments.len() < 5 {
766        return Err(ExtractorError::InvalidPath);
767    }
768
769    // Utiliser l'extracteur récursif
770    let extractor =
771        RecursiveLinkExtractor::from_segments(segments, &state.registry, &state.config)?;
772
773    // Si is_list, récupérer les liens depuis la dernière entité
774    if extractor.is_list {
775        // Valider toute la chaîne de liens avant de retourner les résultats
776        // Pour chaque segment avec un link_definition, vérifier que le lien existe
777        // SAUF pour le dernier segment si c'est une liste (ID = Uuid::nil())
778
779        use crate::links::registry::LinkDirection;
780
781        // VALIDATION COMPLÈTE DE LA CHAÎNE
782        for i in 0..extractor.chain.len() - 1 {
783            let current = &extractor.chain[i];
784            let next = &extractor.chain[i + 1];
785
786            // Si next.entity_id est Uuid::nil(), c'est une liste finale, on ne valide pas ce lien
787            if next.entity_id.is_nil() {
788                continue;
789            }
790
791            // Cas 1: Le segment a un link_definition → validation normale
792            if let Some(link_def) = &current.link_definition {
793                let link_exists = match current.link_direction {
794                    Some(LinkDirection::Forward) => {
795                        // Forward: current est la source, next est le target
796                        let links = state
797                            .link_service
798                            .find_by_source(
799                                &current.entity_id,
800                                Some(&link_def.link_type),
801                                Some(&link_def.target_type),
802                            )
803                            .await
804                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
805                        links.iter().any(|l| l.target_id == next.entity_id)
806                    }
807                    Some(LinkDirection::Reverse) => {
808                        // Reverse: current est le target, next est la source
809                        let links = state
810                            .link_service
811                            .find_by_target(&current.entity_id, None, Some(&link_def.link_type))
812                            .await
813                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
814                        links.iter().any(|l| l.source_id == next.entity_id)
815                    }
816                    None => {
817                        return Err(ExtractorError::InvalidPath);
818                    }
819                };
820
821                if !link_exists {
822                    return Err(ExtractorError::LinkNotFound);
823                }
824            }
825            // Cas 2: Premier segment sans link_definition mais next a un link_definition
826            // → C'est le début d'une chaîne, on doit vérifier que current est lié à next
827            else if let Some(next_link_def) = &next.link_definition {
828                let link_exists = match next.link_direction {
829                    Some(LinkDirection::Forward) => {
830                        // Forward depuis current: current → next
831                        let links = state
832                            .link_service
833                            .find_by_source(
834                                &current.entity_id,
835                                Some(&next_link_def.link_type),
836                                Some(&next_link_def.target_type),
837                            )
838                            .await
839                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
840                        links.iter().any(|l| l.target_id == next.entity_id)
841                    }
842                    Some(LinkDirection::Reverse) => {
843                        // Reverse depuis current: current ← next (donc next est source)
844                        let links = state
845                            .link_service
846                            .find_by_target(
847                                &current.entity_id,
848                                None,
849                                Some(&next_link_def.link_type),
850                            )
851                            .await
852                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
853                        links.iter().any(|l| l.source_id == next.entity_id)
854                    }
855                    None => {
856                        return Err(ExtractorError::InvalidPath);
857                    }
858                };
859
860                if !link_exists {
861                    return Err(ExtractorError::LinkNotFound);
862                }
863            }
864        }
865
866        // Toute la chaîne est valide, récupérer les liens finaux
867        if let Some(link_def) = extractor.final_link_def() {
868            // Pour une liste, on veut l'ID du segment pénultième (celui qui a le lien)
869            let penultimate = extractor
870                .penultimate_segment()
871                .ok_or(ExtractorError::InvalidPath)?;
872            let entity_id = penultimate.entity_id;
873
874            use crate::links::registry::LinkDirection;
875
876            // Récupérer les liens selon la direction
877            let (links, enrichment_context) = match penultimate.link_direction {
878                Some(LinkDirection::Forward) => {
879                    // Forward: entity_id est la source
880                    let links = state
881                        .link_service
882                        .find_by_source(
883                            &entity_id,
884                            Some(&link_def.link_type),
885                            Some(&link_def.target_type),
886                        )
887                        .await
888                        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
889                    (links, EnrichmentContext::FromSource)
890                }
891                Some(LinkDirection::Reverse) => {
892                    // Reverse: entity_id est le target, on cherche les sources
893                    let links = state
894                        .link_service
895                        .find_by_target(&entity_id, None, Some(&link_def.link_type))
896                        .await
897                        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
898                    (links, EnrichmentContext::FromTarget)
899                }
900                None => {
901                    return Err(ExtractorError::InvalidPath);
902                }
903            };
904
905            // Enrichir TOUS les liens
906            let mut all_enriched =
907                enrich_links_with_entities(&state, links, enrichment_context, link_def).await?;
908
909            // Apply filters if provided
910            if let Some(filter_value) = params.filter_value() {
911                all_enriched = apply_link_filters(all_enriched, &filter_value);
912            }
913
914            let total = all_enriched.len();
915
916            // Apply pagination (ALWAYS paginate for nested links too)
917            let page = params.page();
918            let limit = params.limit();
919            let start = (page - 1) * limit;
920
921            let paginated_links: Vec<EnrichedLink> =
922                all_enriched.into_iter().skip(start).take(limit).collect();
923
924            Ok(Json(serde_json::json!({
925                "data": paginated_links,
926                "pagination": {
927                    "page": page,
928                    "limit": limit,
929                    "total": total,
930                    "total_pages": PaginationMeta::new(page, limit, total).total_pages,
931                    "has_next": PaginationMeta::new(page, limit, total).has_next,
932                    "has_prev": PaginationMeta::new(page, limit, total).has_prev
933                },
934                "link_type": link_def.link_type,
935                "direction": format!("{:?}", penultimate.link_direction),
936                "description": link_def.description
937            })))
938        } else {
939            Err(ExtractorError::InvalidPath)
940        }
941    } else {
942        // Item spécifique - récupérer le lien spécifique
943
944        use crate::links::registry::LinkDirection;
945
946        // VALIDATION COMPLÈTE DE LA CHAÎNE (aussi pour items spécifiques)
947        for i in 0..extractor.chain.len() - 1 {
948            let current = &extractor.chain[i];
949            let next = &extractor.chain[i + 1];
950
951            // Cas 1: Le segment a un link_definition → validation normale
952            if let Some(link_def) = &current.link_definition {
953                let link_exists = match current.link_direction {
954                    Some(LinkDirection::Forward) => {
955                        let links = state
956                            .link_service
957                            .find_by_source(
958                                &current.entity_id,
959                                Some(&link_def.link_type),
960                                Some(&link_def.target_type),
961                            )
962                            .await
963                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
964                        links.iter().any(|l| l.target_id == next.entity_id)
965                    }
966                    Some(LinkDirection::Reverse) => {
967                        let links = state
968                            .link_service
969                            .find_by_target(&current.entity_id, None, Some(&link_def.link_type))
970                            .await
971                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
972                        links.iter().any(|l| l.source_id == next.entity_id)
973                    }
974                    None => {
975                        return Err(ExtractorError::InvalidPath);
976                    }
977                };
978
979                if !link_exists {
980                    return Err(ExtractorError::LinkNotFound);
981                }
982            }
983            // Cas 2: Premier segment sans link_definition mais next a un link_definition
984            else if let Some(next_link_def) = &next.link_definition {
985                let link_exists = match next.link_direction {
986                    Some(LinkDirection::Forward) => {
987                        let links = state
988                            .link_service
989                            .find_by_source(
990                                &current.entity_id,
991                                Some(&next_link_def.link_type),
992                                Some(&next_link_def.target_type),
993                            )
994                            .await
995                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
996                        links.iter().any(|l| l.target_id == next.entity_id)
997                    }
998                    Some(LinkDirection::Reverse) => {
999                        let links = state
1000                            .link_service
1001                            .find_by_target(
1002                                &current.entity_id,
1003                                None,
1004                                Some(&next_link_def.link_type),
1005                            )
1006                            .await
1007                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1008                        links.iter().any(|l| l.source_id == next.entity_id)
1009                    }
1010                    None => {
1011                        return Err(ExtractorError::InvalidPath);
1012                    }
1013                };
1014
1015                if !link_exists {
1016                    return Err(ExtractorError::LinkNotFound);
1017                }
1018            }
1019        }
1020
1021        // Toute la chaîne est validée, récupérer le lien final
1022        if let Some(link_def) = extractor.final_link_def() {
1023            let (target_id, _) = extractor.final_target();
1024            let penultimate = extractor.penultimate_segment().unwrap();
1025
1026            // Récupérer le lien selon la direction
1027            let link = match penultimate.link_direction {
1028                Some(LinkDirection::Forward) => {
1029                    // Forward: penultimate est la source, target_id est le target
1030                    let links = state
1031                        .link_service
1032                        .find_by_source(
1033                            &penultimate.entity_id,
1034                            Some(&link_def.link_type),
1035                            Some(&link_def.target_type),
1036                        )
1037                        .await
1038                        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1039
1040                    links
1041                        .into_iter()
1042                        .find(|l| l.target_id == target_id)
1043                        .ok_or(ExtractorError::LinkNotFound)?
1044                }
1045                Some(LinkDirection::Reverse) => {
1046                    // Reverse: penultimate est le target, target_id est la source
1047                    let links = state
1048                        .link_service
1049                        .find_by_target(&penultimate.entity_id, None, Some(&link_def.link_type))
1050                        .await
1051                        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1052
1053                    links
1054                        .into_iter()
1055                        .find(|l| l.source_id == target_id)
1056                        .ok_or(ExtractorError::LinkNotFound)?
1057                }
1058                None => {
1059                    return Err(ExtractorError::InvalidPath);
1060                }
1061            };
1062
1063            // Enrichir le lien
1064            let enriched = enrich_links_with_entities(
1065                &state,
1066                vec![link],
1067                EnrichmentContext::DirectLink,
1068                link_def,
1069            )
1070            .await?;
1071
1072            Ok(Json(serde_json::json!({
1073                "link": enriched.first()
1074            })))
1075        } else {
1076            Err(ExtractorError::InvalidPath)
1077        }
1078    }
1079}
1080
1081/// Handler générique pour POST sur chemins imbriqués
1082///
1083/// Supporte des chemins comme:
1084/// - POST /users/123/invoices/456/orders (crée un nouvel order + link)
1085pub async fn handle_nested_path_post(
1086    State(state): State<AppState>,
1087    Path(path): Path<String>,
1088    Json(payload): Json<CreateLinkedEntityRequest>,
1089) -> Result<Response, ExtractorError> {
1090    let segments: Vec<String> = path
1091        .trim_matches('/')
1092        .split('/')
1093        .map(|s| s.to_string())
1094        .collect();
1095
1096    // Cette route ne gère QUE les chemins imbriqués à 3+ niveaux (5+ segments)
1097    // Les chemins à 2 niveaux sont gérés par les routes spécifiques
1098    if segments.len() < 5 {
1099        return Err(ExtractorError::InvalidPath);
1100    }
1101
1102    let extractor =
1103        RecursiveLinkExtractor::from_segments(segments, &state.registry, &state.config)?;
1104
1105    // Récupérer le dernier lien
1106    let link_def = extractor
1107        .final_link_def()
1108        .ok_or(ExtractorError::InvalidPath)?;
1109
1110    let (source_id, _) = extractor.final_target();
1111    let target_entity_type = &link_def.target_type;
1112
1113    // Récupérer le creator pour l'entité target
1114    let entity_creator = state
1115        .entity_creators
1116        .get(target_entity_type)
1117        .ok_or_else(|| {
1118            ExtractorError::JsonError(format!(
1119                "No entity creator registered for type: {}",
1120                target_entity_type
1121            ))
1122        })?;
1123
1124    // Créer la nouvelle entité
1125    let created_entity = entity_creator
1126        .create_from_json(payload.entity)
1127        .await
1128        .map_err(|e| ExtractorError::JsonError(format!("Failed to create entity: {}", e)))?;
1129
1130    // Extraire l'ID de l'entité créée
1131    let target_entity_id = created_entity["id"].as_str().ok_or_else(|| {
1132        ExtractorError::JsonError("Created entity missing 'id' field".to_string())
1133    })?;
1134    let target_entity_id = Uuid::parse_str(target_entity_id)
1135        .map_err(|e| ExtractorError::JsonError(format!("Invalid UUID in created entity: {}", e)))?;
1136
1137    // Créer le lien
1138    let link = LinkEntity::new(
1139        link_def.link_type.clone(),
1140        source_id,
1141        target_entity_id,
1142        payload.metadata,
1143    );
1144
1145    let created_link = state
1146        .link_service
1147        .create(link)
1148        .await
1149        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1150
1151    let response = serde_json::json!({
1152        "entity": created_entity,
1153        "link": created_link,
1154    });
1155
1156    Ok((StatusCode::CREATED, Json(response)).into_response())
1157}
1158
1159#[cfg(test)]
1160mod tests {
1161    use super::*;
1162    use crate::config::EntityConfig;
1163    use crate::core::LinkDefinition;
1164    use crate::storage::InMemoryLinkService;
1165
1166    fn create_test_state() -> AppState {
1167        let config = Arc::new(LinksConfig {
1168            entities: vec![
1169                EntityConfig {
1170                    singular: "user".to_string(),
1171                    plural: "users".to_string(),
1172                    auth: crate::config::EntityAuthConfig::default(),
1173                },
1174                EntityConfig {
1175                    singular: "car".to_string(),
1176                    plural: "cars".to_string(),
1177                    auth: crate::config::EntityAuthConfig::default(),
1178                },
1179            ],
1180            links: vec![LinkDefinition {
1181                link_type: "owner".to_string(),
1182                source_type: "user".to_string(),
1183                target_type: "car".to_string(),
1184                forward_route_name: "cars-owned".to_string(),
1185                reverse_route_name: "users-owners".to_string(),
1186                description: Some("User owns a car".to_string()),
1187                required_fields: None,
1188                auth: None,
1189            }],
1190            validation_rules: None,
1191        });
1192
1193        let registry = Arc::new(LinkRouteRegistry::new(config.clone()));
1194        let link_service: Arc<dyn LinkService> = Arc::new(InMemoryLinkService::new());
1195
1196        AppState {
1197            link_service,
1198            config,
1199            registry,
1200            entity_fetchers: Arc::new(HashMap::new()),
1201            entity_creators: Arc::new(HashMap::new()),
1202        }
1203    }
1204
1205    #[test]
1206    fn test_state_creation() {
1207        let state = create_test_state();
1208        assert_eq!(state.config.entities.len(), 2);
1209        assert_eq!(state.config.links.len(), 1);
1210    }
1211}