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::events::{EventBus, FrameworkEvent, LinkEvent};
21use crate::core::extractors::{
22 DirectLinkExtractor, ExtractorError, LinkExtractor, RecursiveLinkExtractor,
23};
24use crate::core::{
25 EntityCreator, EntityFetcher, LinkDefinition, LinkService,
26 link::LinkEntity,
27 query::{PaginationMeta, QueryParams},
28};
29use crate::links::registry::{LinkDirection, LinkRouteRegistry};
30
31#[derive(Clone)]
33pub struct AppState {
34 pub link_service: Arc<dyn LinkService>,
35 pub config: Arc<LinksConfig>,
36 pub registry: Arc<LinkRouteRegistry>,
37 pub entity_fetchers: Arc<HashMap<String, Arc<dyn EntityFetcher>>>,
39 pub entity_creators: Arc<HashMap<String, Arc<dyn EntityCreator>>>,
41 pub event_bus: Option<Arc<EventBus>>,
43}
44
45impl AppState {
46 pub fn publish_event(&self, event: FrameworkEvent) {
51 if let Some(ref bus) = self.event_bus {
52 bus.publish(event);
53 }
54 }
55
56 pub fn get_link_auth_policy(
58 link_definition: &LinkDefinition,
59 operation: &str,
60 ) -> Option<String> {
61 link_definition.auth.as_ref().map(|auth| match operation {
62 "list" => auth.list.clone(),
63 "get" => auth.get.clone(),
64 "create" => auth.create.clone(),
65 "update" => auth.update.clone(),
66 "delete" => auth.delete.clone(),
67 _ => "authenticated".to_string(),
68 })
69 }
70}
71
72#[derive(Debug, Serialize)]
74pub struct ListLinksResponse {
75 pub links: Vec<LinkEntity>,
76 pub count: usize,
77 pub link_type: String,
78 pub direction: String,
79 pub description: Option<String>,
80}
81
82#[derive(Debug, Serialize)]
84pub struct EnrichedLink {
85 pub id: Uuid,
87
88 #[serde(rename = "type")]
90 pub entity_type: String,
91
92 pub link_type: String,
94
95 pub source_id: Uuid,
97
98 pub target_id: Uuid,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub source: Option<serde_json::Value>,
104
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub target: Option<serde_json::Value>,
108
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub metadata: Option<serde_json::Value>,
112
113 pub created_at: DateTime<Utc>,
115
116 pub updated_at: DateTime<Utc>,
118
119 pub status: String,
121}
122
123#[derive(Debug, Serialize)]
125pub struct EnrichedListLinksResponse {
126 pub links: Vec<EnrichedLink>,
127 pub count: usize,
128 pub link_type: String,
129 pub direction: String,
130 pub description: Option<String>,
131}
132
133#[derive(Debug, Serialize)]
135pub struct PaginatedEnrichedLinksResponse {
136 pub data: Vec<EnrichedLink>,
137 pub pagination: PaginationMeta,
138 pub link_type: String,
139 pub direction: String,
140 pub description: Option<String>,
141}
142
143#[derive(Debug, Deserialize)]
145pub struct CreateLinkRequest {
146 pub metadata: Option<serde_json::Value>,
147}
148
149#[derive(Debug, Deserialize)]
151pub struct CreateLinkedEntityRequest {
152 pub entity: serde_json::Value,
153 pub metadata: Option<serde_json::Value>,
154}
155
156#[derive(Debug, Clone, Copy)]
158pub enum EnrichmentContext {
159 FromSource,
161 FromTarget,
163 DirectLink,
165}
166
167pub async fn list_links(
171 State(state): State<AppState>,
172 Path((entity_type_plural, entity_id, route_name)): Path<(String, Uuid, String)>,
173 Query(params): Query<QueryParams>,
174) -> Result<Json<PaginatedEnrichedLinksResponse>, ExtractorError> {
175 let extractor = LinkExtractor::from_path_and_registry(
176 (entity_type_plural, entity_id, route_name),
177 &state.registry,
178 &state.config,
179 )?;
180
181 let links = match extractor.direction {
183 LinkDirection::Forward => state
184 .link_service
185 .find_by_source(
186 &extractor.entity_id,
187 Some(&extractor.link_definition.link_type),
188 Some(&extractor.link_definition.target_type),
189 )
190 .await
191 .map_err(|e| ExtractorError::JsonError(e.to_string()))?,
192 LinkDirection::Reverse => state
193 .link_service
194 .find_by_target(
195 &extractor.entity_id,
196 Some(&extractor.link_definition.link_type),
197 Some(&extractor.link_definition.source_type),
198 )
199 .await
200 .map_err(|e| ExtractorError::JsonError(e.to_string()))?,
201 };
202
203 let context = match extractor.direction {
205 LinkDirection::Forward => EnrichmentContext::FromSource,
206 LinkDirection::Reverse => EnrichmentContext::FromTarget,
207 };
208
209 let mut all_enriched =
211 enrich_links_with_entities(&state, links, context, &extractor.link_definition).await?;
212
213 if let Some(filter_value) = params.filter_value() {
215 all_enriched = apply_link_filters(all_enriched, &filter_value);
216 }
217
218 let total = all_enriched.len();
219
220 let page = params.page();
222 let limit = params.limit();
223 let start = (page - 1) * limit;
224
225 let paginated_links: Vec<EnrichedLink> =
226 all_enriched.into_iter().skip(start).take(limit).collect();
227
228 Ok(Json(PaginatedEnrichedLinksResponse {
229 data: paginated_links,
230 pagination: PaginationMeta::new(page, limit, total),
231 link_type: extractor.link_definition.link_type,
232 direction: format!("{:?}", extractor.direction),
233 description: extractor.link_definition.description,
234 }))
235}
236
237async fn enrich_links_with_entities(
239 state: &AppState,
240 links: Vec<LinkEntity>,
241 context: EnrichmentContext,
242 link_definition: &LinkDefinition,
243) -> Result<Vec<EnrichedLink>, ExtractorError> {
244 let mut enriched = Vec::new();
245
246 for link in links {
247 let source_entity = match context {
249 EnrichmentContext::FromSource => None,
250 EnrichmentContext::FromTarget | EnrichmentContext::DirectLink => {
251 fetch_entity_by_type(state, &link_definition.source_type, &link.source_id)
253 .await
254 .ok()
255 }
256 };
257
258 let target_entity = match context {
260 EnrichmentContext::FromTarget => None,
261 EnrichmentContext::FromSource | EnrichmentContext::DirectLink => {
262 fetch_entity_by_type(state, &link_definition.target_type, &link.target_id)
264 .await
265 .ok()
266 }
267 };
268
269 enriched.push(EnrichedLink {
270 id: link.id,
271 entity_type: link.entity_type,
272 link_type: link.link_type,
273 source_id: link.source_id,
274 target_id: link.target_id,
275 source: source_entity,
276 target: target_entity,
277 metadata: link.metadata,
278 created_at: link.created_at,
279 updated_at: link.updated_at,
280 status: link.status,
281 });
282 }
283
284 Ok(enriched)
285}
286
287async fn fetch_entity_by_type(
289 state: &AppState,
290 entity_type: &str,
291 entity_id: &Uuid,
292) -> Result<serde_json::Value, ExtractorError> {
293 let fetcher = state.entity_fetchers.get(entity_type).ok_or_else(|| {
294 ExtractorError::JsonError(format!(
295 "No entity fetcher registered for type: {}",
296 entity_type
297 ))
298 })?;
299
300 fetcher
301 .fetch_as_json(entity_id)
302 .await
303 .map_err(|e| ExtractorError::JsonError(format!("Failed to fetch entity: {}", e)))
304}
305
306fn apply_link_filters(enriched_links: Vec<EnrichedLink>, filter: &Value) -> Vec<EnrichedLink> {
312 if filter.is_null() || !filter.is_object() {
313 return enriched_links;
314 }
315
316 let filter_obj = filter.as_object().unwrap();
317
318 enriched_links
319 .into_iter()
320 .filter(|link| {
321 let mut matches = true;
322
323 let link_json = match serde_json::to_value(link) {
325 Ok(v) => v,
326 Err(_) => return false,
327 };
328
329 for (key, value) in filter_obj.iter() {
330 let field_value = get_nested_value(&link_json, key);
332
333 match field_value {
334 Some(field_val) => {
335 if field_val != *value {
337 matches = false;
338 break;
339 }
340 }
341 None => {
342 matches = false;
343 break;
344 }
345 }
346 }
347
348 matches
349 })
350 .collect()
351}
352
353fn get_nested_value(json: &Value, key: &str) -> Option<Value> {
356 let parts: Vec<&str> = key.split('.').collect();
357
358 match parts.len() {
359 1 => json.get(key).cloned(),
360 2 => {
361 let (parent, child) = (parts[0], parts[1]);
362 json.get(parent).and_then(|v| v.get(child)).cloned()
363 }
364 _ => None,
365 }
366}
367
368pub async fn get_link(
372 State(state): State<AppState>,
373 Path(link_id): Path<Uuid>,
374) -> Result<Response, ExtractorError> {
375 let link = state
376 .link_service
377 .get(&link_id)
378 .await
379 .map_err(|e| ExtractorError::JsonError(e.to_string()))?
380 .ok_or(ExtractorError::LinkNotFound)?;
381
382 let link_definition = state
384 .config
385 .links
386 .iter()
387 .find(|def| def.link_type == link.link_type)
388 .ok_or_else(|| {
389 ExtractorError::JsonError(format!(
390 "No link definition found for link_type: {}",
391 link.link_type
392 ))
393 })?;
394
395 let enriched_links = enrich_links_with_entities(
397 &state,
398 vec![link],
399 EnrichmentContext::DirectLink,
400 link_definition,
401 )
402 .await?;
403
404 let enriched_link = enriched_links
405 .into_iter()
406 .next()
407 .ok_or(ExtractorError::LinkNotFound)?;
408
409 Ok(Json(enriched_link).into_response())
410}
411
412pub async fn get_link_by_route(
416 State(state): State<AppState>,
417 Path((source_type_plural, source_id, route_name, target_id)): Path<(
418 String,
419 Uuid,
420 String,
421 Uuid,
422 )>,
423) -> Result<Response, ExtractorError> {
424 let extractor = DirectLinkExtractor::from_path(
425 (source_type_plural, source_id, route_name, target_id),
426 &state.registry,
427 &state.config,
428 )?;
429
430 let existing_links = match extractor.direction {
432 LinkDirection::Forward => {
433 state
435 .link_service
436 .find_by_source(
437 &extractor.source_id,
438 Some(&extractor.link_definition.link_type),
439 Some(&extractor.target_type),
440 )
441 .await
442 .map_err(|e| ExtractorError::JsonError(e.to_string()))?
443 }
444 LinkDirection::Reverse => {
445 state
447 .link_service
448 .find_by_source(
449 &extractor.target_id,
450 Some(&extractor.link_definition.link_type),
451 Some(&extractor.source_type),
452 )
453 .await
454 .map_err(|e| ExtractorError::JsonError(e.to_string()))?
455 }
456 };
457
458 let link = existing_links
459 .into_iter()
460 .find(|link| match extractor.direction {
461 LinkDirection::Forward => link.target_id == extractor.target_id,
462 LinkDirection::Reverse => link.target_id == extractor.source_id,
463 })
464 .ok_or(ExtractorError::LinkNotFound)?;
465
466 let enriched_links = enrich_links_with_entities(
468 &state,
469 vec![link],
470 EnrichmentContext::DirectLink,
471 &extractor.link_definition,
472 )
473 .await?;
474
475 let enriched_link = enriched_links
476 .into_iter()
477 .next()
478 .ok_or(ExtractorError::LinkNotFound)?;
479
480 Ok(Json(enriched_link).into_response())
481}
482
483pub async fn create_link(
488 State(state): State<AppState>,
489 Path((source_type_plural, source_id, route_name, target_id)): Path<(
490 String,
491 Uuid,
492 String,
493 Uuid,
494 )>,
495 Json(payload): Json<CreateLinkRequest>,
496) -> Result<Response, ExtractorError> {
497 let extractor = DirectLinkExtractor::from_path(
498 (source_type_plural, source_id, route_name, target_id),
499 &state.registry,
500 &state.config,
501 )?;
502
503 let link = LinkEntity::new(
505 extractor.link_definition.link_type,
506 extractor.source_id,
507 extractor.target_id,
508 payload.metadata,
509 );
510
511 let created_link = state
512 .link_service
513 .create(link)
514 .await
515 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
516
517 state.publish_event(FrameworkEvent::Link(LinkEvent::Created {
519 link_type: created_link.link_type.clone(),
520 link_id: created_link.id,
521 source_id: created_link.source_id,
522 target_id: created_link.target_id,
523 metadata: created_link.metadata.clone(),
524 }));
525
526 Ok((StatusCode::CREATED, Json(created_link)).into_response())
527}
528
529pub async fn create_linked_entity(
534 State(state): State<AppState>,
535 Path((source_type_plural, source_id, route_name)): Path<(String, Uuid, String)>,
536 Json(payload): Json<CreateLinkedEntityRequest>,
537) -> Result<Response, ExtractorError> {
538 let extractor = LinkExtractor::from_path_and_registry(
539 (source_type_plural.clone(), source_id, route_name.clone()),
540 &state.registry,
541 &state.config,
542 )?;
543
544 let (source_entity_id, target_entity_type) = match extractor.direction {
546 LinkDirection::Forward => {
547 (extractor.entity_id, &extractor.link_definition.target_type)
549 }
550 LinkDirection::Reverse => {
551 (extractor.entity_id, &extractor.link_definition.source_type)
553 }
554 };
555
556 let entity_creator = state
558 .entity_creators
559 .get(target_entity_type)
560 .ok_or_else(|| {
561 ExtractorError::JsonError(format!(
562 "No entity creator registered for type: {}",
563 target_entity_type
564 ))
565 })?;
566
567 let created_entity = entity_creator
569 .create_from_json(payload.entity)
570 .await
571 .map_err(|e| ExtractorError::JsonError(format!("Failed to create entity: {}", e)))?;
572
573 let target_entity_id = created_entity["id"].as_str().ok_or_else(|| {
575 ExtractorError::JsonError("Created entity missing 'id' field".to_string())
576 })?;
577 let target_entity_id = Uuid::parse_str(target_entity_id)
578 .map_err(|e| ExtractorError::JsonError(format!("Invalid UUID in created entity: {}", e)))?;
579
580 let link = match extractor.direction {
582 LinkDirection::Forward => {
583 LinkEntity::new(
585 extractor.link_definition.link_type,
586 source_entity_id,
587 target_entity_id,
588 payload.metadata,
589 )
590 }
591 LinkDirection::Reverse => {
592 LinkEntity::new(
594 extractor.link_definition.link_type,
595 target_entity_id,
596 source_entity_id,
597 payload.metadata,
598 )
599 }
600 };
601
602 let created_link = state
603 .link_service
604 .create(link)
605 .await
606 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
607
608 state.publish_event(FrameworkEvent::Entity(
610 crate::core::events::EntityEvent::Created {
611 entity_type: target_entity_type.clone(),
612 entity_id: target_entity_id,
613 data: created_entity.clone(),
614 },
615 ));
616
617 state.publish_event(FrameworkEvent::Link(LinkEvent::Created {
619 link_type: created_link.link_type.clone(),
620 link_id: created_link.id,
621 source_id: created_link.source_id,
622 target_id: created_link.target_id,
623 metadata: created_link.metadata.clone(),
624 }));
625
626 let response = serde_json::json!({
628 "entity": created_entity,
629 "link": created_link,
630 });
631
632 Ok((StatusCode::CREATED, Json(response)).into_response())
633}
634
635pub async fn update_link(
639 State(state): State<AppState>,
640 Path((source_type_plural, source_id, route_name, target_id)): Path<(
641 String,
642 Uuid,
643 String,
644 Uuid,
645 )>,
646 Json(payload): Json<CreateLinkRequest>,
647) -> Result<Response, ExtractorError> {
648 let extractor = DirectLinkExtractor::from_path(
649 (source_type_plural, source_id, route_name, target_id),
650 &state.registry,
651 &state.config,
652 )?;
653
654 let existing_links = state
656 .link_service
657 .find_by_source(
658 &extractor.source_id,
659 Some(&extractor.link_definition.link_type),
660 Some(&extractor.target_type),
661 )
662 .await
663 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
664
665 let mut existing_link = existing_links
666 .into_iter()
667 .find(|link| link.target_id == extractor.target_id)
668 .ok_or_else(|| ExtractorError::RouteNotFound("Link not found".to_string()))?;
669
670 existing_link.metadata = payload.metadata;
672 existing_link.touch();
673
674 let link_id = existing_link.id;
676 let updated_link = state
677 .link_service
678 .update(&link_id, existing_link)
679 .await
680 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
681
682 Ok(Json(updated_link).into_response())
683}
684
685pub async fn delete_link(
689 State(state): State<AppState>,
690 Path((source_type_plural, source_id, route_name, target_id)): Path<(
691 String,
692 Uuid,
693 String,
694 Uuid,
695 )>,
696) -> Result<Response, ExtractorError> {
697 let extractor = DirectLinkExtractor::from_path(
698 (source_type_plural, source_id, route_name, target_id),
699 &state.registry,
700 &state.config,
701 )?;
702
703 let existing_links = state
705 .link_service
706 .find_by_source(
707 &extractor.source_id,
708 Some(&extractor.link_definition.link_type),
709 Some(&extractor.target_type),
710 )
711 .await
712 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
713
714 let existing_link = existing_links
715 .into_iter()
716 .find(|link| link.target_id == extractor.target_id)
717 .ok_or(ExtractorError::LinkNotFound)?;
718
719 state
721 .link_service
722 .delete(&existing_link.id)
723 .await
724 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
725
726 state.publish_event(FrameworkEvent::Link(LinkEvent::Deleted {
728 link_type: existing_link.link_type.clone(),
729 link_id: existing_link.id,
730 source_id: existing_link.source_id,
731 target_id: existing_link.target_id,
732 }));
733
734 Ok(StatusCode::NO_CONTENT.into_response())
735}
736
737#[derive(Debug, Serialize)]
739pub struct IntrospectionResponse {
740 pub entity_type: String,
741 pub entity_id: Uuid,
742 pub available_routes: Vec<RouteDescription>,
743}
744
745#[derive(Debug, Serialize)]
747pub struct RouteDescription {
748 pub path: String,
749 pub method: String,
750 pub link_type: String,
751 pub direction: String,
752 pub connected_to: String,
753 pub description: Option<String>,
754}
755
756pub async fn list_available_links(
760 State(state): State<AppState>,
761 Path((entity_type_plural, entity_id)): Path<(String, Uuid)>,
762) -> Result<Json<IntrospectionResponse>, ExtractorError> {
763 let entity_type = state
765 .config
766 .entities
767 .iter()
768 .find(|e| e.plural == entity_type_plural)
769 .map(|e| e.singular.clone())
770 .unwrap_or_else(|| entity_type_plural.clone());
771
772 let routes = state.registry.list_routes_for_entity(&entity_type);
774
775 let available_routes = routes
776 .iter()
777 .map(|r| RouteDescription {
778 path: format!("/{}/{}/{}", entity_type_plural, entity_id, r.route_name),
779 method: "GET".to_string(),
780 link_type: r.link_type.clone(),
781 direction: format!("{:?}", r.direction),
782 connected_to: r.connected_to.clone(),
783 description: r.description.clone(),
784 })
785 .collect();
786
787 Ok(Json(IntrospectionResponse {
788 entity_type,
789 entity_id,
790 available_routes,
791 }))
792}
793
794pub async fn handle_nested_path_get(
800 State(state): State<AppState>,
801 Path(path): Path<String>,
802 Query(params): Query<QueryParams>,
803) -> Result<Json<serde_json::Value>, ExtractorError> {
804 let segments: Vec<String> = path
806 .trim_matches('/')
807 .split('/')
808 .map(|s| s.to_string())
809 .collect();
810
811 if segments.len() < 5 {
814 return Err(ExtractorError::InvalidPath);
815 }
816
817 let extractor =
819 RecursiveLinkExtractor::from_segments(segments, &state.registry, &state.config)?;
820
821 if extractor.is_list {
823 use crate::links::registry::LinkDirection;
828
829 for i in 0..extractor.chain.len() - 1 {
831 let current = &extractor.chain[i];
832 let next = &extractor.chain[i + 1];
833
834 if next.entity_id.is_nil() {
836 continue;
837 }
838
839 if let Some(link_def) = ¤t.link_definition {
841 let link_exists = match current.link_direction {
842 Some(LinkDirection::Forward) => {
843 let links = state
845 .link_service
846 .find_by_source(
847 ¤t.entity_id,
848 Some(&link_def.link_type),
849 Some(&link_def.target_type),
850 )
851 .await
852 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
853 links.iter().any(|l| l.target_id == next.entity_id)
854 }
855 Some(LinkDirection::Reverse) => {
856 let links = state
858 .link_service
859 .find_by_target(¤t.entity_id, None, Some(&link_def.link_type))
860 .await
861 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
862 links.iter().any(|l| l.source_id == next.entity_id)
863 }
864 None => {
865 return Err(ExtractorError::InvalidPath);
866 }
867 };
868
869 if !link_exists {
870 return Err(ExtractorError::LinkNotFound);
871 }
872 }
873 else if let Some(next_link_def) = &next.link_definition {
876 let link_exists = match next.link_direction {
877 Some(LinkDirection::Forward) => {
878 let links = state
880 .link_service
881 .find_by_source(
882 ¤t.entity_id,
883 Some(&next_link_def.link_type),
884 Some(&next_link_def.target_type),
885 )
886 .await
887 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
888 links.iter().any(|l| l.target_id == next.entity_id)
889 }
890 Some(LinkDirection::Reverse) => {
891 let links = state
893 .link_service
894 .find_by_target(
895 ¤t.entity_id,
896 None,
897 Some(&next_link_def.link_type),
898 )
899 .await
900 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
901 links.iter().any(|l| l.source_id == next.entity_id)
902 }
903 None => {
904 return Err(ExtractorError::InvalidPath);
905 }
906 };
907
908 if !link_exists {
909 return Err(ExtractorError::LinkNotFound);
910 }
911 }
912 }
913
914 if let Some(link_def) = extractor.final_link_def() {
916 let penultimate = extractor
918 .penultimate_segment()
919 .ok_or(ExtractorError::InvalidPath)?;
920 let entity_id = penultimate.entity_id;
921
922 use crate::links::registry::LinkDirection;
923
924 let (links, enrichment_context) = match penultimate.link_direction {
926 Some(LinkDirection::Forward) => {
927 let links = state
929 .link_service
930 .find_by_source(
931 &entity_id,
932 Some(&link_def.link_type),
933 Some(&link_def.target_type),
934 )
935 .await
936 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
937 (links, EnrichmentContext::FromSource)
938 }
939 Some(LinkDirection::Reverse) => {
940 let links = state
942 .link_service
943 .find_by_target(&entity_id, None, Some(&link_def.link_type))
944 .await
945 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
946 (links, EnrichmentContext::FromTarget)
947 }
948 None => {
949 return Err(ExtractorError::InvalidPath);
950 }
951 };
952
953 let mut all_enriched =
955 enrich_links_with_entities(&state, links, enrichment_context, link_def).await?;
956
957 if let Some(filter_value) = params.filter_value() {
959 all_enriched = apply_link_filters(all_enriched, &filter_value);
960 }
961
962 let total = all_enriched.len();
963
964 let page = params.page();
966 let limit = params.limit();
967 let start = (page - 1) * limit;
968
969 let paginated_links: Vec<EnrichedLink> =
970 all_enriched.into_iter().skip(start).take(limit).collect();
971
972 Ok(Json(serde_json::json!({
973 "data": paginated_links,
974 "pagination": {
975 "page": page,
976 "limit": limit,
977 "total": total,
978 "total_pages": PaginationMeta::new(page, limit, total).total_pages,
979 "has_next": PaginationMeta::new(page, limit, total).has_next,
980 "has_prev": PaginationMeta::new(page, limit, total).has_prev
981 },
982 "link_type": link_def.link_type,
983 "direction": format!("{:?}", penultimate.link_direction),
984 "description": link_def.description
985 })))
986 } else {
987 Err(ExtractorError::InvalidPath)
988 }
989 } else {
990 use crate::links::registry::LinkDirection;
993
994 for i in 0..extractor.chain.len() - 1 {
996 let current = &extractor.chain[i];
997 let next = &extractor.chain[i + 1];
998
999 if let Some(link_def) = ¤t.link_definition {
1001 let link_exists = match current.link_direction {
1002 Some(LinkDirection::Forward) => {
1003 let links = state
1004 .link_service
1005 .find_by_source(
1006 ¤t.entity_id,
1007 Some(&link_def.link_type),
1008 Some(&link_def.target_type),
1009 )
1010 .await
1011 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1012 links.iter().any(|l| l.target_id == next.entity_id)
1013 }
1014 Some(LinkDirection::Reverse) => {
1015 let links = state
1016 .link_service
1017 .find_by_target(¤t.entity_id, None, Some(&link_def.link_type))
1018 .await
1019 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1020 links.iter().any(|l| l.source_id == next.entity_id)
1021 }
1022 None => {
1023 return Err(ExtractorError::InvalidPath);
1024 }
1025 };
1026
1027 if !link_exists {
1028 return Err(ExtractorError::LinkNotFound);
1029 }
1030 }
1031 else if let Some(next_link_def) = &next.link_definition {
1033 let link_exists = match next.link_direction {
1034 Some(LinkDirection::Forward) => {
1035 let links = state
1036 .link_service
1037 .find_by_source(
1038 ¤t.entity_id,
1039 Some(&next_link_def.link_type),
1040 Some(&next_link_def.target_type),
1041 )
1042 .await
1043 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1044 links.iter().any(|l| l.target_id == next.entity_id)
1045 }
1046 Some(LinkDirection::Reverse) => {
1047 let links = state
1048 .link_service
1049 .find_by_target(
1050 ¤t.entity_id,
1051 None,
1052 Some(&next_link_def.link_type),
1053 )
1054 .await
1055 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1056 links.iter().any(|l| l.source_id == next.entity_id)
1057 }
1058 None => {
1059 return Err(ExtractorError::InvalidPath);
1060 }
1061 };
1062
1063 if !link_exists {
1064 return Err(ExtractorError::LinkNotFound);
1065 }
1066 }
1067 }
1068
1069 if let Some(link_def) = extractor.final_link_def() {
1071 let (target_id, _) = extractor.final_target();
1072 let penultimate = extractor.penultimate_segment().unwrap();
1073
1074 let link = match penultimate.link_direction {
1076 Some(LinkDirection::Forward) => {
1077 let links = state
1079 .link_service
1080 .find_by_source(
1081 &penultimate.entity_id,
1082 Some(&link_def.link_type),
1083 Some(&link_def.target_type),
1084 )
1085 .await
1086 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1087
1088 links
1089 .into_iter()
1090 .find(|l| l.target_id == target_id)
1091 .ok_or(ExtractorError::LinkNotFound)?
1092 }
1093 Some(LinkDirection::Reverse) => {
1094 let links = state
1096 .link_service
1097 .find_by_target(&penultimate.entity_id, None, Some(&link_def.link_type))
1098 .await
1099 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1100
1101 links
1102 .into_iter()
1103 .find(|l| l.source_id == target_id)
1104 .ok_or(ExtractorError::LinkNotFound)?
1105 }
1106 None => {
1107 return Err(ExtractorError::InvalidPath);
1108 }
1109 };
1110
1111 let enriched = enrich_links_with_entities(
1113 &state,
1114 vec![link],
1115 EnrichmentContext::DirectLink,
1116 link_def,
1117 )
1118 .await?;
1119
1120 Ok(Json(serde_json::json!({
1121 "link": enriched.first()
1122 })))
1123 } else {
1124 Err(ExtractorError::InvalidPath)
1125 }
1126 }
1127}
1128
1129pub async fn handle_nested_path_post(
1134 State(state): State<AppState>,
1135 Path(path): Path<String>,
1136 Json(payload): Json<CreateLinkedEntityRequest>,
1137) -> Result<Response, ExtractorError> {
1138 let segments: Vec<String> = path
1139 .trim_matches('/')
1140 .split('/')
1141 .map(|s| s.to_string())
1142 .collect();
1143
1144 if segments.len() < 5 {
1147 return Err(ExtractorError::InvalidPath);
1148 }
1149
1150 let extractor =
1151 RecursiveLinkExtractor::from_segments(segments, &state.registry, &state.config)?;
1152
1153 let link_def = extractor
1155 .final_link_def()
1156 .ok_or(ExtractorError::InvalidPath)?;
1157
1158 let (source_id, _) = extractor.final_target();
1159 let target_entity_type = &link_def.target_type;
1160
1161 let entity_creator = state
1163 .entity_creators
1164 .get(target_entity_type)
1165 .ok_or_else(|| {
1166 ExtractorError::JsonError(format!(
1167 "No entity creator registered for type: {}",
1168 target_entity_type
1169 ))
1170 })?;
1171
1172 let created_entity = entity_creator
1174 .create_from_json(payload.entity)
1175 .await
1176 .map_err(|e| ExtractorError::JsonError(format!("Failed to create entity: {}", e)))?;
1177
1178 let target_entity_id = created_entity["id"].as_str().ok_or_else(|| {
1180 ExtractorError::JsonError("Created entity missing 'id' field".to_string())
1181 })?;
1182 let target_entity_id = Uuid::parse_str(target_entity_id)
1183 .map_err(|e| ExtractorError::JsonError(format!("Invalid UUID in created entity: {}", e)))?;
1184
1185 let link = LinkEntity::new(
1187 link_def.link_type.clone(),
1188 source_id,
1189 target_entity_id,
1190 payload.metadata,
1191 );
1192
1193 let created_link = state
1194 .link_service
1195 .create(link)
1196 .await
1197 .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1198
1199 state.publish_event(FrameworkEvent::Entity(
1201 crate::core::events::EntityEvent::Created {
1202 entity_type: target_entity_type.clone(),
1203 entity_id: target_entity_id,
1204 data: created_entity.clone(),
1205 },
1206 ));
1207
1208 state.publish_event(FrameworkEvent::Link(LinkEvent::Created {
1210 link_type: created_link.link_type.clone(),
1211 link_id: created_link.id,
1212 source_id: created_link.source_id,
1213 target_id: created_link.target_id,
1214 metadata: created_link.metadata.clone(),
1215 }));
1216
1217 let response = serde_json::json!({
1218 "entity": created_entity,
1219 "link": created_link,
1220 });
1221
1222 Ok((StatusCode::CREATED, Json(response)).into_response())
1223}
1224
1225#[cfg(test)]
1226mod tests {
1227 use super::*;
1228 use crate::config::EntityConfig;
1229 use crate::core::LinkDefinition;
1230 use crate::storage::InMemoryLinkService;
1231
1232 fn create_test_state() -> AppState {
1233 let config = Arc::new(LinksConfig {
1234 entities: vec![
1235 EntityConfig {
1236 singular: "user".to_string(),
1237 plural: "users".to_string(),
1238 auth: crate::config::EntityAuthConfig::default(),
1239 },
1240 EntityConfig {
1241 singular: "car".to_string(),
1242 plural: "cars".to_string(),
1243 auth: crate::config::EntityAuthConfig::default(),
1244 },
1245 ],
1246 links: vec![LinkDefinition {
1247 link_type: "owner".to_string(),
1248 source_type: "user".to_string(),
1249 target_type: "car".to_string(),
1250 forward_route_name: "cars-owned".to_string(),
1251 reverse_route_name: "users-owners".to_string(),
1252 description: Some("User owns a car".to_string()),
1253 required_fields: None,
1254 auth: None,
1255 }],
1256 validation_rules: None,
1257 });
1258
1259 let registry = Arc::new(LinkRouteRegistry::new(config.clone()));
1260 let link_service: Arc<dyn LinkService> = Arc::new(InMemoryLinkService::new());
1261
1262 AppState {
1263 link_service,
1264 config,
1265 registry,
1266 entity_fetchers: Arc::new(HashMap::new()),
1267 entity_creators: Arc::new(HashMap::new()),
1268 event_bus: None,
1269 }
1270 }
1271
1272 #[test]
1273 fn test_state_creation() {
1274 let state = create_test_state();
1275 assert_eq!(state.config.entities.len(), 2);
1276 assert_eq!(state.config.links.len(), 1);
1277 }
1278}