1use 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#[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#[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 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 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 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#[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 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 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 let (link_definition, direction) = registry
141 .resolve_route(&source_type, &route_name)
142 .map_err(|_| ExtractorError::RouteNotFound(route_name.clone()))?;
143
144 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#[derive(Debug, Clone, serde::Serialize)]
163pub struct LinkPathSegment {
164 pub entity_type: String,
166 pub entity_id: Uuid,
168 pub route_name: Option<String>,
170 pub link_definition: Option<LinkDefinition>,
172 #[serde(skip_serializing)]
174 pub link_direction: Option<LinkDirection>,
175}
176
177#[derive(Debug, Clone)]
183pub struct RecursiveLinkExtractor {
184 pub chain: Vec<LinkPathSegment>,
185 pub is_list: bool,
188}
189
190impl RecursiveLinkExtractor {
191 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 while i < segments.len() {
208 let entity_type_singular = if let Some(ref entity_type) = current_entity_type {
210 entity_type.clone()
212 } else {
213 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 current_entity_type = None;
227
228 let entity_id = if i < segments.len() {
230 segments[i]
231 .parse::<Uuid>()
232 .map_err(|_| ExtractorError::InvalidEntityId)?
233 } else {
234 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 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 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 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 if let Some(final_entity_type) = current_entity_type {
288 chain.push(LinkPathSegment {
289 entity_type: final_entity_type,
290 entity_id: Uuid::nil(), route_name: None,
292 link_definition: None,
293 link_direction: None,
294 });
295 }
296
297 let is_list = (segments.len() % 2 == 1) && (segments.len() >= 5);
302
303 Ok(Self { chain, is_list })
304 }
305
306 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 pub fn final_link_def(&self) -> Option<&LinkDefinition> {
314 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 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}