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//! - Extract tenant IDs from headers
6//! - Parse link routes and resolve definitions
7
8use axum::http::StatusCode;
9use axum::response::{IntoResponse, Response};
10use axum::Json;
11use uuid::Uuid;
12
13use crate::config::LinksConfig;
14use crate::core::{EntityReference, LinkDefinition};
15use crate::links::registry::{LinkDirection, LinkRouteRegistry};
16
17/// Extract tenant ID from request headers
18///
19/// Expected header: `X-Tenant-ID: <uuid>`
20pub fn extract_tenant_id(headers: &axum::http::HeaderMap) -> Result<Uuid, ExtractorError> {
21    let tenant_id_str = headers
22        .get("X-Tenant-ID")
23        .ok_or(ExtractorError::MissingTenantId)?
24        .to_str()
25        .map_err(|_| ExtractorError::InvalidTenantId)?;
26
27    Uuid::parse_str(tenant_id_str).map_err(|_| ExtractorError::InvalidTenantId)
28}
29
30/// Errors that can occur during extraction
31#[derive(Debug, Clone)]
32pub enum ExtractorError {
33    MissingTenantId,
34    InvalidTenantId,
35    InvalidPath,
36    InvalidEntityId,
37    RouteNotFound(String),
38    LinkNotFound,
39    JsonError(String),
40}
41
42impl std::fmt::Display for ExtractorError {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            ExtractorError::MissingTenantId => write!(f, "Missing X-Tenant-ID header"),
46            ExtractorError::InvalidTenantId => write!(f, "Invalid tenant ID format"),
47            ExtractorError::InvalidPath => write!(f, "Invalid path format"),
48            ExtractorError::InvalidEntityId => write!(f, "Invalid entity ID format"),
49            ExtractorError::RouteNotFound(route) => write!(f, "Route not found: {}", route),
50            ExtractorError::LinkNotFound => write!(f, "Link not found"),
51            ExtractorError::JsonError(msg) => write!(f, "JSON error: {}", msg),
52        }
53    }
54}
55
56impl std::error::Error for ExtractorError {}
57
58impl IntoResponse for ExtractorError {
59    fn into_response(self) -> Response {
60        let (status, message) = match self {
61            ExtractorError::MissingTenantId => (StatusCode::BAD_REQUEST, self.to_string()),
62            ExtractorError::InvalidTenantId => (StatusCode::BAD_REQUEST, self.to_string()),
63            ExtractorError::InvalidPath => (StatusCode::BAD_REQUEST, self.to_string()),
64            ExtractorError::InvalidEntityId => (StatusCode::BAD_REQUEST, self.to_string()),
65            ExtractorError::RouteNotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
66            ExtractorError::LinkNotFound => (StatusCode::NOT_FOUND, self.to_string()),
67            ExtractorError::JsonError(_) => (StatusCode::BAD_REQUEST, self.to_string()),
68        };
69
70        (status, Json(serde_json::json!({ "error": message }))).into_response()
71    }
72}
73
74/// Extractor for link information from path
75///
76/// Automatically parses the path and resolves link definitions.
77/// Supports both forward and reverse navigation.
78#[derive(Debug, Clone)]
79pub struct LinkExtractor {
80    pub tenant_id: Uuid,
81    pub entity_id: Uuid,
82    pub entity_type: String,
83    pub link_definition: LinkDefinition,
84    pub direction: LinkDirection,
85}
86
87impl LinkExtractor {
88    /// Parse a link route path
89    ///
90    /// Expected format: `/{entity_type}/{entity_id}/{route_name}`
91    /// Example: `/users/123.../cars-owned`
92    pub fn from_path_and_registry(
93        path_parts: (String, Uuid, String),
94        registry: &LinkRouteRegistry,
95        config: &LinksConfig,
96        tenant_id: Uuid,
97    ) -> Result<Self, ExtractorError> {
98        let (entity_type_plural, entity_id, route_name) = path_parts;
99
100        // Convert plural to singular
101        let entity_type = config
102            .entities
103            .iter()
104            .find(|e| e.plural == entity_type_plural)
105            .map(|e| e.singular.clone())
106            .unwrap_or(entity_type_plural);
107
108        // Resolve the route
109        let (link_definition, direction) = registry
110            .resolve_route(&entity_type, &route_name)
111            .map_err(|_| ExtractorError::RouteNotFound(route_name.clone()))?;
112
113        Ok(Self {
114            tenant_id,
115            entity_id,
116            entity_type,
117            link_definition,
118            direction,
119        })
120    }
121}
122
123/// Extractor for direct link creation/deletion/update
124///
125/// NEW Format: `/{source_type}/{source_id}/{route_name}/{target_id}`
126/// Example: `/users/123.../cars-owned/456...`
127///
128/// This uses the route_name (e.g., "cars-owned") instead of link_type (e.g., "owner")
129/// to provide more semantic and RESTful URLs.
130#[derive(Debug, Clone)]
131pub struct DirectLinkExtractor {
132    pub tenant_id: Uuid,
133    pub source: EntityReference,
134    pub target: EntityReference,
135    pub link_definition: LinkDefinition,
136    pub direction: LinkDirection,
137}
138
139impl DirectLinkExtractor {
140    /// Parse a direct link path using route_name
141    ///
142    /// NEW: path_parts = (source_type_plural, source_id, route_name, target_id)
143    ///
144    /// The route_name is resolved to a link definition using the LinkRouteRegistry,
145    /// which handles both forward and reverse navigation automatically.
146    pub fn from_path(
147        path_parts: (String, Uuid, String, Uuid),
148        registry: &LinkRouteRegistry,
149        config: &LinksConfig,
150        tenant_id: Uuid,
151    ) -> Result<Self, ExtractorError> {
152        let (source_type_plural, source_id, route_name, target_id) = path_parts;
153
154        // Convert plural to singular
155        let source_type = config
156            .entities
157            .iter()
158            .find(|e| e.plural == source_type_plural)
159            .map(|e| e.singular.clone())
160            .unwrap_or(source_type_plural);
161
162        // Resolve the route to get link definition and direction
163        let (link_definition, direction) = registry
164            .resolve_route(&source_type, &route_name)
165            .map_err(|_| ExtractorError::RouteNotFound(route_name.clone()))?;
166
167        // Determine target type based on direction
168        let target_type = match direction {
169            LinkDirection::Forward => link_definition.target_type.clone(),
170            LinkDirection::Reverse => link_definition.source_type.clone(),
171        };
172
173        let source = EntityReference::new(source_id, source_type);
174        let target = EntityReference::new(target_id, target_type);
175
176        Ok(Self {
177            tenant_id,
178            source,
179            target,
180            link_definition,
181            direction,
182        })
183    }
184}