1use axum::{
7 Json,
8 extract::{Path, Query, State},
9 http::StatusCode,
10 response::{IntoResponse, Response},
11};
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use std::collections::HashMap;
16use std::sync::Arc;
17use uuid::Uuid;
18
19use crate::config::LinksConfig;
20use crate::core::extractors::{
21 DirectLinkExtractor, ExtractorError, LinkExtractor, RecursiveLinkExtractor,
22};
23use crate::core::{
24 EntityCreator, EntityFetcher, LinkDefinition, LinkService,
25 link::LinkEntity,
26 query::{PaginationMeta, QueryParams},
27};
28use crate::links::registry::{LinkDirection, LinkRouteRegistry};
29
30#[derive(Clone)]
32pub struct AppState {
33 pub link_service: Arc<dyn LinkService>,
34 pub config: Arc<LinksConfig>,
35 pub registry: Arc<LinkRouteRegistry>,
36 pub entity_fetchers: Arc<HashMap<String, Arc<dyn EntityFetcher>>>,
38 pub entity_creators: Arc<HashMap<String, Arc<dyn EntityCreator>>>,
40}
41
42impl AppState {
43 pub fn get_link_auth_policy(
45 link_definition: &LinkDefinition,
46 operation: &str,
47 ) -> Option<String> {
48 link_definition.auth.as_ref().map(|auth| match operation {
49 "list" => auth.list.clone(),
50 "get" => auth.get.clone(),
51 "create" => auth.create.clone(),
52 "update" => auth.update.clone(),
53 "delete" => auth.delete.clone(),
54 _ => "authenticated".to_string(),
55 })
56 }
57}
58
59#[derive(Debug, Serialize)]
61pub struct ListLinksResponse {
62 pub links: Vec<LinkEntity>,
63 pub count: usize,
64 pub link_type: String,
65 pub direction: String,
66 pub description: Option<String>,
67}
68
69#[derive(Debug, Serialize)]
71pub struct EnrichedLink {
72 pub id: Uuid,
74
75 #[serde(rename = "type")]
77 pub entity_type: String,
78
79 pub link_type: String,
81
82 pub source_id: Uuid,
84
85 pub target_id: Uuid,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub source: Option<serde_json::Value>,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub target: Option<serde_json::Value>,
95
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub metadata: Option<serde_json::Value>,
99
100 pub created_at: DateTime<Utc>,
102
103 pub updated_at: DateTime<Utc>,
105
106 pub status: String,
108}
109
110#[derive(Debug, Serialize)]
112pub struct EnrichedListLinksResponse {
113 pub links: Vec<EnrichedLink>,
114 pub count: usize,
115 pub link_type: String,
116 pub direction: String,
117 pub description: Option<String>,
118}
119
120#[derive(Debug, Serialize)]
122pub struct PaginatedEnrichedLinksResponse {
123 pub data: Vec<EnrichedLink>,
124 pub pagination: PaginationMeta,
125 pub link_type: String,
126 pub direction: String,
127 pub description: Option<String>,
128}
129
130#[derive(Debug, Deserialize)]
132pub struct CreateLinkRequest {
133 pub metadata: Option<serde_json::Value>,
134}
135
136#[derive(Debug, Deserialize)]
138pub struct CreateLinkedEntityRequest {
139 pub entity: serde_json::Value,
140 pub metadata: Option<serde_json::Value>,
141}
142
143#[derive(Debug, Clone, Copy)]
145pub enum EnrichmentContext {
146 FromSource,
148 FromTarget,
150 DirectLink,
152}
153
154pub async fn list_links(
158 State(state): State<AppState>,
159 Path((entity_type_plural, entity_id, route_name)): Path<(String, Uuid, String)>,
160 Query(params): Query<QueryParams>,
161) -> Result<Json<PaginatedEnrichedLinksResponse>, ExtractorError> {
162 let extractor = LinkExtractor::from_path_and_registry(
163 (entity_type_plural, entity_id, route_name),
164 &state.registry,
165 &state.config,
166 )?;
167
168 let links = match extractor.direction {
170 LinkDirection::Forward => state
171 .link_service
172 .find_by_source(
173 &extractor.entity_id,
174 Some(&extractor.link_definition.link_type),
175 Some(&extractor.link_definition.target_type),
176 )
177 .await
178 .map_err(|e| ExtractorError::JsonError(e.to_string()))?,
179 LinkDirection::Reverse => state
180 .link_service
181 .find_by_target(
182 &extractor.entity_id,
183 Some(&extractor.link_definition.link_type),
184 Some(&extractor.link_definition.source_type),
185 )
186 .await
187 .map_err(|e| ExtractorError::JsonError(e.to_string()))?,
188 };
189
190 let context = match extractor.direction {
192 LinkDirection::Forward => EnrichmentContext::FromSource,
193 LinkDirection::Reverse => EnrichmentContext::FromTarget,
194 };
195
196 let mut all_enriched =
198 enrich_links_with_entities(&state, links, context, &extractor.link_definition).await?;
199
200 if let Some(filter_value) = params.filter_value() {
202 all_enriched = apply_link_filters(all_enriched, &filter_value);
203 }
204
205 let total = all_enriched.len();
206
207 let page = params.page();
209 let limit = params.limit();
210 let start = (page - 1) * limit;
211
212 let paginated_links: Vec<EnrichedLink> =
213 all_enriched.into_iter().skip(start).take(limit).collect();
214
215 Ok(Json(PaginatedEnrichedLinksResponse {
216 data: paginated_links,
217 pagination: PaginationMeta::new(page, limit, total),
218 link_type: extractor.link_definition.link_type,
219 direction: format!("{:?}", extractor.direction),
220 description: extractor.link_definition.description,
221 }))
222}
223
224async fn enrich_links_with_entities(
226 state: &AppState,
227 links: Vec<LinkEntity>,
228 context: EnrichmentContext,
229 link_definition: &LinkDefinition,
230) -> Result<Vec<EnrichedLink>, ExtractorError> {
231 let mut enriched = Vec::new();
232
233 for link in links {
234 let source_entity = match context {
236 EnrichmentContext::FromSource => None,
237 EnrichmentContext::FromTarget | EnrichmentContext::DirectLink => {
238 fetch_entity_by_type(state, &link_definition.source_type, &link.source_id)
240 .await
241 .ok()
242 }
243 };
244
245 let target_entity = match context {
247 EnrichmentContext::FromTarget => None,
248 EnrichmentContext::FromSource | EnrichmentContext::DirectLink => {
249 fetch_entity_by_type(state, &link_definition.target_type, &link.target_id)
251 .await
252 .ok()
253 }
254 };
255
256 enriched.push(EnrichedLink {
257 id: link.id,
258 entity_type: link.entity_type,
259 link_type: link.link_type,
260 source_id: link.source_id,
261 target_id: link.target_id,
262 source: source_entity,
263 target: target_entity,
264 metadata: link.metadata,
265 created_at: link.created_at,
266 updated_at: link.updated_at,
267 status: link.status,
268 });
269 }
270
271 Ok(enriched)
272}
273
274async fn fetch_entity_by_type(
276 state: &AppState,
277 entity_type: &str,
278 entity_id: &Uuid,
279) -> Result<serde_json::Value, ExtractorError> {
280 let fetcher = state.entity_fetchers.get(entity_type).ok_or_else(|| {
281 ExtractorError::JsonError(format!(
282 "No entity fetcher registered for type: {}",
283 entity_type
284 ))
285 })?;
286
287 fetcher
288 .fetch_as_json(entity_id)
289 .await
290 .map_err(|e| ExtractorError::JsonError(format!("Failed to fetch entity: {}", e)))
291}
292
293fn apply_link_filters(enriched_links: Vec<EnrichedLink>, filter: &Value) -> Vec<EnrichedLink> {
299 if filter.is_null() || !filter.is_object() {
300 return enriched_links;
301 }
302
303 let filter_obj = filter.as_object().unwrap();
304
305 enriched_links
306 .into_iter()
307 .filter(|link| {
308 let mut matches = true;
309
310 let link_json = match serde_json::to_value(link) {
312 Ok(v) => v,
313 Err(_) => return false,
314 };
315
316 for (key, value) in filter_obj.iter() {
317 let field_value = get_nested_value(&link_json, key);
319
320 match field_value {
321 Some(field_val) => {
322 if field_val != *value {
324 matches = false;
325 break;
326 }
327 }
328 None => {
329 matches = false;
330 break;
331 }
332 }
333 }
334
335 matches
336 })
337 .collect()
338}
339
340fn get_nested_value(json: &Value, key: &str) -> Option<Value> {
343 let parts: Vec<&str> = key.split('.').collect();
344
345 match parts.len() {
346 1 => json.get(key).cloned(),
347 2 => {
348 let (parent, child) = (parts[0], parts[1]);
349 json.get(parent).and_then(|v| v.get(child)).cloned()
350 }
351 _ => None,
352 }
353}
354
355pub async fn get_link(
359 State(state): State<AppState>,
360 Path(link_id): Path<Uuid>,
361) -> Result<Response, ExtractorError> {
362 let link = state
363 .link_service
364 .get(&link_id)
365 .await
366 .map_err(|e| ExtractorError::JsonError(e.to_string()))?
367 .ok_or(ExtractorError::LinkNotFound)?;
368
369 let link_definition = state
371 .config
372 .links
373 .iter()
374 .find(|def| def.link_type == link.link_type)
375 .ok_or_else(|| {
376 ExtractorError::JsonError(format!(
377 "No link definition found for link_type: {}",
378 link.link_type
379 ))
380 })?;
381
382 let enriched_links = enrich_links_with_entities(
384 &state,
385 vec![link],
386 EnrichmentContext::DirectLink,
387 link_definition,
388 )
389 .await?;
390
391 let enriched_link = enriched_links
392 .into_iter()
393 .next()
394 .ok_or(ExtractorError::LinkNotFound)?;
395
396 Ok(Json(enriched_link).into_response())
397}
398
399pub async fn get_link_by_route(
403 State(state): State<AppState>,
404 Path((source_type_plural, source_id, route_name, target_id)): Path<(
405 String,
406 Uuid,
407 String,
408 Uuid,
409 )>,
410) -> Result<Response, ExtractorError> {
411 let extractor = DirectLinkExtractor::from_path(
412 (source_type_plural, source_id, route_name, target_id),
413 &state.registry,
414 &state.config,
415 )?;
416
417 let existing_links = match extractor.direction {
419 LinkDirection::Forward => {
420 state
422 .link_service
423 .find_by_source(
424 &extractor.source_id,
425 Some(&extractor.link_definition.link_type),
426 Some(&extractor.target_type),
427 )
428 .await
429 .map_err(|e| ExtractorError::JsonError(e.to_string()))?
430 }
431 LinkDirection::Reverse => {
432 state
434 .link_service
435 .find_by_source(
436 &extractor.target_id,
437 Some(&extractor.link_definition.link_type),
438 Some(&extractor.source_type),
439 )
440 .await
441 .map_err(|e| ExtractorError::JsonError(e.to_string()))?
442 }
443 };
444
445 let link = existing_links
446 .into_iter()
447 .find(|link| match extractor.direction {
448 LinkDirection::Forward => link.target_id == extractor.target_id,
449 LinkDirection::Reverse => link.target_id == extractor.source_id,
450 })
451 .ok_or(ExtractorError::LinkNotFound)?;
452
453 let enriched_links = enrich_links_with_entities(
455 &state,
456 vec![link],
457 EnrichmentContext::DirectLink,
458 &extractor.link_definition,
459 )
460 .await?;
461
462 let enriched_link = enriched_links
463 .into_iter()
464 .next()
465 .ok_or(ExtractorError::LinkNotFound)?;
466
467 Ok(Json(enriched_link).into_response())
468}
469
470pub async fn create_link(
475 State(state): State<AppState>,
476 Path((source_type_plural, source_id, route_name, target_id)): Path<(
477 String,
478 Uuid,
479 String,
480 Uuid,
481 )>,
482 Json(payload): Json<CreateLinkRequest>,
483) -> Result<Response, ExtractorError> {
484 let extractor = DirectLinkExtractor::from_path(
485 (source_type_plural, source_id, route_name, target_id),
486 &state.registry,
487 &state.config,
488 )?;
489
490 let link = LinkEntity::new(
492 extractor.link_definition.link_type,
493 extractor.source_id,
494 extractor.target_id,
495 payload.metadata,
496 );
497
498 let created_link = state
499 .link_service
500 .create(link)
501 .await
502 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
503
504 Ok((StatusCode::CREATED, Json(created_link)).into_response())
505}
506
507pub async fn create_linked_entity(
512 State(state): State<AppState>,
513 Path((source_type_plural, source_id, route_name)): Path<(String, Uuid, String)>,
514 Json(payload): Json<CreateLinkedEntityRequest>,
515) -> Result<Response, ExtractorError> {
516 let extractor = LinkExtractor::from_path_and_registry(
517 (source_type_plural.clone(), source_id, route_name.clone()),
518 &state.registry,
519 &state.config,
520 )?;
521
522 let (source_entity_id, target_entity_type) = match extractor.direction {
524 LinkDirection::Forward => {
525 (extractor.entity_id, &extractor.link_definition.target_type)
527 }
528 LinkDirection::Reverse => {
529 (extractor.entity_id, &extractor.link_definition.source_type)
531 }
532 };
533
534 let entity_creator = state
536 .entity_creators
537 .get(target_entity_type)
538 .ok_or_else(|| {
539 ExtractorError::JsonError(format!(
540 "No entity creator registered for type: {}",
541 target_entity_type
542 ))
543 })?;
544
545 let created_entity = entity_creator
547 .create_from_json(payload.entity)
548 .await
549 .map_err(|e| ExtractorError::JsonError(format!("Failed to create entity: {}", e)))?;
550
551 let target_entity_id = created_entity["id"].as_str().ok_or_else(|| {
553 ExtractorError::JsonError("Created entity missing 'id' field".to_string())
554 })?;
555 let target_entity_id = Uuid::parse_str(target_entity_id)
556 .map_err(|e| ExtractorError::JsonError(format!("Invalid UUID in created entity: {}", e)))?;
557
558 let link = match extractor.direction {
560 LinkDirection::Forward => {
561 LinkEntity::new(
563 extractor.link_definition.link_type,
564 source_entity_id,
565 target_entity_id,
566 payload.metadata,
567 )
568 }
569 LinkDirection::Reverse => {
570 LinkEntity::new(
572 extractor.link_definition.link_type,
573 target_entity_id,
574 source_entity_id,
575 payload.metadata,
576 )
577 }
578 };
579
580 let created_link = state
581 .link_service
582 .create(link)
583 .await
584 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
585
586 let response = serde_json::json!({
588 "entity": created_entity,
589 "link": created_link,
590 });
591
592 Ok((StatusCode::CREATED, Json(response)).into_response())
593}
594
595pub async fn update_link(
599 State(state): State<AppState>,
600 Path((source_type_plural, source_id, route_name, target_id)): Path<(
601 String,
602 Uuid,
603 String,
604 Uuid,
605 )>,
606 Json(payload): Json<CreateLinkRequest>,
607) -> Result<Response, ExtractorError> {
608 let extractor = DirectLinkExtractor::from_path(
609 (source_type_plural, source_id, route_name, target_id),
610 &state.registry,
611 &state.config,
612 )?;
613
614 let existing_links = state
616 .link_service
617 .find_by_source(
618 &extractor.source_id,
619 Some(&extractor.link_definition.link_type),
620 Some(&extractor.target_type),
621 )
622 .await
623 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
624
625 let mut existing_link = existing_links
626 .into_iter()
627 .find(|link| link.target_id == extractor.target_id)
628 .ok_or_else(|| ExtractorError::RouteNotFound("Link not found".to_string()))?;
629
630 existing_link.metadata = payload.metadata;
632 existing_link.touch();
633
634 let link_id = existing_link.id;
636 let updated_link = state
637 .link_service
638 .update(&link_id, existing_link)
639 .await
640 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
641
642 Ok(Json(updated_link).into_response())
643}
644
645pub async fn delete_link(
649 State(state): State<AppState>,
650 Path((source_type_plural, source_id, route_name, target_id)): Path<(
651 String,
652 Uuid,
653 String,
654 Uuid,
655 )>,
656) -> Result<Response, ExtractorError> {
657 let extractor = DirectLinkExtractor::from_path(
658 (source_type_plural, source_id, route_name, target_id),
659 &state.registry,
660 &state.config,
661 )?;
662
663 let existing_links = state
665 .link_service
666 .find_by_source(
667 &extractor.source_id,
668 Some(&extractor.link_definition.link_type),
669 Some(&extractor.target_type),
670 )
671 .await
672 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
673
674 let existing_link = existing_links
675 .into_iter()
676 .find(|link| link.target_id == extractor.target_id)
677 .ok_or(ExtractorError::LinkNotFound)?;
678
679 state
681 .link_service
682 .delete(&existing_link.id)
683 .await
684 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
685
686 Ok(StatusCode::NO_CONTENT.into_response())
687}
688
689#[derive(Debug, Serialize)]
691pub struct IntrospectionResponse {
692 pub entity_type: String,
693 pub entity_id: Uuid,
694 pub available_routes: Vec<RouteDescription>,
695}
696
697#[derive(Debug, Serialize)]
699pub struct RouteDescription {
700 pub path: String,
701 pub method: String,
702 pub link_type: String,
703 pub direction: String,
704 pub connected_to: String,
705 pub description: Option<String>,
706}
707
708pub async fn list_available_links(
712 State(state): State<AppState>,
713 Path((entity_type_plural, entity_id)): Path<(String, Uuid)>,
714) -> Result<Json<IntrospectionResponse>, ExtractorError> {
715 let entity_type = state
717 .config
718 .entities
719 .iter()
720 .find(|e| e.plural == entity_type_plural)
721 .map(|e| e.singular.clone())
722 .unwrap_or_else(|| entity_type_plural.clone());
723
724 let routes = state.registry.list_routes_for_entity(&entity_type);
726
727 let available_routes = routes
728 .iter()
729 .map(|r| RouteDescription {
730 path: format!("/{}/{}/{}", entity_type_plural, entity_id, r.route_name),
731 method: "GET".to_string(),
732 link_type: r.link_type.clone(),
733 direction: format!("{:?}", r.direction),
734 connected_to: r.connected_to.clone(),
735 description: r.description.clone(),
736 })
737 .collect();
738
739 Ok(Json(IntrospectionResponse {
740 entity_type,
741 entity_id,
742 available_routes,
743 }))
744}
745
746pub async fn handle_nested_path_get(
752 State(state): State<AppState>,
753 Path(path): Path<String>,
754 Query(params): Query<QueryParams>,
755) -> Result<Json<serde_json::Value>, ExtractorError> {
756 let segments: Vec<String> = path
758 .trim_matches('/')
759 .split('/')
760 .map(|s| s.to_string())
761 .collect();
762
763 if segments.len() < 5 {
766 return Err(ExtractorError::InvalidPath);
767 }
768
769 let extractor =
771 RecursiveLinkExtractor::from_segments(segments, &state.registry, &state.config)?;
772
773 if extractor.is_list {
775 use crate::links::registry::LinkDirection;
780
781 for i in 0..extractor.chain.len() - 1 {
783 let current = &extractor.chain[i];
784 let next = &extractor.chain[i + 1];
785
786 if next.entity_id.is_nil() {
788 continue;
789 }
790
791 if let Some(link_def) = ¤t.link_definition {
793 let link_exists = match current.link_direction {
794 Some(LinkDirection::Forward) => {
795 let links = state
797 .link_service
798 .find_by_source(
799 ¤t.entity_id,
800 Some(&link_def.link_type),
801 Some(&link_def.target_type),
802 )
803 .await
804 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
805 links.iter().any(|l| l.target_id == next.entity_id)
806 }
807 Some(LinkDirection::Reverse) => {
808 let links = state
810 .link_service
811 .find_by_target(¤t.entity_id, None, Some(&link_def.link_type))
812 .await
813 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
814 links.iter().any(|l| l.source_id == next.entity_id)
815 }
816 None => {
817 return Err(ExtractorError::InvalidPath);
818 }
819 };
820
821 if !link_exists {
822 return Err(ExtractorError::LinkNotFound);
823 }
824 }
825 else if let Some(next_link_def) = &next.link_definition {
828 let link_exists = match next.link_direction {
829 Some(LinkDirection::Forward) => {
830 let links = state
832 .link_service
833 .find_by_source(
834 ¤t.entity_id,
835 Some(&next_link_def.link_type),
836 Some(&next_link_def.target_type),
837 )
838 .await
839 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
840 links.iter().any(|l| l.target_id == next.entity_id)
841 }
842 Some(LinkDirection::Reverse) => {
843 let links = state
845 .link_service
846 .find_by_target(
847 ¤t.entity_id,
848 None,
849 Some(&next_link_def.link_type),
850 )
851 .await
852 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
853 links.iter().any(|l| l.source_id == next.entity_id)
854 }
855 None => {
856 return Err(ExtractorError::InvalidPath);
857 }
858 };
859
860 if !link_exists {
861 return Err(ExtractorError::LinkNotFound);
862 }
863 }
864 }
865
866 if let Some(link_def) = extractor.final_link_def() {
868 let penultimate = extractor
870 .penultimate_segment()
871 .ok_or(ExtractorError::InvalidPath)?;
872 let entity_id = penultimate.entity_id;
873
874 use crate::links::registry::LinkDirection;
875
876 let (links, enrichment_context) = match penultimate.link_direction {
878 Some(LinkDirection::Forward) => {
879 let links = state
881 .link_service
882 .find_by_source(
883 &entity_id,
884 Some(&link_def.link_type),
885 Some(&link_def.target_type),
886 )
887 .await
888 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
889 (links, EnrichmentContext::FromSource)
890 }
891 Some(LinkDirection::Reverse) => {
892 let links = state
894 .link_service
895 .find_by_target(&entity_id, None, Some(&link_def.link_type))
896 .await
897 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
898 (links, EnrichmentContext::FromTarget)
899 }
900 None => {
901 return Err(ExtractorError::InvalidPath);
902 }
903 };
904
905 let mut all_enriched =
907 enrich_links_with_entities(&state, links, enrichment_context, link_def).await?;
908
909 if let Some(filter_value) = params.filter_value() {
911 all_enriched = apply_link_filters(all_enriched, &filter_value);
912 }
913
914 let total = all_enriched.len();
915
916 let page = params.page();
918 let limit = params.limit();
919 let start = (page - 1) * limit;
920
921 let paginated_links: Vec<EnrichedLink> =
922 all_enriched.into_iter().skip(start).take(limit).collect();
923
924 Ok(Json(serde_json::json!({
925 "data": paginated_links,
926 "pagination": {
927 "page": page,
928 "limit": limit,
929 "total": total,
930 "total_pages": PaginationMeta::new(page, limit, total).total_pages,
931 "has_next": PaginationMeta::new(page, limit, total).has_next,
932 "has_prev": PaginationMeta::new(page, limit, total).has_prev
933 },
934 "link_type": link_def.link_type,
935 "direction": format!("{:?}", penultimate.link_direction),
936 "description": link_def.description
937 })))
938 } else {
939 Err(ExtractorError::InvalidPath)
940 }
941 } else {
942 use crate::links::registry::LinkDirection;
945
946 for i in 0..extractor.chain.len() - 1 {
948 let current = &extractor.chain[i];
949 let next = &extractor.chain[i + 1];
950
951 if let Some(link_def) = ¤t.link_definition {
953 let link_exists = match current.link_direction {
954 Some(LinkDirection::Forward) => {
955 let links = state
956 .link_service
957 .find_by_source(
958 ¤t.entity_id,
959 Some(&link_def.link_type),
960 Some(&link_def.target_type),
961 )
962 .await
963 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
964 links.iter().any(|l| l.target_id == next.entity_id)
965 }
966 Some(LinkDirection::Reverse) => {
967 let links = state
968 .link_service
969 .find_by_target(¤t.entity_id, None, Some(&link_def.link_type))
970 .await
971 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
972 links.iter().any(|l| l.source_id == next.entity_id)
973 }
974 None => {
975 return Err(ExtractorError::InvalidPath);
976 }
977 };
978
979 if !link_exists {
980 return Err(ExtractorError::LinkNotFound);
981 }
982 }
983 else if let Some(next_link_def) = &next.link_definition {
985 let link_exists = match next.link_direction {
986 Some(LinkDirection::Forward) => {
987 let links = state
988 .link_service
989 .find_by_source(
990 ¤t.entity_id,
991 Some(&next_link_def.link_type),
992 Some(&next_link_def.target_type),
993 )
994 .await
995 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
996 links.iter().any(|l| l.target_id == next.entity_id)
997 }
998 Some(LinkDirection::Reverse) => {
999 let links = state
1000 .link_service
1001 .find_by_target(
1002 ¤t.entity_id,
1003 None,
1004 Some(&next_link_def.link_type),
1005 )
1006 .await
1007 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1008 links.iter().any(|l| l.source_id == next.entity_id)
1009 }
1010 None => {
1011 return Err(ExtractorError::InvalidPath);
1012 }
1013 };
1014
1015 if !link_exists {
1016 return Err(ExtractorError::LinkNotFound);
1017 }
1018 }
1019 }
1020
1021 if let Some(link_def) = extractor.final_link_def() {
1023 let (target_id, _) = extractor.final_target();
1024 let penultimate = extractor.penultimate_segment().unwrap();
1025
1026 let link = match penultimate.link_direction {
1028 Some(LinkDirection::Forward) => {
1029 let links = state
1031 .link_service
1032 .find_by_source(
1033 &penultimate.entity_id,
1034 Some(&link_def.link_type),
1035 Some(&link_def.target_type),
1036 )
1037 .await
1038 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1039
1040 links
1041 .into_iter()
1042 .find(|l| l.target_id == target_id)
1043 .ok_or(ExtractorError::LinkNotFound)?
1044 }
1045 Some(LinkDirection::Reverse) => {
1046 let links = state
1048 .link_service
1049 .find_by_target(&penultimate.entity_id, None, Some(&link_def.link_type))
1050 .await
1051 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1052
1053 links
1054 .into_iter()
1055 .find(|l| l.source_id == target_id)
1056 .ok_or(ExtractorError::LinkNotFound)?
1057 }
1058 None => {
1059 return Err(ExtractorError::InvalidPath);
1060 }
1061 };
1062
1063 let enriched = enrich_links_with_entities(
1065 &state,
1066 vec![link],
1067 EnrichmentContext::DirectLink,
1068 link_def,
1069 )
1070 .await?;
1071
1072 Ok(Json(serde_json::json!({
1073 "link": enriched.first()
1074 })))
1075 } else {
1076 Err(ExtractorError::InvalidPath)
1077 }
1078 }
1079}
1080
1081pub async fn handle_nested_path_post(
1086 State(state): State<AppState>,
1087 Path(path): Path<String>,
1088 Json(payload): Json<CreateLinkedEntityRequest>,
1089) -> Result<Response, ExtractorError> {
1090 let segments: Vec<String> = path
1091 .trim_matches('/')
1092 .split('/')
1093 .map(|s| s.to_string())
1094 .collect();
1095
1096 if segments.len() < 5 {
1099 return Err(ExtractorError::InvalidPath);
1100 }
1101
1102 let extractor =
1103 RecursiveLinkExtractor::from_segments(segments, &state.registry, &state.config)?;
1104
1105 let link_def = extractor
1107 .final_link_def()
1108 .ok_or(ExtractorError::InvalidPath)?;
1109
1110 let (source_id, _) = extractor.final_target();
1111 let target_entity_type = &link_def.target_type;
1112
1113 let entity_creator = state
1115 .entity_creators
1116 .get(target_entity_type)
1117 .ok_or_else(|| {
1118 ExtractorError::JsonError(format!(
1119 "No entity creator registered for type: {}",
1120 target_entity_type
1121 ))
1122 })?;
1123
1124 let created_entity = entity_creator
1126 .create_from_json(payload.entity)
1127 .await
1128 .map_err(|e| ExtractorError::JsonError(format!("Failed to create entity: {}", e)))?;
1129
1130 let target_entity_id = created_entity["id"].as_str().ok_or_else(|| {
1132 ExtractorError::JsonError("Created entity missing 'id' field".to_string())
1133 })?;
1134 let target_entity_id = Uuid::parse_str(target_entity_id)
1135 .map_err(|e| ExtractorError::JsonError(format!("Invalid UUID in created entity: {}", e)))?;
1136
1137 let link = LinkEntity::new(
1139 link_def.link_type.clone(),
1140 source_id,
1141 target_entity_id,
1142 payload.metadata,
1143 );
1144
1145 let created_link = state
1146 .link_service
1147 .create(link)
1148 .await
1149 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1150
1151 let response = serde_json::json!({
1152 "entity": created_entity,
1153 "link": created_link,
1154 });
1155
1156 Ok((StatusCode::CREATED, Json(response)).into_response())
1157}
1158
1159#[cfg(test)]
1160mod tests {
1161 use super::*;
1162 use crate::config::EntityConfig;
1163 use crate::core::LinkDefinition;
1164 use crate::storage::InMemoryLinkService;
1165
1166 fn create_test_state() -> AppState {
1167 let config = Arc::new(LinksConfig {
1168 entities: vec![
1169 EntityConfig {
1170 singular: "user".to_string(),
1171 plural: "users".to_string(),
1172 auth: crate::config::EntityAuthConfig::default(),
1173 },
1174 EntityConfig {
1175 singular: "car".to_string(),
1176 plural: "cars".to_string(),
1177 auth: crate::config::EntityAuthConfig::default(),
1178 },
1179 ],
1180 links: vec![LinkDefinition {
1181 link_type: "owner".to_string(),
1182 source_type: "user".to_string(),
1183 target_type: "car".to_string(),
1184 forward_route_name: "cars-owned".to_string(),
1185 reverse_route_name: "users-owners".to_string(),
1186 description: Some("User owns a car".to_string()),
1187 required_fields: None,
1188 auth: None,
1189 }],
1190 validation_rules: None,
1191 });
1192
1193 let registry = Arc::new(LinkRouteRegistry::new(config.clone()));
1194 let link_service: Arc<dyn LinkService> = Arc::new(InMemoryLinkService::new());
1195
1196 AppState {
1197 link_service,
1198 config,
1199 registry,
1200 entity_fetchers: Arc::new(HashMap::new()),
1201 entity_creators: Arc::new(HashMap::new()),
1202 }
1203 }
1204
1205 #[test]
1206 fn test_state_creation() {
1207 let state = create_test_state();
1208 assert_eq!(state.config.entities.len(), 2);
1209 assert_eq!(state.config.links.len(), 1);
1210 }
1211}