1use 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
17pub 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#[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#[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 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 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 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#[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 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 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 let (link_definition, direction) = registry
164 .resolve_route(&source_type, &route_name)
165 .map_err(|_| ExtractorError::RouteNotFound(route_name.clone()))?;
166
167 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}