1use axum::{
7 Json,
8 extract::{Path, State},
9 http::StatusCode,
10 response::{IntoResponse, Response},
11};
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::sync::Arc;
16use uuid::Uuid;
17
18use crate::config::LinksConfig;
19use crate::core::extractors::{DirectLinkExtractor, ExtractorError, LinkExtractor};
20use crate::core::{EntityCreator, EntityFetcher, LinkDefinition, LinkService, link::LinkEntity};
21use crate::links::registry::{LinkDirection, LinkRouteRegistry};
22
23#[derive(Clone)]
25pub struct AppState {
26 pub link_service: Arc<dyn LinkService>,
27 pub config: Arc<LinksConfig>,
28 pub registry: Arc<LinkRouteRegistry>,
29 pub entity_fetchers: Arc<HashMap<String, Arc<dyn EntityFetcher>>>,
31 pub entity_creators: Arc<HashMap<String, Arc<dyn EntityCreator>>>,
33}
34
35impl AppState {
36 pub fn get_link_auth_policy(
38 link_definition: &LinkDefinition,
39 operation: &str,
40 ) -> Option<String> {
41 link_definition.auth.as_ref().map(|auth| match operation {
42 "list" => auth.list.clone(),
43 "get" => auth.get.clone(),
44 "create" => auth.create.clone(),
45 "update" => auth.update.clone(),
46 "delete" => auth.delete.clone(),
47 _ => "authenticated".to_string(),
48 })
49 }
50}
51
52#[derive(Debug, Serialize)]
54pub struct ListLinksResponse {
55 pub links: Vec<LinkEntity>,
56 pub count: usize,
57 pub link_type: String,
58 pub direction: String,
59 pub description: Option<String>,
60}
61
62#[derive(Debug, Serialize)]
64pub struct EnrichedLink {
65 pub id: Uuid,
67
68 #[serde(rename = "type")]
70 pub entity_type: String,
71
72 pub link_type: String,
74
75 pub source_id: Uuid,
77
78 pub target_id: Uuid,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub source: Option<serde_json::Value>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub target: Option<serde_json::Value>,
88
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub metadata: Option<serde_json::Value>,
92
93 pub created_at: DateTime<Utc>,
95
96 pub updated_at: DateTime<Utc>,
98
99 pub status: String,
101}
102
103#[derive(Debug, Serialize)]
105pub struct EnrichedListLinksResponse {
106 pub links: Vec<EnrichedLink>,
107 pub count: usize,
108 pub link_type: String,
109 pub direction: String,
110 pub description: Option<String>,
111}
112
113#[derive(Debug, Deserialize)]
115pub struct CreateLinkRequest {
116 pub metadata: Option<serde_json::Value>,
117}
118
119#[derive(Debug, Deserialize)]
121pub struct CreateLinkedEntityRequest {
122 pub entity: serde_json::Value,
123 pub metadata: Option<serde_json::Value>,
124}
125
126#[derive(Debug, Clone, Copy)]
128enum EnrichmentContext {
129 FromSource,
131 FromTarget,
133 DirectLink,
135}
136
137pub async fn list_links(
141 State(state): State<AppState>,
142 Path((entity_type_plural, entity_id, route_name)): Path<(String, Uuid, String)>,
143) -> Result<Json<EnrichedListLinksResponse>, ExtractorError> {
144 let extractor = LinkExtractor::from_path_and_registry(
145 (entity_type_plural, entity_id, route_name),
146 &state.registry,
147 &state.config,
148 )?;
149
150 let links = match extractor.direction {
152 LinkDirection::Forward => state
153 .link_service
154 .find_by_source(
155 &extractor.entity_id,
156 Some(&extractor.link_definition.link_type),
157 Some(&extractor.link_definition.target_type),
158 )
159 .await
160 .map_err(|e| ExtractorError::JsonError(e.to_string()))?,
161 LinkDirection::Reverse => state
162 .link_service
163 .find_by_target(
164 &extractor.entity_id,
165 Some(&extractor.link_definition.link_type),
166 Some(&extractor.link_definition.source_type),
167 )
168 .await
169 .map_err(|e| ExtractorError::JsonError(e.to_string()))?,
170 };
171
172 let context = match extractor.direction {
174 LinkDirection::Forward => EnrichmentContext::FromSource,
175 LinkDirection::Reverse => EnrichmentContext::FromTarget,
176 };
177
178 let enriched_links =
180 enrich_links_with_entities(&state, links, context, &extractor.link_definition).await?;
181
182 Ok(Json(EnrichedListLinksResponse {
183 count: enriched_links.len(),
184 links: enriched_links,
185 link_type: extractor.link_definition.link_type,
186 direction: format!("{:?}", extractor.direction),
187 description: extractor.link_definition.description,
188 }))
189}
190
191async fn enrich_links_with_entities(
193 state: &AppState,
194 links: Vec<LinkEntity>,
195 context: EnrichmentContext,
196 link_definition: &LinkDefinition,
197) -> Result<Vec<EnrichedLink>, ExtractorError> {
198 let mut enriched = Vec::new();
199
200 for link in links {
201 let source_entity = match context {
203 EnrichmentContext::FromSource => None,
204 EnrichmentContext::FromTarget | EnrichmentContext::DirectLink => {
205 fetch_entity_by_type(state, &link_definition.source_type, &link.source_id)
207 .await
208 .ok()
209 }
210 };
211
212 let target_entity = match context {
214 EnrichmentContext::FromTarget => None,
215 EnrichmentContext::FromSource | EnrichmentContext::DirectLink => {
216 fetch_entity_by_type(state, &link_definition.target_type, &link.target_id)
218 .await
219 .ok()
220 }
221 };
222
223 enriched.push(EnrichedLink {
224 id: link.id,
225 entity_type: link.entity_type,
226 link_type: link.link_type,
227 source_id: link.source_id,
228 target_id: link.target_id,
229 source: source_entity,
230 target: target_entity,
231 metadata: link.metadata,
232 created_at: link.created_at,
233 updated_at: link.updated_at,
234 status: link.status,
235 });
236 }
237
238 Ok(enriched)
239}
240
241async fn fetch_entity_by_type(
243 state: &AppState,
244 entity_type: &str,
245 entity_id: &Uuid,
246) -> Result<serde_json::Value, ExtractorError> {
247 let fetcher = state.entity_fetchers.get(entity_type).ok_or_else(|| {
248 ExtractorError::JsonError(format!(
249 "No entity fetcher registered for type: {}",
250 entity_type
251 ))
252 })?;
253
254 fetcher
255 .fetch_as_json(entity_id)
256 .await
257 .map_err(|e| ExtractorError::JsonError(format!("Failed to fetch entity: {}", e)))
258}
259
260pub async fn get_link(
264 State(state): State<AppState>,
265 Path(link_id): Path<Uuid>,
266) -> Result<Response, ExtractorError> {
267 let link = state
268 .link_service
269 .get(&link_id)
270 .await
271 .map_err(|e| ExtractorError::JsonError(e.to_string()))?
272 .ok_or(ExtractorError::LinkNotFound)?;
273
274 let link_definition = state
276 .config
277 .links
278 .iter()
279 .find(|def| def.link_type == link.link_type)
280 .ok_or_else(|| {
281 ExtractorError::JsonError(format!(
282 "No link definition found for link_type: {}",
283 link.link_type
284 ))
285 })?;
286
287 let enriched_links = enrich_links_with_entities(
289 &state,
290 vec![link],
291 EnrichmentContext::DirectLink,
292 link_definition,
293 )
294 .await?;
295
296 let enriched_link = enriched_links
297 .into_iter()
298 .next()
299 .ok_or(ExtractorError::LinkNotFound)?;
300
301 Ok(Json(enriched_link).into_response())
302}
303
304pub async fn get_link_by_route(
308 State(state): State<AppState>,
309 Path((source_type_plural, source_id, route_name, target_id)): Path<(
310 String,
311 Uuid,
312 String,
313 Uuid,
314 )>,
315) -> Result<Response, ExtractorError> {
316 let extractor = DirectLinkExtractor::from_path(
317 (source_type_plural, source_id, route_name, target_id),
318 &state.registry,
319 &state.config,
320 )?;
321
322 let existing_links = match extractor.direction {
324 LinkDirection::Forward => {
325 state
327 .link_service
328 .find_by_source(
329 &extractor.source_id,
330 Some(&extractor.link_definition.link_type),
331 Some(&extractor.target_type),
332 )
333 .await
334 .map_err(|e| ExtractorError::JsonError(e.to_string()))?
335 }
336 LinkDirection::Reverse => {
337 state
339 .link_service
340 .find_by_source(
341 &extractor.target_id,
342 Some(&extractor.link_definition.link_type),
343 Some(&extractor.source_type),
344 )
345 .await
346 .map_err(|e| ExtractorError::JsonError(e.to_string()))?
347 }
348 };
349
350 let link = existing_links
351 .into_iter()
352 .find(|link| match extractor.direction {
353 LinkDirection::Forward => link.target_id == extractor.target_id,
354 LinkDirection::Reverse => link.target_id == extractor.source_id,
355 })
356 .ok_or(ExtractorError::LinkNotFound)?;
357
358 let enriched_links = enrich_links_with_entities(
360 &state,
361 vec![link],
362 EnrichmentContext::DirectLink,
363 &extractor.link_definition,
364 )
365 .await?;
366
367 let enriched_link = enriched_links
368 .into_iter()
369 .next()
370 .ok_or(ExtractorError::LinkNotFound)?;
371
372 Ok(Json(enriched_link).into_response())
373}
374
375pub async fn create_link(
380 State(state): State<AppState>,
381 Path((source_type_plural, source_id, route_name, target_id)): Path<(
382 String,
383 Uuid,
384 String,
385 Uuid,
386 )>,
387 Json(payload): Json<CreateLinkRequest>,
388) -> Result<Response, ExtractorError> {
389 let extractor = DirectLinkExtractor::from_path(
390 (source_type_plural, source_id, route_name, target_id),
391 &state.registry,
392 &state.config,
393 )?;
394
395 let link = LinkEntity::new(
397 extractor.link_definition.link_type,
398 extractor.source_id,
399 extractor.target_id,
400 payload.metadata,
401 );
402
403 let created_link = state
404 .link_service
405 .create(link)
406 .await
407 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
408
409 Ok((StatusCode::CREATED, Json(created_link)).into_response())
410}
411
412pub async fn create_linked_entity(
417 State(state): State<AppState>,
418 Path((source_type_plural, source_id, route_name)): Path<(String, Uuid, String)>,
419 Json(payload): Json<CreateLinkedEntityRequest>,
420) -> Result<Response, ExtractorError> {
421 let extractor = LinkExtractor::from_path_and_registry(
422 (source_type_plural.clone(), source_id, route_name.clone()),
423 &state.registry,
424 &state.config,
425 )?;
426
427 let (source_entity_id, target_entity_type) = match extractor.direction {
429 LinkDirection::Forward => {
430 (extractor.entity_id, &extractor.link_definition.target_type)
432 }
433 LinkDirection::Reverse => {
434 (extractor.entity_id, &extractor.link_definition.source_type)
436 }
437 };
438
439 let entity_creator = state
441 .entity_creators
442 .get(target_entity_type)
443 .ok_or_else(|| {
444 ExtractorError::JsonError(format!(
445 "No entity creator registered for type: {}",
446 target_entity_type
447 ))
448 })?;
449
450 let created_entity = entity_creator
452 .create_from_json(payload.entity)
453 .await
454 .map_err(|e| ExtractorError::JsonError(format!("Failed to create entity: {}", e)))?;
455
456 let target_entity_id = created_entity["id"].as_str().ok_or_else(|| {
458 ExtractorError::JsonError("Created entity missing 'id' field".to_string())
459 })?;
460 let target_entity_id = Uuid::parse_str(target_entity_id)
461 .map_err(|e| ExtractorError::JsonError(format!("Invalid UUID in created entity: {}", e)))?;
462
463 let link = match extractor.direction {
465 LinkDirection::Forward => {
466 LinkEntity::new(
468 extractor.link_definition.link_type,
469 source_entity_id,
470 target_entity_id,
471 payload.metadata,
472 )
473 }
474 LinkDirection::Reverse => {
475 LinkEntity::new(
477 extractor.link_definition.link_type,
478 target_entity_id,
479 source_entity_id,
480 payload.metadata,
481 )
482 }
483 };
484
485 let created_link = state
486 .link_service
487 .create(link)
488 .await
489 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
490
491 let response = serde_json::json!({
493 "entity": created_entity,
494 "link": created_link,
495 });
496
497 Ok((StatusCode::CREATED, Json(response)).into_response())
498}
499
500pub async fn update_link(
504 State(state): State<AppState>,
505 Path((source_type_plural, source_id, route_name, target_id)): Path<(
506 String,
507 Uuid,
508 String,
509 Uuid,
510 )>,
511 Json(payload): Json<CreateLinkRequest>,
512) -> Result<Response, ExtractorError> {
513 let extractor = DirectLinkExtractor::from_path(
514 (source_type_plural, source_id, route_name, target_id),
515 &state.registry,
516 &state.config,
517 )?;
518
519 let existing_links = state
521 .link_service
522 .find_by_source(
523 &extractor.source_id,
524 Some(&extractor.link_definition.link_type),
525 Some(&extractor.target_type),
526 )
527 .await
528 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
529
530 let mut existing_link = existing_links
531 .into_iter()
532 .find(|link| link.target_id == extractor.target_id)
533 .ok_or_else(|| ExtractorError::RouteNotFound("Link not found".to_string()))?;
534
535 existing_link.metadata = payload.metadata;
537 existing_link.touch();
538
539 let link_id = existing_link.id;
541 let updated_link = state
542 .link_service
543 .update(&link_id, existing_link)
544 .await
545 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
546
547 Ok(Json(updated_link).into_response())
548}
549
550pub async fn delete_link(
554 State(state): State<AppState>,
555 Path((source_type_plural, source_id, route_name, target_id)): Path<(
556 String,
557 Uuid,
558 String,
559 Uuid,
560 )>,
561) -> Result<Response, ExtractorError> {
562 let extractor = DirectLinkExtractor::from_path(
563 (source_type_plural, source_id, route_name, target_id),
564 &state.registry,
565 &state.config,
566 )?;
567
568 let existing_links = state
570 .link_service
571 .find_by_source(
572 &extractor.source_id,
573 Some(&extractor.link_definition.link_type),
574 Some(&extractor.target_type),
575 )
576 .await
577 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
578
579 let existing_link = existing_links
580 .into_iter()
581 .find(|link| link.target_id == extractor.target_id)
582 .ok_or(ExtractorError::LinkNotFound)?;
583
584 state
586 .link_service
587 .delete(&existing_link.id)
588 .await
589 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
590
591 Ok(StatusCode::NO_CONTENT.into_response())
592}
593
594#[derive(Debug, Serialize)]
596pub struct IntrospectionResponse {
597 pub entity_type: String,
598 pub entity_id: Uuid,
599 pub available_routes: Vec<RouteDescription>,
600}
601
602#[derive(Debug, Serialize)]
604pub struct RouteDescription {
605 pub path: String,
606 pub method: String,
607 pub link_type: String,
608 pub direction: String,
609 pub connected_to: String,
610 pub description: Option<String>,
611}
612
613pub async fn list_available_links(
617 State(state): State<AppState>,
618 Path((entity_type_plural, entity_id)): Path<(String, Uuid)>,
619) -> Result<Json<IntrospectionResponse>, ExtractorError> {
620 let entity_type = state
622 .config
623 .entities
624 .iter()
625 .find(|e| e.plural == entity_type_plural)
626 .map(|e| e.singular.clone())
627 .unwrap_or_else(|| entity_type_plural.clone());
628
629 let routes = state.registry.list_routes_for_entity(&entity_type);
631
632 let available_routes = routes
633 .iter()
634 .map(|r| RouteDescription {
635 path: format!("/{}/{}/{}", entity_type_plural, entity_id, r.route_name),
636 method: "GET".to_string(),
637 link_type: r.link_type.clone(),
638 direction: format!("{:?}", r.direction),
639 connected_to: r.connected_to.clone(),
640 description: r.description.clone(),
641 })
642 .collect();
643
644 Ok(Json(IntrospectionResponse {
645 entity_type,
646 entity_id,
647 available_routes,
648 }))
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654 use crate::config::EntityConfig;
655 use crate::core::LinkDefinition;
656 use crate::storage::InMemoryLinkService;
657
658 fn create_test_state() -> AppState {
659 let config = Arc::new(LinksConfig {
660 entities: vec![
661 EntityConfig {
662 singular: "user".to_string(),
663 plural: "users".to_string(),
664 auth: crate::config::EntityAuthConfig::default(),
665 },
666 EntityConfig {
667 singular: "car".to_string(),
668 plural: "cars".to_string(),
669 auth: crate::config::EntityAuthConfig::default(),
670 },
671 ],
672 links: vec![LinkDefinition {
673 link_type: "owner".to_string(),
674 source_type: "user".to_string(),
675 target_type: "car".to_string(),
676 forward_route_name: "cars-owned".to_string(),
677 reverse_route_name: "users-owners".to_string(),
678 description: Some("User owns a car".to_string()),
679 required_fields: None,
680 auth: None,
681 }],
682 validation_rules: None,
683 });
684
685 let registry = Arc::new(LinkRouteRegistry::new(config.clone()));
686 let link_service: Arc<dyn LinkService> = Arc::new(InMemoryLinkService::new());
687
688 AppState {
689 link_service,
690 config,
691 registry,
692 entity_fetchers: Arc::new(HashMap::new()),
693 entity_creators: Arc::new(HashMap::new()),
694 }
695 }
696
697 #[test]
698 fn test_state_creation() {
699 let state = create_test_state();
700 assert_eq!(state.config.entities.len(), 2);
701 assert_eq!(state.config.links.len(), 1);
702 }
703}