1use axum::http::StatusCode;
8use axum::response::{IntoResponse, Response};
9use axum::Json;
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}