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 events: None,
1258 sinks: None,
1259 });
1260
1261 let registry = Arc::new(LinkRouteRegistry::new(config.clone()));
1262 let link_service: Arc<dyn LinkService> = Arc::new(InMemoryLinkService::new());
1263
1264 AppState {
1265 link_service,
1266 config,
1267 registry,
1268 entity_fetchers: Arc::new(HashMap::new()),
1269 entity_creators: Arc::new(HashMap::new()),
1270 event_bus: None,
1271 }
1272 }
1273
1274 #[test]
1275 fn test_state_creation() {
1276 let state = create_test_state();
1277 assert_eq!(state.config.entities.len(), 2);
1278 assert_eq!(state.config.links.len(), 1);
1279 }
1280
1281 #[test]
1290 fn test_get_nested_value_top_level_key() {
1291 let json = serde_json::json!({
1292 "name": "Alice",
1293 "age": 30
1294 });
1295 let result = get_nested_value(&json, "name");
1296 assert_eq!(
1297 result,
1298 Some(serde_json::Value::String("Alice".to_string())),
1299 "should retrieve top-level string value"
1300 );
1301 }
1302
1303 #[test]
1304 fn test_get_nested_value_top_level_number() {
1305 let json = serde_json::json!({ "count": 42 });
1306 let result = get_nested_value(&json, "count");
1307 assert_eq!(
1308 result,
1309 Some(serde_json::json!(42)),
1310 "should retrieve top-level numeric value"
1311 );
1312 }
1313
1314 #[test]
1315 fn test_get_nested_value_missing_top_level_key() {
1316 let json = serde_json::json!({ "name": "Alice" });
1317 let result = get_nested_value(&json, "missing");
1318 assert_eq!(result, None, "missing top-level key should return None");
1319 }
1320
1321 #[test]
1322 fn test_get_nested_value_two_level_path() {
1323 let json = serde_json::json!({
1324 "source": { "name": "Alice", "email": "alice@example.com" },
1325 "target": { "amount": 100 }
1326 });
1327 let result = get_nested_value(&json, "source.name");
1328 assert_eq!(
1329 result,
1330 Some(serde_json::Value::String("Alice".to_string())),
1331 "should navigate two-level dot path"
1332 );
1333 }
1334
1335 #[test]
1336 fn test_get_nested_value_two_level_numeric() {
1337 let json = serde_json::json!({
1338 "target": { "amount": 99.5 }
1339 });
1340 let result = get_nested_value(&json, "target.amount");
1341 assert_eq!(
1342 result,
1343 Some(serde_json::json!(99.5)),
1344 "should retrieve nested numeric value"
1345 );
1346 }
1347
1348 #[test]
1349 fn test_get_nested_value_missing_parent() {
1350 let json = serde_json::json!({ "name": "Alice" });
1351 let result = get_nested_value(&json, "source.name");
1352 assert_eq!(result, None, "missing parent should return None");
1353 }
1354
1355 #[test]
1356 fn test_get_nested_value_missing_child() {
1357 let json = serde_json::json!({ "source": { "name": "Alice" } });
1358 let result = get_nested_value(&json, "source.missing");
1359 assert_eq!(result, None, "missing child key should return None");
1360 }
1361
1362 #[test]
1363 fn test_get_nested_value_three_levels_returns_none() {
1364 let json = serde_json::json!({
1365 "a": { "b": { "c": "deep" } }
1366 });
1367 let result = get_nested_value(&json, "a.b.c");
1368 assert_eq!(
1369 result, None,
1370 "three-level dot path should return None (only 1 or 2 levels supported)"
1371 );
1372 }
1373
1374 #[test]
1375 fn test_get_nested_value_null_value() {
1376 let json = serde_json::json!({ "field": null });
1377 let result = get_nested_value(&json, "field");
1378 assert_eq!(
1379 result,
1380 Some(serde_json::Value::Null),
1381 "null values should be returned as Some(Null)"
1382 );
1383 }
1384
1385 #[test]
1386 fn test_get_nested_value_boolean() {
1387 let json = serde_json::json!({ "active": true });
1388 let result = get_nested_value(&json, "active");
1389 assert_eq!(
1390 result,
1391 Some(serde_json::json!(true)),
1392 "should retrieve boolean value"
1393 );
1394 }
1395
1396 fn make_enriched_link(
1402 link_type: &str,
1403 status: &str,
1404 target: Option<serde_json::Value>,
1405 source: Option<serde_json::Value>,
1406 metadata: Option<serde_json::Value>,
1407 ) -> EnrichedLink {
1408 EnrichedLink {
1409 id: Uuid::new_v4(),
1410 entity_type: "link".to_string(),
1411 link_type: link_type.to_string(),
1412 source_id: Uuid::new_v4(),
1413 target_id: Uuid::new_v4(),
1414 source,
1415 target,
1416 metadata,
1417 created_at: chrono::Utc::now(),
1418 updated_at: chrono::Utc::now(),
1419 status: status.to_string(),
1420 }
1421 }
1422
1423 #[test]
1424 fn test_apply_link_filters_null_filter_returns_all() {
1425 let links = vec![
1426 make_enriched_link("owner", "active", None, None, None),
1427 make_enriched_link("driver", "active", None, None, None),
1428 ];
1429 let result = apply_link_filters(links, &serde_json::Value::Null);
1430 assert_eq!(result.len(), 2, "null filter should return all links");
1431 }
1432
1433 #[test]
1434 fn test_apply_link_filters_non_object_filter_returns_all() {
1435 let links = vec![make_enriched_link("owner", "active", None, None, None)];
1436 let result = apply_link_filters(links, &serde_json::json!("not an object"));
1437 assert_eq!(result.len(), 1, "non-object filter should return all links");
1438 }
1439
1440 #[test]
1441 fn test_apply_link_filters_empty_object_returns_all() {
1442 let links = vec![
1443 make_enriched_link("owner", "active", None, None, None),
1444 make_enriched_link("driver", "inactive", None, None, None),
1445 ];
1446 let result = apply_link_filters(links, &serde_json::json!({}));
1447 assert_eq!(
1448 result.len(),
1449 2,
1450 "empty object filter should return all links"
1451 );
1452 }
1453
1454 #[test]
1455 fn test_apply_link_filters_by_status() {
1456 let links = vec![
1457 make_enriched_link("owner", "active", None, None, None),
1458 make_enriched_link("driver", "inactive", None, None, None),
1459 make_enriched_link("owner", "active", None, None, None),
1460 ];
1461 let filter = serde_json::json!({ "status": "active" });
1462 let result = apply_link_filters(links, &filter);
1463 assert_eq!(result.len(), 2, "should filter to only active links");
1464 for link in &result {
1465 assert_eq!(link.status, "active");
1466 }
1467 }
1468
1469 #[test]
1470 fn test_apply_link_filters_by_link_type() {
1471 let links = vec![
1472 make_enriched_link("owner", "active", None, None, None),
1473 make_enriched_link("driver", "active", None, None, None),
1474 make_enriched_link("owner", "active", None, None, None),
1475 ];
1476 let filter = serde_json::json!({ "link_type": "owner" });
1477 let result = apply_link_filters(links, &filter);
1478 assert_eq!(result.len(), 2, "should filter to only 'owner' links");
1479 }
1480
1481 #[test]
1482 fn test_apply_link_filters_by_nested_target_field() {
1483 let links = vec![
1484 make_enriched_link(
1485 "owner",
1486 "active",
1487 Some(serde_json::json!({ "name": "Car A" })),
1488 None,
1489 None,
1490 ),
1491 make_enriched_link(
1492 "owner",
1493 "active",
1494 Some(serde_json::json!({ "name": "Car B" })),
1495 None,
1496 None,
1497 ),
1498 ];
1499 let filter = serde_json::json!({ "target.name": "Car A" });
1500 let result = apply_link_filters(links, &filter);
1501 assert_eq!(result.len(), 1, "should filter by nested target.name");
1502 }
1503
1504 #[test]
1505 fn test_apply_link_filters_by_nested_source_field() {
1506 let links = vec![
1507 make_enriched_link(
1508 "owner",
1509 "active",
1510 None,
1511 Some(serde_json::json!({ "email": "alice@test.com" })),
1512 None,
1513 ),
1514 make_enriched_link(
1515 "owner",
1516 "active",
1517 None,
1518 Some(serde_json::json!({ "email": "bob@test.com" })),
1519 None,
1520 ),
1521 ];
1522 let filter = serde_json::json!({ "source.email": "bob@test.com" });
1523 let result = apply_link_filters(links, &filter);
1524 assert_eq!(result.len(), 1, "should filter by nested source.email");
1525 }
1526
1527 #[test]
1528 fn test_apply_link_filters_multiple_criteria() {
1529 let links = vec![
1530 make_enriched_link("owner", "active", None, None, None),
1531 make_enriched_link("owner", "inactive", None, None, None),
1532 make_enriched_link("driver", "active", None, None, None),
1533 ];
1534 let filter = serde_json::json!({ "link_type": "owner", "status": "active" });
1535 let result = apply_link_filters(links, &filter);
1536 assert_eq!(
1537 result.len(),
1538 1,
1539 "should filter by both link_type AND status"
1540 );
1541 assert_eq!(result[0].link_type, "owner");
1542 assert_eq!(result[0].status, "active");
1543 }
1544
1545 #[test]
1546 fn test_apply_link_filters_no_match_returns_empty() {
1547 let links = vec![make_enriched_link("owner", "active", None, None, None)];
1548 let filter = serde_json::json!({ "status": "deleted" });
1549 let result = apply_link_filters(links, &filter);
1550 assert!(
1551 result.is_empty(),
1552 "non-matching filter should return empty vec"
1553 );
1554 }
1555
1556 #[test]
1557 fn test_apply_link_filters_missing_field_excludes_link() {
1558 let links = vec![make_enriched_link("owner", "active", None, None, None)];
1559 let filter = serde_json::json!({ "nonexistent_field": "value" });
1561 let result = apply_link_filters(links, &filter);
1562 assert!(
1563 result.is_empty(),
1564 "filtering by a missing field should exclude the link"
1565 );
1566 }
1567
1568 fn make_link_def_with_auth() -> LinkDefinition {
1573 use crate::core::link::LinkAuthConfig;
1574 LinkDefinition {
1575 link_type: "test".to_string(),
1576 source_type: "a".to_string(),
1577 target_type: "b".to_string(),
1578 forward_route_name: "bs".to_string(),
1579 reverse_route_name: "as".to_string(),
1580 description: None,
1581 required_fields: None,
1582 auth: Some(LinkAuthConfig {
1583 list: "public".to_string(),
1584 get: "authenticated".to_string(),
1585 create: "admin_only".to_string(),
1586 update: "owner".to_string(),
1587 delete: "service_only".to_string(),
1588 }),
1589 }
1590 }
1591
1592 #[test]
1593 fn test_get_link_auth_policy_list() {
1594 let def = make_link_def_with_auth();
1595 let result = AppState::get_link_auth_policy(&def, "list");
1596 assert_eq!(
1597 result,
1598 Some("public".to_string()),
1599 "list operation should return 'public' policy"
1600 );
1601 }
1602
1603 #[test]
1604 fn test_get_link_auth_policy_get() {
1605 let def = make_link_def_with_auth();
1606 let result = AppState::get_link_auth_policy(&def, "get");
1607 assert_eq!(result, Some("authenticated".to_string()));
1608 }
1609
1610 #[test]
1611 fn test_get_link_auth_policy_create() {
1612 let def = make_link_def_with_auth();
1613 let result = AppState::get_link_auth_policy(&def, "create");
1614 assert_eq!(result, Some("admin_only".to_string()));
1615 }
1616
1617 #[test]
1618 fn test_get_link_auth_policy_update() {
1619 let def = make_link_def_with_auth();
1620 let result = AppState::get_link_auth_policy(&def, "update");
1621 assert_eq!(result, Some("owner".to_string()));
1622 }
1623
1624 #[test]
1625 fn test_get_link_auth_policy_delete() {
1626 let def = make_link_def_with_auth();
1627 let result = AppState::get_link_auth_policy(&def, "delete");
1628 assert_eq!(result, Some("service_only".to_string()));
1629 }
1630
1631 #[test]
1632 fn test_get_link_auth_policy_unknown_operation() {
1633 let def = make_link_def_with_auth();
1634 let result = AppState::get_link_auth_policy(&def, "unknown_op");
1635 assert_eq!(
1636 result,
1637 Some("authenticated".to_string()),
1638 "unknown operations should default to 'authenticated'"
1639 );
1640 }
1641
1642 #[test]
1643 fn test_get_link_auth_policy_no_auth_config() {
1644 let def = LinkDefinition {
1645 link_type: "test".to_string(),
1646 source_type: "a".to_string(),
1647 target_type: "b".to_string(),
1648 forward_route_name: "bs".to_string(),
1649 reverse_route_name: "as".to_string(),
1650 description: None,
1651 required_fields: None,
1652 auth: None,
1653 };
1654 let result = AppState::get_link_auth_policy(&def, "list");
1655 assert_eq!(
1656 result, None,
1657 "should return None when no auth config is set"
1658 );
1659 }
1660
1661 #[test]
1666 fn test_publish_event_no_event_bus_does_not_panic() {
1667 let state = create_test_state();
1668 state.publish_event(FrameworkEvent::Link(LinkEvent::Created {
1670 link_type: "owner".to_string(),
1671 link_id: Uuid::new_v4(),
1672 source_id: Uuid::new_v4(),
1673 target_id: Uuid::new_v4(),
1674 metadata: None,
1675 }));
1676 }
1678
1679 #[test]
1680 fn test_publish_event_with_event_bus() {
1681 let bus = Arc::new(EventBus::new(16));
1682 let mut state = create_test_state();
1683 state.event_bus = Some(bus.clone());
1684
1685 let mut rx = bus.subscribe();
1686
1687 state.publish_event(FrameworkEvent::Link(LinkEvent::Created {
1688 link_type: "owner".to_string(),
1689 link_id: Uuid::new_v4(),
1690 source_id: Uuid::new_v4(),
1691 target_id: Uuid::new_v4(),
1692 metadata: None,
1693 }));
1694
1695 let envelope = rx.try_recv().expect("should receive published event");
1697 assert!(
1698 matches!(
1699 envelope.event,
1700 FrameworkEvent::Link(LinkEvent::Created { .. })
1701 ),
1702 "received event should be a Link::Created"
1703 );
1704 }
1705
1706 fn create_chain_test_state() -> AppState {
1712 let config = Arc::new(LinksConfig {
1713 entities: vec![
1714 EntityConfig {
1715 singular: "order".to_string(),
1716 plural: "orders".to_string(),
1717 auth: crate::config::EntityAuthConfig::default(),
1718 },
1719 EntityConfig {
1720 singular: "invoice".to_string(),
1721 plural: "invoices".to_string(),
1722 auth: crate::config::EntityAuthConfig::default(),
1723 },
1724 EntityConfig {
1725 singular: "payment".to_string(),
1726 plural: "payments".to_string(),
1727 auth: crate::config::EntityAuthConfig::default(),
1728 },
1729 ],
1730 links: vec![
1731 LinkDefinition {
1732 link_type: "billing".to_string(),
1733 source_type: "order".to_string(),
1734 target_type: "invoice".to_string(),
1735 forward_route_name: "invoices".to_string(),
1736 reverse_route_name: "order".to_string(),
1737 description: Some("Order has invoices".to_string()),
1738 required_fields: None,
1739 auth: None,
1740 },
1741 LinkDefinition {
1742 link_type: "payment".to_string(),
1743 source_type: "invoice".to_string(),
1744 target_type: "payment".to_string(),
1745 forward_route_name: "payments".to_string(),
1746 reverse_route_name: "invoice".to_string(),
1747 description: None,
1748 required_fields: None,
1749 auth: None,
1750 },
1751 ],
1752 validation_rules: None,
1753 events: None,
1754 sinks: None,
1755 });
1756
1757 let registry = Arc::new(LinkRouteRegistry::new(config.clone()));
1758 let link_service: Arc<dyn LinkService> = Arc::new(InMemoryLinkService::new());
1759
1760 AppState {
1761 link_service,
1762 config,
1763 registry,
1764 entity_fetchers: Arc::new(HashMap::new()),
1765 entity_creators: Arc::new(HashMap::new()),
1766 event_bus: None,
1767 }
1768 }
1769
1770 struct MockEntityFetcher {
1772 entities: std::sync::RwLock<HashMap<Uuid, serde_json::Value>>,
1773 }
1774
1775 impl MockEntityFetcher {
1776 fn new() -> Self {
1777 Self {
1778 entities: std::sync::RwLock::new(HashMap::new()),
1779 }
1780 }
1781
1782 fn insert(&self, id: Uuid, data: serde_json::Value) {
1783 self.entities
1784 .write()
1785 .expect("lock should not be poisoned")
1786 .insert(id, data);
1787 }
1788 }
1789
1790 #[async_trait::async_trait]
1791 impl crate::core::EntityFetcher for MockEntityFetcher {
1792 async fn fetch_as_json(&self, entity_id: &Uuid) -> anyhow::Result<serde_json::Value> {
1793 let entities = self
1794 .entities
1795 .read()
1796 .map_err(|e| anyhow::anyhow!("lock error: {}", e))?;
1797 entities
1798 .get(entity_id)
1799 .cloned()
1800 .ok_or_else(|| anyhow::anyhow!("Entity not found: {}", entity_id))
1801 }
1802 }
1803
1804 struct MockEntityCreator;
1806
1807 #[async_trait::async_trait]
1808 impl crate::core::EntityCreator for MockEntityCreator {
1809 async fn create_from_json(
1810 &self,
1811 entity_data: serde_json::Value,
1812 ) -> anyhow::Result<serde_json::Value> {
1813 let mut data = entity_data;
1814 if data.get("id").is_none() {
1815 data["id"] = serde_json::json!(Uuid::new_v4().to_string());
1816 }
1817 Ok(data)
1818 }
1819 }
1820
1821 #[tokio::test]
1826 async fn test_enrich_links_from_source_omits_source() {
1827 let state = create_test_state();
1828 let user_id = Uuid::new_v4();
1829 let car_id = Uuid::new_v4();
1830 let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
1831
1832 let link_def = &state.config.links[0];
1833 let enriched =
1834 enrich_links_with_entities(&state, vec![link], EnrichmentContext::FromSource, link_def)
1835 .await
1836 .expect("enrichment should succeed");
1837
1838 assert_eq!(enriched.len(), 1);
1839 assert!(
1840 enriched[0].source.is_none(),
1841 "FromSource context should omit source entity"
1842 );
1843 assert!(enriched[0].target.is_none());
1845 }
1846
1847 #[tokio::test]
1848 async fn test_enrich_links_from_target_omits_target() {
1849 let state = create_test_state();
1850 let user_id = Uuid::new_v4();
1851 let car_id = Uuid::new_v4();
1852 let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
1853
1854 let link_def = &state.config.links[0];
1855 let enriched =
1856 enrich_links_with_entities(&state, vec![link], EnrichmentContext::FromTarget, link_def)
1857 .await
1858 .expect("enrichment should succeed");
1859
1860 assert_eq!(enriched.len(), 1);
1861 assert!(
1862 enriched[0].target.is_none(),
1863 "FromTarget context should omit target entity"
1864 );
1865 }
1866
1867 #[tokio::test]
1868 async fn test_enrich_links_with_fetcher_includes_entity() {
1869 let user_id = Uuid::new_v4();
1870 let car_id = Uuid::new_v4();
1871
1872 let car_fetcher = Arc::new(MockEntityFetcher::new());
1873 car_fetcher.insert(
1874 car_id,
1875 serde_json::json!({ "id": car_id.to_string(), "model": "Tesla" }),
1876 );
1877
1878 let mut fetchers: HashMap<String, Arc<dyn crate::core::EntityFetcher>> = HashMap::new();
1879 fetchers.insert("car".to_string(), car_fetcher);
1880
1881 let mut state = create_test_state();
1882 state.entity_fetchers = Arc::new(fetchers);
1883
1884 let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
1885 let link_def = &state.config.links[0];
1886
1887 let enriched =
1888 enrich_links_with_entities(&state, vec![link], EnrichmentContext::FromSource, link_def)
1889 .await
1890 .expect("enrichment should succeed");
1891
1892 assert_eq!(enriched.len(), 1);
1893 let target = enriched[0]
1894 .target
1895 .as_ref()
1896 .expect("target entity should be fetched");
1897 assert_eq!(target["model"], "Tesla");
1898 }
1899
1900 #[tokio::test]
1901 async fn test_enrich_links_direct_link_context() {
1902 let user_id = Uuid::new_v4();
1903 let car_id = Uuid::new_v4();
1904
1905 let user_fetcher = Arc::new(MockEntityFetcher::new());
1906 user_fetcher.insert(
1907 user_id,
1908 serde_json::json!({ "id": user_id.to_string(), "name": "Alice" }),
1909 );
1910
1911 let car_fetcher = Arc::new(MockEntityFetcher::new());
1912 car_fetcher.insert(
1913 car_id,
1914 serde_json::json!({ "id": car_id.to_string(), "model": "BMW" }),
1915 );
1916
1917 let mut fetchers: HashMap<String, Arc<dyn crate::core::EntityFetcher>> = HashMap::new();
1918 fetchers.insert("user".to_string(), user_fetcher);
1919 fetchers.insert("car".to_string(), car_fetcher);
1920
1921 let mut state = create_test_state();
1922 state.entity_fetchers = Arc::new(fetchers);
1923
1924 let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
1925 let link_def = &state.config.links[0];
1926
1927 let enriched =
1928 enrich_links_with_entities(&state, vec![link], EnrichmentContext::DirectLink, link_def)
1929 .await
1930 .expect("enrichment should succeed");
1931
1932 assert_eq!(enriched.len(), 1);
1933 assert!(
1934 enriched[0].source.is_some(),
1935 "DirectLink context should include source"
1936 );
1937 assert!(
1938 enriched[0].target.is_some(),
1939 "DirectLink context should include target"
1940 );
1941 assert_eq!(
1942 enriched[0].source.as_ref().expect("source")["name"],
1943 "Alice"
1944 );
1945 assert_eq!(enriched[0].target.as_ref().expect("target")["model"], "BMW");
1946 }
1947
1948 #[tokio::test]
1949 async fn test_enrich_links_preserves_metadata() {
1950 let state = create_test_state();
1951 let metadata = serde_json::json!({ "role": "primary" });
1952 let link = crate::core::link::LinkEntity::new(
1953 "owner",
1954 Uuid::new_v4(),
1955 Uuid::new_v4(),
1956 Some(metadata.clone()),
1957 );
1958
1959 let link_def = &state.config.links[0];
1960 let enriched =
1961 enrich_links_with_entities(&state, vec![link], EnrichmentContext::FromSource, link_def)
1962 .await
1963 .expect("enrichment should succeed");
1964
1965 assert_eq!(enriched[0].metadata, Some(metadata));
1966 }
1967
1968 #[tokio::test]
1969 async fn test_enrich_links_empty_input() {
1970 let state = create_test_state();
1971 let link_def = &state.config.links[0];
1972 let enriched =
1973 enrich_links_with_entities(&state, vec![], EnrichmentContext::FromSource, link_def)
1974 .await
1975 .expect("enrichment should succeed");
1976 assert!(
1977 enriched.is_empty(),
1978 "enriching empty vec should return empty vec"
1979 );
1980 }
1981
1982 #[tokio::test]
1987 async fn test_fetch_entity_by_type_no_fetcher_registered() {
1988 let state = create_test_state();
1989 let result = fetch_entity_by_type(&state, "unknown_type", &Uuid::new_v4()).await;
1990 assert!(
1991 result.is_err(),
1992 "should error when no fetcher is registered"
1993 );
1994 let err_msg = result.unwrap_err().to_string();
1995 assert!(
1996 err_msg.contains("No entity fetcher registered"),
1997 "error should mention missing fetcher, got: {}",
1998 err_msg
1999 );
2000 }
2001
2002 #[tokio::test]
2003 async fn test_fetch_entity_by_type_entity_not_found() {
2004 let fetcher = Arc::new(MockEntityFetcher::new());
2005 let mut fetchers: HashMap<String, Arc<dyn crate::core::EntityFetcher>> = HashMap::new();
2006 fetchers.insert("car".to_string(), fetcher);
2007
2008 let mut state = create_test_state();
2009 state.entity_fetchers = Arc::new(fetchers);
2010
2011 let result = fetch_entity_by_type(&state, "car", &Uuid::new_v4()).await;
2012 assert!(result.is_err(), "should error when entity is not found");
2013 }
2014
2015 #[tokio::test]
2016 async fn test_fetch_entity_by_type_success() {
2017 let car_id = Uuid::new_v4();
2018 let fetcher = Arc::new(MockEntityFetcher::new());
2019 fetcher.insert(
2020 car_id,
2021 serde_json::json!({ "id": car_id.to_string(), "model": "Audi" }),
2022 );
2023
2024 let mut fetchers: HashMap<String, Arc<dyn crate::core::EntityFetcher>> = HashMap::new();
2025 fetchers.insert("car".to_string(), fetcher);
2026
2027 let mut state = create_test_state();
2028 state.entity_fetchers = Arc::new(fetchers);
2029
2030 let result = fetch_entity_by_type(&state, "car", &car_id)
2031 .await
2032 .expect("should succeed");
2033 assert_eq!(result["model"], "Audi");
2034 }
2035
2036 #[tokio::test]
2041 async fn test_list_links_forward_empty() {
2042 let state = create_test_state();
2043 let user_id = Uuid::new_v4();
2044
2045 let result = list_links(
2046 State(state),
2047 Path(("users".to_string(), user_id, "cars-owned".to_string())),
2048 Query(crate::core::query::QueryParams::default()),
2049 )
2050 .await
2051 .expect("handler should succeed");
2052
2053 let resp = result.0;
2054 assert_eq!(resp.data.len(), 0);
2055 assert_eq!(resp.pagination.total, 0);
2056 assert_eq!(resp.link_type, "owner");
2057 assert_eq!(resp.direction, "Forward");
2058 }
2059
2060 #[tokio::test]
2061 async fn test_list_links_forward_with_links() {
2062 let state = create_test_state();
2063 let user_id = Uuid::new_v4();
2064 let car1_id = Uuid::new_v4();
2065 let car2_id = Uuid::new_v4();
2066
2067 let link1 = crate::core::link::LinkEntity::new("owner", user_id, car1_id, None);
2069 let link2 = crate::core::link::LinkEntity::new("owner", user_id, car2_id, None);
2070 state
2071 .link_service
2072 .create(link1)
2073 .await
2074 .expect("create should succeed");
2075 state
2076 .link_service
2077 .create(link2)
2078 .await
2079 .expect("create should succeed");
2080
2081 let result = list_links(
2082 State(state),
2083 Path(("users".to_string(), user_id, "cars-owned".to_string())),
2084 Query(crate::core::query::QueryParams::default()),
2085 )
2086 .await
2087 .expect("handler should succeed");
2088
2089 let resp = result.0;
2090 assert_eq!(resp.data.len(), 2);
2091 assert_eq!(resp.pagination.total, 2);
2092 }
2093
2094 #[tokio::test]
2095 async fn test_list_links_reverse() {
2096 let state = create_test_state();
2097 let user_id = Uuid::new_v4();
2098 let car_id = Uuid::new_v4();
2099
2100 let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
2101 state
2102 .link_service
2103 .create(link)
2104 .await
2105 .expect("create should succeed");
2106
2107 let result = list_links(
2108 State(state),
2109 Path(("cars".to_string(), car_id, "users-owners".to_string())),
2110 Query(crate::core::query::QueryParams::default()),
2111 )
2112 .await
2113 .expect("handler should succeed");
2114
2115 let resp = result.0;
2116 assert_eq!(resp.data.len(), 1);
2117 assert_eq!(resp.direction, "Reverse");
2118 }
2119
2120 #[tokio::test]
2121 async fn test_list_links_invalid_route() {
2122 let state = create_test_state();
2123 let result = list_links(
2124 State(state),
2125 Path((
2126 "users".to_string(),
2127 Uuid::new_v4(),
2128 "nonexistent".to_string(),
2129 )),
2130 Query(crate::core::query::QueryParams::default()),
2131 )
2132 .await;
2133
2134 assert!(result.is_err(), "should fail with invalid route");
2135 }
2136
2137 #[tokio::test]
2138 async fn test_list_links_pagination() {
2139 let state = create_test_state();
2140 let user_id = Uuid::new_v4();
2141
2142 for _ in 0..5 {
2144 let car_id = Uuid::new_v4();
2145 let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
2146 state
2147 .link_service
2148 .create(link)
2149 .await
2150 .expect("create should succeed");
2151 }
2152
2153 let params = crate::core::query::QueryParams {
2154 page: 1,
2155 limit: 2,
2156 filter: None,
2157 sort: None,
2158 };
2159
2160 let result = list_links(
2161 State(state),
2162 Path(("users".to_string(), user_id, "cars-owned".to_string())),
2163 Query(params),
2164 )
2165 .await
2166 .expect("handler should succeed");
2167
2168 let resp = result.0;
2169 assert_eq!(resp.data.len(), 2, "page 1 should have 2 items");
2170 assert_eq!(resp.pagination.total, 5);
2171 assert_eq!(resp.pagination.total_pages, 3);
2172 assert!(resp.pagination.has_next);
2173 assert!(!resp.pagination.has_prev);
2174 }
2175
2176 #[tokio::test]
2177 async fn test_list_links_with_filter() {
2178 let state = create_test_state();
2179 let user_id = Uuid::new_v4();
2180
2181 let car1_id = Uuid::new_v4();
2183 let car2_id = Uuid::new_v4();
2184 let link1 = crate::core::link::LinkEntity::new("owner", user_id, car1_id, None);
2185 let mut link2 = crate::core::link::LinkEntity::new("owner", user_id, car2_id, None);
2186 link2.status = "inactive".to_string();
2187
2188 state
2189 .link_service
2190 .create(link1)
2191 .await
2192 .expect("create should succeed");
2193 state
2194 .link_service
2195 .create(link2)
2196 .await
2197 .expect("create should succeed");
2198
2199 let params = crate::core::query::QueryParams {
2200 page: 1,
2201 limit: 20,
2202 filter: Some(r#"{"status": "active"}"#.to_string()),
2203 sort: None,
2204 };
2205
2206 let result = list_links(
2207 State(state),
2208 Path(("users".to_string(), user_id, "cars-owned".to_string())),
2209 Query(params),
2210 )
2211 .await
2212 .expect("handler should succeed");
2213
2214 let resp = result.0;
2215 assert_eq!(resp.data.len(), 1, "filter should return only active links");
2216 assert_eq!(resp.data[0].status, "active");
2217 }
2218
2219 #[tokio::test]
2224 async fn test_get_link_not_found() {
2225 let state = create_test_state();
2226 let result = get_link(State(state), Path(Uuid::new_v4())).await;
2227 assert!(result.is_err(), "should fail for nonexistent link");
2228 }
2229
2230 #[tokio::test]
2231 async fn test_get_link_success() {
2232 let state = create_test_state();
2233 let user_id = Uuid::new_v4();
2234 let car_id = Uuid::new_v4();
2235 let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
2236 let link_id = link.id;
2237
2238 state
2239 .link_service
2240 .create(link)
2241 .await
2242 .expect("create should succeed");
2243
2244 let result = get_link(State(state), Path(link_id)).await;
2245 assert!(result.is_ok(), "should succeed for existing link");
2246 }
2247
2248 #[tokio::test]
2253 async fn test_create_link_success() {
2254 let state = create_test_state();
2255 let user_id = Uuid::new_v4();
2256 let car_id = Uuid::new_v4();
2257
2258 let result = create_link(
2259 State(state.clone()),
2260 Path((
2261 "users".to_string(),
2262 user_id,
2263 "cars-owned".to_string(),
2264 car_id,
2265 )),
2266 Json(CreateLinkRequest { metadata: None }),
2267 )
2268 .await;
2269
2270 assert!(result.is_ok(), "create_link should succeed");
2271 let response = result.expect("should be ok");
2272 assert_eq!(response.status(), StatusCode::CREATED);
2273
2274 let links = state
2276 .link_service
2277 .find_by_source(&user_id, Some("owner"), None)
2278 .await
2279 .expect("find_by_source should succeed");
2280 assert_eq!(links.len(), 1);
2281 assert_eq!(links[0].source_id, user_id);
2282 assert_eq!(links[0].target_id, car_id);
2283 }
2284
2285 #[tokio::test]
2286 async fn test_create_link_with_metadata() {
2287 let state = create_test_state();
2288 let user_id = Uuid::new_v4();
2289 let car_id = Uuid::new_v4();
2290
2291 let metadata = serde_json::json!({ "primary_owner": true });
2292
2293 let result = create_link(
2294 State(state.clone()),
2295 Path((
2296 "users".to_string(),
2297 user_id,
2298 "cars-owned".to_string(),
2299 car_id,
2300 )),
2301 Json(CreateLinkRequest {
2302 metadata: Some(metadata.clone()),
2303 }),
2304 )
2305 .await;
2306
2307 assert!(result.is_ok());
2308
2309 let links = state
2310 .link_service
2311 .find_by_source(&user_id, Some("owner"), None)
2312 .await
2313 .expect("find_by_source should succeed");
2314 assert_eq!(links[0].metadata, Some(metadata));
2315 }
2316
2317 #[tokio::test]
2318 async fn test_create_link_invalid_route() {
2319 let state = create_test_state();
2320 let result = create_link(
2321 State(state),
2322 Path((
2323 "users".to_string(),
2324 Uuid::new_v4(),
2325 "nonexistent".to_string(),
2326 Uuid::new_v4(),
2327 )),
2328 Json(CreateLinkRequest { metadata: None }),
2329 )
2330 .await;
2331
2332 assert!(result.is_err(), "should fail with invalid route");
2333 }
2334
2335 #[tokio::test]
2340 async fn test_delete_link_success() {
2341 let state = create_test_state();
2342 let user_id = Uuid::new_v4();
2343 let car_id = Uuid::new_v4();
2344
2345 let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
2347 state
2348 .link_service
2349 .create(link)
2350 .await
2351 .expect("create should succeed");
2352
2353 let result = delete_link(
2355 State(state.clone()),
2356 Path((
2357 "users".to_string(),
2358 user_id,
2359 "cars-owned".to_string(),
2360 car_id,
2361 )),
2362 )
2363 .await;
2364
2365 assert!(result.is_ok(), "delete_link should succeed");
2366 let response = result.expect("should be ok");
2367 assert_eq!(response.status(), StatusCode::NO_CONTENT);
2368
2369 let links = state
2371 .link_service
2372 .find_by_source(&user_id, Some("owner"), None)
2373 .await
2374 .expect("find_by_source should succeed");
2375 assert!(links.is_empty(), "link should be deleted");
2376 }
2377
2378 #[tokio::test]
2379 async fn test_delete_link_not_found() {
2380 let state = create_test_state();
2381 let result = delete_link(
2382 State(state),
2383 Path((
2384 "users".to_string(),
2385 Uuid::new_v4(),
2386 "cars-owned".to_string(),
2387 Uuid::new_v4(),
2388 )),
2389 )
2390 .await;
2391
2392 assert!(result.is_err(), "should fail when link does not exist");
2393 }
2394
2395 #[tokio::test]
2400 async fn test_create_linked_entity_success() {
2401 let mut state = create_test_state();
2402 let user_id = Uuid::new_v4();
2403
2404 let mut creators: HashMap<String, Arc<dyn crate::core::EntityCreator>> = HashMap::new();
2406 creators.insert("car".to_string(), Arc::new(MockEntityCreator));
2407 state.entity_creators = Arc::new(creators);
2408
2409 let entity_data = serde_json::json!({ "model": "Tesla Model 3", "year": 2024 });
2410
2411 let result = create_linked_entity(
2412 State(state.clone()),
2413 Path(("users".to_string(), user_id, "cars-owned".to_string())),
2414 Json(CreateLinkedEntityRequest {
2415 entity: entity_data,
2416 metadata: None,
2417 }),
2418 )
2419 .await;
2420
2421 assert!(result.is_ok(), "create_linked_entity should succeed");
2422 let response = result.expect("should be ok");
2423 assert_eq!(response.status(), StatusCode::CREATED);
2424
2425 let links = state
2427 .link_service
2428 .find_by_source(&user_id, Some("owner"), None)
2429 .await
2430 .expect("find_by_source should succeed");
2431 assert_eq!(links.len(), 1, "a link should have been created");
2432 }
2433
2434 #[tokio::test]
2435 async fn test_create_linked_entity_no_creator_registered() {
2436 let state = create_test_state();
2437 let user_id = Uuid::new_v4();
2438
2439 let result = create_linked_entity(
2440 State(state),
2441 Path(("users".to_string(), user_id, "cars-owned".to_string())),
2442 Json(CreateLinkedEntityRequest {
2443 entity: serde_json::json!({}),
2444 metadata: None,
2445 }),
2446 )
2447 .await;
2448
2449 assert!(
2450 result.is_err(),
2451 "should fail when no entity creator is registered"
2452 );
2453 }
2454
2455 #[tokio::test]
2460 async fn test_update_link_success() {
2461 let state = create_test_state();
2462 let user_id = Uuid::new_v4();
2463 let car_id = Uuid::new_v4();
2464
2465 let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
2466 state
2467 .link_service
2468 .create(link)
2469 .await
2470 .expect("create should succeed");
2471
2472 let new_metadata = serde_json::json!({ "insured": true });
2473
2474 let result = update_link(
2475 State(state.clone()),
2476 Path((
2477 "users".to_string(),
2478 user_id,
2479 "cars-owned".to_string(),
2480 car_id,
2481 )),
2482 Json(CreateLinkRequest {
2483 metadata: Some(new_metadata.clone()),
2484 }),
2485 )
2486 .await;
2487
2488 assert!(result.is_ok(), "update_link should succeed");
2489
2490 let links = state
2492 .link_service
2493 .find_by_source(&user_id, Some("owner"), None)
2494 .await
2495 .expect("find_by_source should succeed");
2496 assert_eq!(links[0].metadata, Some(new_metadata));
2497 }
2498
2499 #[tokio::test]
2500 async fn test_update_link_not_found() {
2501 let state = create_test_state();
2502 let result = update_link(
2503 State(state),
2504 Path((
2505 "users".to_string(),
2506 Uuid::new_v4(),
2507 "cars-owned".to_string(),
2508 Uuid::new_v4(),
2509 )),
2510 Json(CreateLinkRequest { metadata: None }),
2511 )
2512 .await;
2513
2514 assert!(result.is_err(), "should fail when link does not exist");
2515 }
2516
2517 #[tokio::test]
2522 async fn test_get_link_by_route_forward_success() {
2523 let state = create_test_state();
2524 let user_id = Uuid::new_v4();
2525 let car_id = Uuid::new_v4();
2526
2527 let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
2528 state
2529 .link_service
2530 .create(link)
2531 .await
2532 .expect("create should succeed");
2533
2534 let result = get_link_by_route(
2535 State(state),
2536 Path((
2537 "users".to_string(),
2538 user_id,
2539 "cars-owned".to_string(),
2540 car_id,
2541 )),
2542 )
2543 .await;
2544
2545 assert!(result.is_ok(), "get_link_by_route should succeed");
2546 }
2547
2548 #[tokio::test]
2549 async fn test_get_link_by_route_not_found() {
2550 let state = create_test_state();
2551 let result = get_link_by_route(
2552 State(state),
2553 Path((
2554 "users".to_string(),
2555 Uuid::new_v4(),
2556 "cars-owned".to_string(),
2557 Uuid::new_v4(),
2558 )),
2559 )
2560 .await;
2561
2562 assert!(result.is_err(), "should fail when link does not exist");
2563 }
2564
2565 #[tokio::test]
2570 async fn test_list_available_links_known_entity() {
2571 let state = create_test_state();
2572 let user_id = Uuid::new_v4();
2573
2574 let result = list_available_links(State(state), Path(("users".to_string(), user_id)))
2575 .await
2576 .expect("handler should succeed");
2577
2578 let resp = result.0;
2579 assert_eq!(resp.entity_type, "user");
2580 assert_eq!(resp.entity_id, user_id);
2581 assert!(
2583 !resp.available_routes.is_empty(),
2584 "user should have available routes"
2585 );
2586 }
2587
2588 #[tokio::test]
2589 async fn test_list_available_links_car_has_reverse_routes() {
2590 let state = create_test_state();
2591 let car_id = Uuid::new_v4();
2592
2593 let result = list_available_links(State(state), Path(("cars".to_string(), car_id)))
2594 .await
2595 .expect("handler should succeed");
2596
2597 let resp = result.0;
2598 assert_eq!(resp.entity_type, "car");
2599 assert!(
2600 !resp.available_routes.is_empty(),
2601 "car should have reverse routes"
2602 );
2603 let route_names: Vec<&str> = resp
2605 .available_routes
2606 .iter()
2607 .map(|r| r.path.as_str())
2608 .collect();
2609 let has_owners = route_names.iter().any(|p| p.contains("users-owners"));
2610 assert!(has_owners, "car should have users-owners route");
2611 }
2612
2613 #[tokio::test]
2618 async fn test_handle_nested_path_get_too_few_segments() {
2619 let state = create_chain_test_state();
2620 let result = handle_nested_path_get(
2622 State(state),
2623 Path("orders/abc/invoices".to_string()),
2624 Query(crate::core::query::QueryParams::default()),
2625 )
2626 .await;
2627
2628 assert!(result.is_err(), "should fail with fewer than 5 segments");
2629 }
2630
2631 #[tokio::test]
2632 async fn test_handle_nested_path_get_list_returns_paginated() {
2633 let state = create_chain_test_state();
2634
2635 let order_id = Uuid::new_v4();
2636 let invoice_id = Uuid::new_v4();
2637 let payment_id = Uuid::new_v4();
2638
2639 let link1 = crate::core::link::LinkEntity::new("billing", order_id, invoice_id, None);
2641 state
2642 .link_service
2643 .create(link1)
2644 .await
2645 .expect("create should succeed");
2646
2647 let link2 = crate::core::link::LinkEntity::new("payment", invoice_id, payment_id, None);
2648 state
2649 .link_service
2650 .create(link2)
2651 .await
2652 .expect("create should succeed");
2653
2654 let path = format!("orders/{}/invoices/{}/payments", order_id, invoice_id);
2656 let result = handle_nested_path_get(
2657 State(state),
2658 Path(path),
2659 Query(crate::core::query::QueryParams::default()),
2660 )
2661 .await
2662 .expect("handler should succeed");
2663
2664 let json = result.0;
2665 assert!(
2666 json.get("data").is_some(),
2667 "response should contain 'data' field"
2668 );
2669 assert!(
2670 json.get("pagination").is_some(),
2671 "response should contain 'pagination' field"
2672 );
2673 let data = json["data"].as_array().expect("data should be an array");
2674 assert_eq!(data.len(), 1, "should find one payment link");
2675 }
2676
2677 #[tokio::test]
2678 async fn test_handle_nested_path_get_specific_item() {
2679 let state = create_chain_test_state();
2680
2681 let order_id = Uuid::new_v4();
2682 let invoice_id = Uuid::new_v4();
2683 let payment_id = Uuid::new_v4();
2684
2685 let link1 = crate::core::link::LinkEntity::new("billing", order_id, invoice_id, None);
2686 state
2687 .link_service
2688 .create(link1)
2689 .await
2690 .expect("create should succeed");
2691
2692 let link2 = crate::core::link::LinkEntity::new("payment", invoice_id, payment_id, None);
2693 state
2694 .link_service
2695 .create(link2)
2696 .await
2697 .expect("create should succeed");
2698
2699 let path = format!(
2701 "orders/{}/invoices/{}/payments/{}",
2702 order_id, invoice_id, payment_id
2703 );
2704 let result = handle_nested_path_get(
2705 State(state),
2706 Path(path),
2707 Query(crate::core::query::QueryParams::default()),
2708 )
2709 .await
2710 .expect("handler should succeed");
2711
2712 let json = result.0;
2713 assert!(
2714 json.get("link").is_some(),
2715 "response should contain 'link' field for specific item"
2716 );
2717 }
2718
2719 #[tokio::test]
2720 async fn test_handle_nested_path_get_broken_chain() {
2721 let state = create_chain_test_state();
2722
2723 let order_id = Uuid::new_v4();
2724 let invoice_id = Uuid::new_v4();
2725
2726 let link1 = crate::core::link::LinkEntity::new("billing", order_id, invoice_id, None);
2728 state
2729 .link_service
2730 .create(link1)
2731 .await
2732 .expect("create should succeed");
2733
2734 let fake_payment_id = Uuid::new_v4();
2737 let path = format!(
2738 "orders/{}/invoices/{}/payments/{}",
2739 order_id, invoice_id, fake_payment_id
2740 );
2741 let result = handle_nested_path_get(
2742 State(state),
2743 Path(path),
2744 Query(crate::core::query::QueryParams::default()),
2745 )
2746 .await;
2747
2748 assert!(
2749 result.is_err(),
2750 "should fail when link chain is broken (no payment link)"
2751 );
2752 }
2753
2754 #[tokio::test]
2755 async fn test_handle_nested_path_get_invalid_chain_first_link() {
2756 let state = create_chain_test_state();
2757
2758 let order_id = Uuid::new_v4();
2759 let wrong_invoice_id = Uuid::new_v4();
2760 let payment_id = Uuid::new_v4();
2761
2762 let other_order_id = Uuid::new_v4();
2764 let link1 =
2765 crate::core::link::LinkEntity::new("billing", other_order_id, wrong_invoice_id, None);
2766 state
2767 .link_service
2768 .create(link1)
2769 .await
2770 .expect("create should succeed");
2771
2772 let link2 =
2773 crate::core::link::LinkEntity::new("payment", wrong_invoice_id, payment_id, None);
2774 state
2775 .link_service
2776 .create(link2)
2777 .await
2778 .expect("create should succeed");
2779
2780 let path = format!("orders/{}/invoices/{}/payments", order_id, wrong_invoice_id);
2783 let result = handle_nested_path_get(
2784 State(state),
2785 Path(path),
2786 Query(crate::core::query::QueryParams::default()),
2787 )
2788 .await;
2789
2790 assert!(
2791 result.is_err(),
2792 "should fail when first link in chain does not exist"
2793 );
2794 }
2795
2796 #[tokio::test]
2801 async fn test_handle_nested_path_post_too_few_segments() {
2802 let state = create_chain_test_state();
2803 let result = handle_nested_path_post(
2804 State(state),
2805 Path("orders/abc/invoices".to_string()),
2806 Json(CreateLinkedEntityRequest {
2807 entity: serde_json::json!({}),
2808 metadata: None,
2809 }),
2810 )
2811 .await;
2812
2813 assert!(result.is_err(), "should fail with fewer than 5 segments");
2814 }
2815
2816 #[tokio::test]
2817 async fn test_handle_nested_path_post_success() {
2818 let mut state = create_chain_test_state();
2819
2820 let mut creators: HashMap<String, Arc<dyn crate::core::EntityCreator>> = HashMap::new();
2822 creators.insert("payment".to_string(), Arc::new(MockEntityCreator));
2823 state.entity_creators = Arc::new(creators);
2824
2825 let order_id = Uuid::new_v4();
2826 let invoice_id = Uuid::new_v4();
2827
2828 let link1 = crate::core::link::LinkEntity::new("billing", order_id, invoice_id, None);
2830 state
2831 .link_service
2832 .create(link1)
2833 .await
2834 .expect("create should succeed");
2835
2836 let path = format!("orders/{}/invoices/{}/payments", order_id, invoice_id);
2838 let result = handle_nested_path_post(
2839 State(state.clone()),
2840 Path(path),
2841 Json(CreateLinkedEntityRequest {
2842 entity: serde_json::json!({ "amount": 100.0 }),
2843 metadata: None,
2844 }),
2845 )
2846 .await;
2847
2848 assert!(result.is_ok(), "handle_nested_path_post should succeed");
2849 let response = result.expect("should be ok");
2850 assert_eq!(response.status(), StatusCode::CREATED);
2851 }
2852
2853 #[tokio::test]
2854 async fn test_handle_nested_path_post_no_creator() {
2855 let state = create_chain_test_state();
2856
2857 let order_id = Uuid::new_v4();
2858 let invoice_id = Uuid::new_v4();
2859
2860 let link1 = crate::core::link::LinkEntity::new("billing", order_id, invoice_id, None);
2861 state
2862 .link_service
2863 .create(link1)
2864 .await
2865 .expect("create should succeed");
2866
2867 let path = format!("orders/{}/invoices/{}/payments", order_id, invoice_id);
2868 let result = handle_nested_path_post(
2869 State(state),
2870 Path(path),
2871 Json(CreateLinkedEntityRequest {
2872 entity: serde_json::json!({}),
2873 metadata: None,
2874 }),
2875 )
2876 .await;
2877
2878 assert!(
2879 result.is_err(),
2880 "should fail when no entity creator is registered for target type"
2881 );
2882 }
2883
2884 #[test]
2889 fn test_enriched_link_skips_none_source_and_target() {
2890 let link = make_enriched_link("owner", "active", None, None, None);
2891 let json = serde_json::to_value(&link).expect("serialization should succeed");
2892 assert!(
2893 json.get("source").is_none(),
2894 "None source should be skipped in serialization"
2895 );
2896 assert!(
2897 json.get("target").is_none(),
2898 "None target should be skipped in serialization"
2899 );
2900 assert!(
2901 json.get("metadata").is_none(),
2902 "None metadata should be skipped in serialization"
2903 );
2904 }
2905
2906 #[test]
2907 fn test_enriched_link_includes_present_fields() {
2908 let link = make_enriched_link(
2909 "owner",
2910 "active",
2911 Some(serde_json::json!({ "name": "Car" })),
2912 Some(serde_json::json!({ "name": "User" })),
2913 Some(serde_json::json!({ "priority": 1 })),
2914 );
2915 let json = serde_json::to_value(&link).expect("serialization should succeed");
2916 assert!(json.get("source").is_some());
2917 assert!(json.get("target").is_some());
2918 assert!(json.get("metadata").is_some());
2919 assert_eq!(json["type"], "link");
2920 }
2921
2922 #[tokio::test]
2927 async fn test_create_link_emits_event() {
2928 let bus = Arc::new(EventBus::new(16));
2929 let mut state = create_test_state();
2930 state.event_bus = Some(bus.clone());
2931
2932 let mut rx = bus.subscribe();
2933
2934 let user_id = Uuid::new_v4();
2935 let car_id = Uuid::new_v4();
2936
2937 let _result = create_link(
2938 State(state),
2939 Path((
2940 "users".to_string(),
2941 user_id,
2942 "cars-owned".to_string(),
2943 car_id,
2944 )),
2945 Json(CreateLinkRequest { metadata: None }),
2946 )
2947 .await
2948 .expect("create should succeed");
2949
2950 let envelope = rx.try_recv().expect("should receive link created event");
2951 match envelope.event {
2952 FrameworkEvent::Link(LinkEvent::Created {
2953 link_type,
2954 source_id,
2955 target_id,
2956 ..
2957 }) => {
2958 assert_eq!(link_type, "owner");
2959 assert_eq!(source_id, user_id);
2960 assert_eq!(target_id, car_id);
2961 }
2962 other => panic!("expected Link::Created event, got: {:?}", other),
2963 }
2964 }
2965
2966 #[tokio::test]
2967 async fn test_delete_link_emits_event() {
2968 let bus = Arc::new(EventBus::new(16));
2969 let mut state = create_test_state();
2970 state.event_bus = Some(bus.clone());
2971
2972 let user_id = Uuid::new_v4();
2973 let car_id = Uuid::new_v4();
2974 let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
2975 state
2976 .link_service
2977 .create(link)
2978 .await
2979 .expect("create should succeed");
2980
2981 let mut rx = bus.subscribe();
2982
2983 delete_link(
2984 State(state),
2985 Path((
2986 "users".to_string(),
2987 user_id,
2988 "cars-owned".to_string(),
2989 car_id,
2990 )),
2991 )
2992 .await
2993 .expect("delete should succeed");
2994
2995 let envelope = rx.try_recv().expect("should receive link deleted event");
2996 match envelope.event {
2997 FrameworkEvent::Link(LinkEvent::Deleted {
2998 link_type,
2999 source_id,
3000 target_id,
3001 ..
3002 }) => {
3003 assert_eq!(link_type, "owner");
3004 assert_eq!(source_id, user_id);
3005 assert_eq!(target_id, car_id);
3006 }
3007 other => panic!("expected Link::Deleted event, got: {:?}", other),
3008 }
3009 }
3010}