1use crate::domains::Domain;
9use crate::persona::{PersonaGenerator, PersonaProfile, PersonaRegistry};
10use crate::persona_graph::{PersonaGraph, PersonaNode};
11use mockforge_core::Result;
12use serde_json::Value;
13use std::collections::HashMap;
14use std::sync::{Arc, RwLock};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum EntityType {
22 User,
24 Device,
26 Organization,
28 Generic,
30}
31
32impl EntityType {
33 pub fn as_str(&self) -> &'static str {
35 match self {
36 EntityType::User => "user",
37 EntityType::Device => "device",
38 EntityType::Organization => "organization",
39 EntityType::Generic => "generic",
40 }
41 }
42
43 pub fn from_str(s: &str) -> Self {
45 match s.to_lowercase().as_str() {
46 "user" | "users" => EntityType::User,
47 "device" | "devices" => EntityType::Device,
48 "organization" | "organizations" | "org" | "orgs" => EntityType::Organization,
49 _ => EntityType::Generic,
50 }
51 }
52}
53
54#[derive(Debug)]
61pub struct ConsistencyStore {
62 persona_registry: Arc<PersonaRegistry>,
64 generators: Arc<RwLock<HashMap<Domain, PersonaGenerator>>>,
66 default_domain: Option<Domain>,
68 persona_graph: Arc<PersonaGraph>,
70}
71
72impl ConsistencyStore {
73 pub fn new() -> Self {
75 Self {
76 persona_registry: Arc::new(PersonaRegistry::new()),
77 generators: Arc::new(RwLock::new(HashMap::new())),
78 default_domain: None,
79 persona_graph: Arc::new(PersonaGraph::new()),
80 }
81 }
82
83 pub fn with_default_domain(default_domain: Domain) -> Self {
85 Self {
86 persona_registry: Arc::new(PersonaRegistry::new()),
87 generators: Arc::new(RwLock::new(HashMap::new())),
88 default_domain: Some(default_domain),
89 persona_graph: Arc::new(PersonaGraph::new()),
90 }
91 }
92
93 pub fn with_registry_and_domain(
95 persona_registry: Arc<PersonaRegistry>,
96 default_domain: Option<Domain>,
97 ) -> Self {
98 Self {
99 persona_registry,
100 generators: Arc::new(RwLock::new(HashMap::new())),
101 default_domain,
102 persona_graph: Arc::new(PersonaGraph::new()),
103 }
104 }
105
106 pub fn with_persona_graph(persona_graph: Arc<PersonaGraph>) -> Self {
108 Self {
109 persona_registry: Arc::new(PersonaRegistry::new()),
110 generators: Arc::new(RwLock::new(HashMap::new())),
111 default_domain: None,
112 persona_graph,
113 }
114 }
115
116 pub fn get_entity_persona(&self, entity_id: &str, domain: Option<Domain>) -> PersonaProfile {
121 let domain = domain.or(self.default_domain).unwrap_or(Domain::General);
122 self.persona_registry.get_or_create_persona(entity_id.to_string(), domain)
123 }
124
125 pub fn get_or_create_persona_by_type(
134 &self,
135 entity_id: &str,
136 entity_type: EntityType,
137 domain: Option<Domain>,
138 ) -> PersonaProfile {
139 let domain = domain.or(self.default_domain).unwrap_or(Domain::General);
140 let persona_id = format!("{}:{}", entity_type.as_str(), entity_id);
141 let persona = self.persona_registry.get_or_create_persona(persona_id.clone(), domain);
142
143 let entity_type_str = entity_type.as_str();
145 self.persona_graph
146 .get_or_create_node_with_links(&persona_id, entity_type_str, None, None);
147
148 if entity_type != EntityType::Generic {
150 let base_persona = self.get_entity_persona(entity_id, Some(domain));
152 let base_persona_id = base_persona.id.clone();
153
154 self.persona_graph
156 .get_or_create_node_with_links(&base_persona_id, "base", None, None);
157
158 let mut base_persona_mut = base_persona.clone();
160 match entity_type {
161 EntityType::User => {
162 self.persona_graph.link_entity_types(
166 &base_persona_id,
167 "base",
168 &persona_id,
169 entity_type_str,
170 );
171 }
172 EntityType::Device => {
173 base_persona_mut
175 .add_relationship("owns_devices".to_string(), persona_id.clone());
176 self.persona_graph.link_entity_types(
178 &base_persona_id,
179 "base",
180 &persona_id,
181 entity_type_str,
182 );
183 }
184 EntityType::Organization => {
185 base_persona_mut.add_relationship("has_users".to_string(), persona_id.clone());
187 self.persona_graph.link_entity_types(
189 &base_persona_id,
190 "base",
191 &persona_id,
192 entity_type_str,
193 );
194 }
195 EntityType::Generic => {}
196 }
197
198 for (rel_type, related_ids) in &base_persona_mut.relationships {
201 for related_id in related_ids {
202 if let Some(existing) = self.persona_registry.get_persona(entity_id) {
204 if !existing.get_related_personas(rel_type).contains(related_id) {
205 self.persona_registry
206 .add_relationship(entity_id, rel_type.clone(), related_id.clone())
207 .ok();
208 }
209 }
210 }
211 }
212 }
213
214 persona
215 }
216
217 pub fn link_personas(
222 &self,
223 from_entity_id: &str,
224 from_entity_type: &str,
225 to_entity_id: &str,
226 to_entity_type: &str,
227 ) {
228 let from_persona_id = format!("{}:{}", from_entity_type, from_entity_id);
229 let to_persona_id = format!("{}:{}", to_entity_type, to_entity_id);
230
231 self.persona_graph.get_or_create_node_with_links(
233 &from_persona_id,
234 from_entity_type,
235 None,
236 None,
237 );
238 self.persona_graph.get_or_create_node_with_links(
239 &to_persona_id,
240 to_entity_type,
241 None,
242 None,
243 );
244
245 self.persona_graph.link_entity_types(
247 &from_persona_id,
248 from_entity_type,
249 &to_persona_id,
250 to_entity_type,
251 );
252 }
253
254 pub fn persona_graph(&self) -> &Arc<PersonaGraph> {
256 &self.persona_graph
257 }
258
259 pub fn get_personas_for_base_id(
263 &self,
264 base_id: &str,
265 domain: Option<Domain>,
266 ) -> Vec<PersonaProfile> {
267 let domain = domain.or(self.default_domain).unwrap_or(Domain::General);
268 let mut personas = Vec::new();
269
270 let base_persona = self.get_entity_persona(base_id, Some(domain));
272 personas.push(base_persona);
273
274 for entity_type in [
276 EntityType::User,
277 EntityType::Device,
278 EntityType::Organization,
279 ] {
280 let persona_id = format!("{}:{}", entity_type.as_str(), base_id);
281 if let Some(persona) = self.persona_registry.get_persona(&persona_id) {
282 personas.push(persona);
283 }
284 }
285
286 personas
287 }
288
289 pub fn generate_consistent_value(
294 &self,
295 entity_id: &str,
296 field_type: &str,
297 domain: Option<Domain>,
298 ) -> Result<Value> {
299 self.generate_consistent_value_with_reality(entity_id, field_type, domain, 0.0, None, None)
301 }
302
303 pub fn generate_consistent_value_with_reality(
316 &self,
317 entity_id: &str,
318 field_type: &str,
319 domain: Option<Domain>,
320 reality_ratio: f64,
321 recorded_data: Option<&Value>,
322 real_data: Option<&Value>,
323 ) -> Result<Value> {
324 let persona = self.get_entity_persona(entity_id, domain);
326 let domain = persona.domain;
327
328 let generator = {
332 let generators = self.generators.read().unwrap();
333 if generators.contains_key(&domain) {
334 PersonaGenerator::new(domain)
337 } else {
338 drop(generators);
339 let mut generators = self.generators.write().unwrap();
340 generators.insert(domain, PersonaGenerator::new(domain));
341 PersonaGenerator::new(domain)
342 }
343 };
344
345 generator.generate_for_persona_with_reality(
347 &persona,
348 field_type,
349 reality_ratio,
350 recorded_data,
351 real_data,
352 )
353 }
354
355 pub fn persona_registry(&self) -> &Arc<PersonaRegistry> {
357 &self.persona_registry
358 }
359
360 pub fn set_default_domain(&mut self, domain: Option<Domain>) {
362 self.default_domain = domain;
363 }
364
365 pub fn default_domain(&self) -> Option<Domain> {
367 self.default_domain
368 }
369
370 pub fn clear(&self) {
372 self.persona_registry.clear();
373 let mut generators = self.generators.write().unwrap();
374 generators.clear();
375 }
376
377 pub fn persona_count(&self) -> usize {
379 self.persona_registry.count()
380 }
381}
382
383impl Default for ConsistencyStore {
384 fn default() -> Self {
385 Self::new()
386 }
387}
388
389pub struct EntityIdExtractor;
394
395impl EntityIdExtractor {
396 pub fn from_field_name(field_name: &str) -> Option<String> {
401 let field_lower = field_name.to_lowercase();
402
403 let patterns = [
405 "user_id",
406 "userid",
407 "user-id",
408 "device_id",
409 "deviceid",
410 "device-id",
411 "transaction_id",
412 "transactionid",
413 "transaction-id",
414 "order_id",
415 "orderid",
416 "order-id",
417 "customer_id",
418 "customerid",
419 "customer-id",
420 "patient_id",
421 "patientid",
422 "patient-id",
423 "account_id",
424 "accountid",
425 "account-id",
426 "id", ];
428
429 for pattern in &patterns {
431 if field_lower == *pattern {
432 return Some(field_name.to_string());
433 }
434 }
435
436 for pattern in &patterns {
438 if field_lower.ends_with(&format!("_{}", pattern))
439 || field_lower.ends_with(&format!("-{}", pattern))
440 {
441 return Some(field_name.to_string());
442 }
443 }
444
445 None
446 }
447
448 pub fn from_path(path: &str) -> Option<(String, EntityType)> {
453 let segments: Vec<&str> = path.split('/').collect();
458 if segments.len() >= 3 {
459 let resource = segments[segments.len() - 2].to_lowercase();
460 let id = segments[segments.len() - 1];
461
462 let entity_type = EntityType::from_str(&resource);
464
465 if entity_type != EntityType::Generic && !id.is_empty() {
466 return Some((id.to_string(), entity_type));
467 }
468
469 let entity_types = [
471 "transaction",
472 "transactions",
473 "order",
474 "orders",
475 "customer",
476 "customers",
477 "patient",
478 "patients",
479 "account",
480 "accounts",
481 ];
482
483 if entity_types.contains(&resource.as_str()) && !id.is_empty() {
484 return Some((id.to_string(), EntityType::Generic));
485 }
486 }
487
488 None
489 }
490
491 pub fn from_path_id_only(path: &str) -> Option<String> {
495 Self::from_path(path).map(|(id, _)| id)
496 }
497
498 pub fn from_json_value(value: &Value) -> Option<String> {
502 if let Some(obj) = value.as_object() {
503 let id_fields = [
505 "user_id",
506 "userId",
507 "user-id",
508 "device_id",
509 "deviceId",
510 "device-id",
511 "transaction_id",
512 "transactionId",
513 "transaction-id",
514 "order_id",
515 "orderId",
516 "order-id",
517 "customer_id",
518 "customerId",
519 "customer-id",
520 "patient_id",
521 "patientId",
522 "patient-id",
523 "account_id",
524 "accountId",
525 "account-id",
526 "id",
527 ];
528
529 for field in &id_fields {
530 if let Some(id_value) = obj.get(*field) {
531 if let Some(id_str) = id_value.as_str() {
532 return Some(id_str.to_string());
533 } else if let Some(id_num) = id_value.as_u64() {
534 return Some(id_num.to_string());
535 }
536 }
537 }
538 }
539
540 None
541 }
542
543 pub fn from_multiple_sources(
547 field_name: Option<&str>,
548 path: Option<&str>,
549 json_value: Option<&Value>,
550 ) -> Option<String> {
551 if let Some(field) = field_name {
553 if let Some(id) = Self::from_field_name(field) {
554 return Some(id);
555 }
556 }
557
558 if let Some(p) = path {
560 if let Some((id, _)) = Self::from_path(p) {
561 return Some(id);
562 }
563 }
564
565 if let Some(json) = json_value {
567 if let Some(id) = Self::from_json_value(json) {
568 return Some(id);
569 }
570 }
571
572 None
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579 use serde_json::json;
580
581 #[test]
582 fn test_consistency_store_new() {
583 let store = ConsistencyStore::new();
584 assert_eq!(store.persona_count(), 0);
585 }
586
587 #[test]
588 fn test_consistency_store_with_default_domain() {
589 let store = ConsistencyStore::with_default_domain(Domain::Finance);
590 assert_eq!(store.default_domain(), Some(Domain::Finance));
591 }
592
593 #[test]
594 fn test_get_entity_persona() {
595 let store = ConsistencyStore::with_default_domain(Domain::Finance);
596 let persona1 = store.get_entity_persona("user123", None);
597 let persona2 = store.get_entity_persona("user123", None);
598
599 assert_eq!(persona1.id, persona2.id);
601 assert_eq!(persona1.seed, persona2.seed);
602 }
603
604 #[test]
605 fn test_generate_consistent_value() {
606 let store = ConsistencyStore::with_default_domain(Domain::Finance);
607
608 let value1 = store.generate_consistent_value("user123", "amount", None).unwrap();
610 let value2 = store.generate_consistent_value("user123", "amount", None).unwrap();
611
612 assert!(value1.is_string() || value1.is_number());
614 assert!(value2.is_string() || value2.is_number());
615 }
616
617 #[test]
618 fn test_entity_id_extractor_from_field_name() {
619 assert_eq!(EntityIdExtractor::from_field_name("user_id"), Some("user_id".to_string()));
620 assert_eq!(EntityIdExtractor::from_field_name("deviceId"), Some("deviceId".to_string()));
621 assert_eq!(
622 EntityIdExtractor::from_field_name("transaction_id"),
623 Some("transaction_id".to_string())
624 );
625 assert_eq!(EntityIdExtractor::from_field_name("name"), None);
626 }
627
628 #[test]
629 fn test_entity_id_extractor_from_path() {
630 assert_eq!(
631 EntityIdExtractor::from_path("/users/123"),
632 Some(("123".to_string(), EntityType::User))
633 );
634 assert_eq!(
635 EntityIdExtractor::from_path("/devices/abc-123"),
636 Some(("abc-123".to_string(), EntityType::Device))
637 );
638 assert_eq!(
639 EntityIdExtractor::from_path("/organizations/org1"),
640 Some(("org1".to_string(), EntityType::Organization))
641 );
642 assert_eq!(EntityIdExtractor::from_path("/api/health"), None);
643 }
644
645 #[test]
646 fn test_entity_id_extractor_from_path_id_only() {
647 assert_eq!(EntityIdExtractor::from_path_id_only("/users/123"), Some("123".to_string()));
648 assert_eq!(
649 EntityIdExtractor::from_path_id_only("/devices/abc-123"),
650 Some("abc-123".to_string())
651 );
652 assert_eq!(
653 EntityIdExtractor::from_path_id_only("/organizations/org1"),
654 Some("org1".to_string())
655 );
656 assert_eq!(EntityIdExtractor::from_path_id_only("/api/health"), None);
657 }
658
659 #[test]
660 fn test_entity_type() {
661 assert_eq!(EntityType::User.as_str(), "user");
662 assert_eq!(EntityType::Device.as_str(), "device");
663 assert_eq!(EntityType::Organization.as_str(), "organization");
664 assert_eq!(EntityType::from_str("users"), EntityType::User);
665 assert_eq!(EntityType::from_str("devices"), EntityType::Device);
666 assert_eq!(EntityType::from_str("organizations"), EntityType::Organization);
667 }
668
669 #[test]
670 fn test_get_or_create_persona_by_type() {
671 let store = ConsistencyStore::with_default_domain(Domain::Finance);
672
673 let user_persona = store.get_or_create_persona_by_type("user123", EntityType::User, None);
675 let device_persona =
676 store.get_or_create_persona_by_type("user123", EntityType::Device, None);
677
678 assert_ne!(user_persona.id, device_persona.id);
680 assert!(user_persona.id.contains("user:user123"));
681 assert!(device_persona.id.contains("device:user123"));
682 }
683
684 #[test]
685 fn test_get_personas_for_base_id() {
686 let store = ConsistencyStore::with_default_domain(Domain::Finance);
687
688 store.get_or_create_persona_by_type("user123", EntityType::User, None);
690 store.get_or_create_persona_by_type("user123", EntityType::Device, None);
691
692 let personas = store.get_personas_for_base_id("user123", None);
693 assert!(personas.len() >= 2); }
695
696 #[test]
697 fn test_entity_id_extractor_from_json_value() {
698 let json = json!({
699 "user_id": "user123",
700 "name": "John Doe"
701 });
702 assert_eq!(EntityIdExtractor::from_json_value(&json), Some("user123".to_string()));
703
704 let json2 = json!({
705 "id": 456,
706 "name": "Device"
707 });
708 assert_eq!(EntityIdExtractor::from_json_value(&json2), Some("456".to_string()));
709 }
710
711 #[test]
712 fn test_entity_id_extractor_from_multiple_sources() {
713 let id1 = EntityIdExtractor::from_multiple_sources(Some("user_id"), None, None);
715 assert_eq!(id1, Some("user_id".to_string()));
716
717 let id2 = EntityIdExtractor::from_multiple_sources(Some("name"), Some("/users/123"), None);
719 assert_eq!(id2, Some("123".to_string()));
720
721 let json = json!({"user_id": "user456"});
723 let id3 = EntityIdExtractor::from_multiple_sources(
724 Some("name"),
725 Some("/api/health"),
726 Some(&json),
727 );
728 assert_eq!(id3, Some("user456".to_string()));
729 }
730}