1use axum::extract::{FromRequest, Path, Request};
9use axum::http::StatusCode;
10use axum::response::{IntoResponse, Response};
11use axum::Json;
12use serde::{Deserialize, Serialize};
13use std::sync::Arc;
14use uuid::Uuid;
15
16use crate::config::LinksConfig;
17use crate::core::{EntityReference, Link, LinkDefinition};
18use crate::links::registry::{LinkDirection, LinkRouteRegistry};
19
20pub fn extract_tenant_id(headers: &axum::http::HeaderMap) -> Result<Uuid, ExtractorError> {
24 let tenant_id_str = headers
25 .get("X-Tenant-ID")
26 .ok_or(ExtractorError::MissingTenantId)?
27 .to_str()
28 .map_err(|_| ExtractorError::InvalidTenantId)?;
29
30 Uuid::parse_str(tenant_id_str).map_err(|_| ExtractorError::InvalidTenantId)
31}
32
33#[derive(Debug, Clone)]
35pub enum ExtractorError {
36 MissingTenantId,
37 InvalidTenantId,
38 InvalidPath,
39 InvalidEntityId,
40 RouteNotFound(String),
41 LinkNotFound,
42 JsonError(String),
43}
44
45impl std::fmt::Display for ExtractorError {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 match self {
48 ExtractorError::MissingTenantId => write!(f, "Missing X-Tenant-ID header"),
49 ExtractorError::InvalidTenantId => write!(f, "Invalid tenant ID format"),
50 ExtractorError::InvalidPath => write!(f, "Invalid path format"),
51 ExtractorError::InvalidEntityId => write!(f, "Invalid entity ID format"),
52 ExtractorError::RouteNotFound(route) => write!(f, "Route not found: {}", route),
53 ExtractorError::LinkNotFound => write!(f, "Link not found"),
54 ExtractorError::JsonError(msg) => write!(f, "JSON error: {}", msg),
55 }
56 }
57}
58
59impl std::error::Error for ExtractorError {}
60
61impl IntoResponse for ExtractorError {
62 fn into_response(self) -> Response {
63 let (status, message) = match self {
64 ExtractorError::MissingTenantId => (StatusCode::BAD_REQUEST, self.to_string()),
65 ExtractorError::InvalidTenantId => (StatusCode::BAD_REQUEST, self.to_string()),
66 ExtractorError::InvalidPath => (StatusCode::BAD_REQUEST, self.to_string()),
67 ExtractorError::InvalidEntityId => (StatusCode::BAD_REQUEST, self.to_string()),
68 ExtractorError::RouteNotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
69 ExtractorError::LinkNotFound => (StatusCode::NOT_FOUND, self.to_string()),
70 ExtractorError::JsonError(_) => (StatusCode::BAD_REQUEST, self.to_string()),
71 };
72
73 (status, Json(serde_json::json!({ "error": message }))).into_response()
74 }
75}
76
77#[derive(Debug, Clone)]
82pub struct LinkExtractor {
83 pub tenant_id: Uuid,
84 pub entity_id: Uuid,
85 pub entity_type: String,
86 pub link_definition: LinkDefinition,
87 pub direction: LinkDirection,
88}
89
90impl LinkExtractor {
91 pub fn from_path_and_registry(
96 path_parts: (String, Uuid, String),
97 registry: &LinkRouteRegistry,
98 config: &LinksConfig,
99 tenant_id: Uuid,
100 ) -> Result<Self, ExtractorError> {
101 let (entity_type_plural, entity_id, route_name) = path_parts;
102
103 let entity_type = config
105 .entities
106 .iter()
107 .find(|e| e.plural == entity_type_plural)
108 .map(|e| e.singular.clone())
109 .unwrap_or(entity_type_plural);
110
111 let (link_definition, direction) = registry
113 .resolve_route(&entity_type, &route_name)
114 .map_err(|_| ExtractorError::RouteNotFound(route_name.clone()))?;
115
116 Ok(Self {
117 tenant_id,
118 entity_id,
119 entity_type,
120 link_definition,
121 direction,
122 })
123 }
124}
125
126#[derive(Debug, Clone)]
130pub struct DirectLinkExtractor {
131 pub tenant_id: Uuid,
132 pub link_type: String,
133 pub source: EntityReference,
134 pub target: EntityReference,
135 pub link_definition: Option<LinkDefinition>,
136}
137
138impl DirectLinkExtractor {
139 pub fn from_path(
141 path_parts: (String, Uuid, String, String, Uuid),
142 config: &LinksConfig,
143 tenant_id: Uuid,
144 ) -> Result<Self, ExtractorError> {
145 let (source_type_plural, source_id, link_type, target_type_plural, target_id) = path_parts;
146
147 let source_type = config
149 .entities
150 .iter()
151 .find(|e| e.plural == source_type_plural)
152 .map(|e| e.singular.clone())
153 .unwrap_or(source_type_plural);
154
155 let target_type = config
156 .entities
157 .iter()
158 .find(|e| e.plural == target_type_plural)
159 .map(|e| e.singular.clone())
160 .unwrap_or(target_type_plural);
161
162 let source = EntityReference::new(source_id, source_type.clone());
163 let target = EntityReference::new(target_id, target_type.clone());
164
165 let link_definition = config
167 .links
168 .iter()
169 .find(|def| {
170 def.link_type == link_type
171 && def.source_type == source_type
172 && def.target_type == target_type
173 })
174 .cloned();
175
176 Ok(Self {
177 tenant_id,
178 link_type,
179 source,
180 target,
181 link_definition,
182 })
183 }
184}