1#[cfg(not(feature = "std"))]
23use alloc::string::{String, ToString};
24#[cfg(not(feature = "std"))]
25use alloc::vec::Vec;
26
27use crate::collections::HashMap;
28
29#[derive(Debug, Clone, PartialEq, Eq, Default)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
33pub struct EntityDescriptor {
34 pub name: String,
35 pub entity_type: String,
36 pub attributes: Vec<(String, String)>,
37 #[cfg_attr(feature = "serde", serde(default))]
48 pub relations: Vec<(String, String)>,
49}
50
51impl EntityDescriptor {
52 pub fn new(name: impl Into<String>, entity_type: impl Into<String>) -> Self {
53 Self {
54 name: name.into(),
55 entity_type: entity_type.into(),
56 attributes: Vec::new(),
57 relations: Vec::new(),
58 }
59 }
60
61 pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
63 self.attributes.push((key.into(), value.into()));
64 self
65 }
66
67 pub fn with_relation(mut self, label: impl Into<String>, target: impl Into<String>) -> Self {
75 self.relations.push((label.into(), target.into()));
76 self
77 }
78
79 pub fn attribute(&self, key: &str) -> Option<&str> {
81 self.attributes
82 .iter()
83 .find(|(k, _)| k == key)
84 .map(|(_, v)| v.as_str())
85 }
86
87 pub fn relation(&self, label: &str) -> Option<&str> {
92 self.relations
93 .iter()
94 .find(|(l, _)| l == label)
95 .map(|(_, t)| t.as_str())
96 }
97}
98
99#[derive(Debug, Clone, Default)]
107pub struct EntityRegistry {
108 entries: HashMap<(String, String), EntityDescriptor>,
109}
110
111impl EntityRegistry {
112 pub fn new() -> Self {
113 Self::default()
114 }
115
116 pub fn insert(&mut self, descriptor: EntityDescriptor) {
120 let key = (descriptor.entity_type.clone(), descriptor.name.clone());
121 self.entries.insert(key, descriptor);
122 }
123
124 pub fn get(&self, entity_type: &str, name: &str) -> Option<&EntityDescriptor> {
126 self.entries
127 .get(&(entity_type.to_string(), name.to_string()))
128 }
129
130 pub fn iter(&self) -> impl Iterator<Item = &EntityDescriptor> {
131 self.entries.values()
132 }
133
134 pub fn len(&self) -> usize {
135 self.entries.len()
136 }
137
138 pub fn is_empty(&self) -> bool {
139 self.entries.is_empty()
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Default)]
149pub struct SubgraphDescription {
150 pub attributes: Vec<String>,
153 pub relation: Option<(String, String)>,
159}
160
161pub(crate) fn incremental_attributes_with_remaining<'a>(
172 target: &EntityDescriptor,
173 registry: &'a EntityRegistry,
174 preference_order: &[String],
175) -> (Vec<String>, Vec<&'a EntityDescriptor>) {
176 let mut distractors: Vec<&EntityDescriptor> = registry
179 .iter()
180 .filter(|d| d.name != target.name && d.entity_type == target.entity_type)
181 .collect();
182
183 if distractors.is_empty() {
185 return (Vec::new(), Vec::new());
186 }
187
188 let mut walked: Vec<&String> = preference_order.iter().collect();
192 for (k, _) in &target.attributes {
193 if !walked.iter().any(|s| s.as_str() == k.as_str()) {
194 walked.push(k);
195 }
196 }
197
198 let mut chosen: Vec<String> = Vec::new();
199
200 for attr_key in walked {
201 if distractors.is_empty() {
202 break;
203 }
204 let target_value = match target.attribute(attr_key) {
205 Some(v) => v,
206 None => continue,
207 };
208
209 let still_matching: Vec<&EntityDescriptor> = distractors
211 .iter()
212 .copied()
213 .filter(|d| d.attribute(attr_key) == Some(target_value))
214 .collect();
215
216 if still_matching.len() < distractors.len() {
217 chosen.push(target_value.to_string());
218 distractors = still_matching;
219 }
220 }
221
222 (chosen, distractors)
223}
224
225pub fn distinguishing_attributes(
243 target: &EntityDescriptor,
244 registry: &EntityRegistry,
245 preference_order: &[String],
246) -> Vec<String> {
247 let (attrs, _) = incremental_attributes_with_remaining(target, registry, preference_order);
248 attrs
249}
250
251pub fn distinguishing_subgraph(
272 target: &EntityDescriptor,
273 registry: &EntityRegistry,
274 preference_order: &[String],
275) -> SubgraphDescription {
276 let (attrs, remaining) =
277 incremental_attributes_with_remaining(target, registry, preference_order);
278
279 if remaining.is_empty() {
281 return SubgraphDescription {
282 attributes: attrs,
283 relation: None,
284 };
285 }
286
287 for (label, target_name) in &target.relations {
290 let any_shared = remaining.iter().any(|d| {
291 d.relations
292 .iter()
293 .any(|(l, t)| l == label && t == target_name)
294 });
295 if !any_shared {
296 return SubgraphDescription {
297 attributes: attrs,
298 relation: Some((label.clone(), target_name.clone())),
299 };
300 }
301 }
302
303 SubgraphDescription {
305 attributes: attrs,
306 relation: None,
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 fn reg_with(entities: Vec<EntityDescriptor>) -> EntityRegistry {
315 let mut r = EntityRegistry::new();
316 for e in entities {
317 r.insert(e);
318 }
319 r
320 }
321
322 #[test]
323 fn no_distractors_yields_empty_attribute_list() {
324 let target =
325 EntityDescriptor::new("UserService", "class").with_attribute("layer", "domain");
326 let registry = reg_with(vec![target.clone()]);
327 let attrs = distinguishing_attributes(&target, ®istry, &[]);
328 assert!(attrs.is_empty());
329 }
330
331 #[test]
332 fn different_type_distractor_does_not_force_attribute() {
333 let target =
334 EntityDescriptor::new("UserService", "class").with_attribute("layer", "domain");
335 let other = EntityDescriptor::new("UserService", "trait").with_attribute("layer", "infra");
336 let registry = reg_with(vec![target.clone(), other]);
337 let attrs = distinguishing_attributes(&target, ®istry, &[]);
339 assert!(attrs.is_empty());
340 }
341
342 #[test]
343 fn same_type_requires_distinguishing_attribute() {
344 let target =
345 EntityDescriptor::new("UserService", "class").with_attribute("layer", "domain");
346 let distractor =
347 EntityDescriptor::new("AuthService", "class").with_attribute("layer", "infra");
348 let registry = reg_with(vec![target.clone(), distractor]);
349 let attrs = distinguishing_attributes(&target, ®istry, &[]);
350 assert_eq!(attrs, vec!["domain".to_string()]);
351 }
352
353 #[test]
354 fn preference_order_is_respected() {
355 let target = EntityDescriptor::new("Foo", "class")
356 .with_attribute("color", "red")
357 .with_attribute("size", "small");
358 let d1 = EntityDescriptor::new("Bar", "class")
359 .with_attribute("color", "blue")
360 .with_attribute("size", "small");
361
362 let registry = reg_with(vec![target.clone(), d1]);
363
364 let attrs = distinguishing_attributes(
366 &target,
367 ®istry,
368 &["size".to_string(), "color".to_string()],
369 );
370 assert_eq!(attrs, vec!["red".to_string()]);
371 }
372
373 #[test]
374 fn preferred_attribute_selected_when_sufficient() {
375 let target = EntityDescriptor::new("Foo", "widget")
376 .with_attribute("color", "red")
377 .with_attribute("size", "small");
378 let d1 = EntityDescriptor::new("Bar", "widget")
379 .with_attribute("color", "blue")
380 .with_attribute("size", "large");
381
382 let registry = reg_with(vec![target.clone(), d1]);
383 let attrs = distinguishing_attributes(&target, ®istry, &["color".to_string()]);
384 assert_eq!(attrs, vec!["red".to_string()]);
385 }
386
387 #[test]
388 fn multiple_attributes_needed_for_full_disambiguation() {
389 let target = EntityDescriptor::new("Foo", "widget")
390 .with_attribute("color", "red")
391 .with_attribute("size", "small");
392 let same_color = EntityDescriptor::new("Bar", "widget")
393 .with_attribute("color", "red")
394 .with_attribute("size", "large");
395 let same_size = EntityDescriptor::new("Baz", "widget")
396 .with_attribute("color", "blue")
397 .with_attribute("size", "small");
398
399 let registry = reg_with(vec![target.clone(), same_color, same_size]);
400 let attrs = distinguishing_attributes(
401 &target,
402 ®istry,
403 &["color".to_string(), "size".to_string()],
404 );
405 assert_eq!(attrs, vec!["red".to_string(), "small".to_string()]);
407 }
408
409 #[test]
410 fn useless_attribute_is_skipped() {
411 let target = EntityDescriptor::new("Foo", "widget")
413 .with_attribute("color", "red")
414 .with_attribute("size", "small");
415 let d1 = EntityDescriptor::new("Bar", "widget")
416 .with_attribute("color", "red")
417 .with_attribute("size", "large");
418 let d2 = EntityDescriptor::new("Baz", "widget")
419 .with_attribute("color", "red")
420 .with_attribute("size", "medium");
421
422 let registry = reg_with(vec![target.clone(), d1, d2]);
423 let attrs = distinguishing_attributes(
424 &target,
425 ®istry,
426 &["color".to_string(), "size".to_string()],
427 );
428 assert_eq!(attrs, vec!["small".to_string()]);
430 }
431
432 #[test]
433 fn stops_as_soon_as_unambiguous() {
434 let target = EntityDescriptor::new("Foo", "widget")
435 .with_attribute("color", "red")
436 .with_attribute("size", "small")
437 .with_attribute("shape", "round");
438 let d1 = EntityDescriptor::new("Bar", "widget").with_attribute("color", "blue");
439
440 let registry = reg_with(vec![target.clone(), d1]);
441 let attrs = distinguishing_attributes(
442 &target,
443 ®istry,
444 &["color".to_string(), "size".to_string(), "shape".to_string()],
445 );
446 assert_eq!(attrs, vec!["red".to_string()]);
448 }
449
450 #[test]
451 fn falls_back_to_registration_order_when_no_preference() {
452 let target = EntityDescriptor::new("Foo", "widget")
453 .with_attribute("first_attr", "A")
454 .with_attribute("second_attr", "B");
455 let d1 = EntityDescriptor::new("Bar", "widget")
456 .with_attribute("first_attr", "X")
457 .with_attribute("second_attr", "B");
458
459 let registry = reg_with(vec![target.clone(), d1]);
460 let attrs = distinguishing_attributes(&target, ®istry, &[]);
463 assert_eq!(attrs, vec!["A".to_string()]);
464 }
465
466 #[test]
467 fn missing_attribute_on_target_skips_without_panic() {
468 let target = EntityDescriptor::new("Foo", "widget").with_attribute("size", "small");
469 let d1 = EntityDescriptor::new("Bar", "widget").with_attribute("size", "large");
470
471 let registry = reg_with(vec![target.clone(), d1]);
472 let attrs = distinguishing_attributes(
474 &target,
475 ®istry,
476 &["color".to_string(), "size".to_string()],
477 );
478 assert_eq!(attrs, vec!["small".to_string()]);
479 }
480
481 #[test]
482 fn registry_insert_replaces_same_type_and_name() {
483 let mut r = EntityRegistry::new();
484 r.insert(EntityDescriptor::new("X", "t").with_attribute("a", "1"));
485 r.insert(EntityDescriptor::new("X", "t").with_attribute("a", "2"));
486 assert_eq!(r.get("t", "X").unwrap().attribute("a"), Some("2"));
487 assert_eq!(r.len(), 1);
488 }
489
490 #[test]
491 fn registry_keeps_same_name_different_type_as_separate_entries() {
492 let mut r = EntityRegistry::new();
493 r.insert(EntityDescriptor::new("UserService", "class").with_attribute("a", "1"));
494 r.insert(EntityDescriptor::new("UserService", "trait").with_attribute("a", "2"));
495 assert_eq!(r.len(), 2);
496 assert_eq!(
497 r.get("class", "UserService").unwrap().attribute("a"),
498 Some("1")
499 );
500 assert_eq!(
501 r.get("trait", "UserService").unwrap().attribute("a"),
502 Some("2")
503 );
504 }
505
506 #[test]
509 fn with_relation_adds_edge() {
510 let e = EntityDescriptor::new("Handler", "function").with_relation("calls", "AuthService");
511 assert_eq!(
512 e.relations,
513 vec![("calls".to_string(), "AuthService".to_string())]
514 );
515 }
516
517 #[test]
518 fn relation_lookup_by_label() {
519 let e = EntityDescriptor::new("Handler", "function")
520 .with_relation("calls", "AuthService")
521 .with_relation("tests", "HandlerTests");
522 assert_eq!(e.relation("calls"), Some("AuthService"));
523 assert_eq!(e.relation("tests"), Some("HandlerTests"));
524 assert_eq!(e.relation("unknown"), None);
525 }
526
527 #[test]
528 fn default_has_empty_relations() {
529 let e = EntityDescriptor::default();
530 assert!(e.relations.is_empty());
531 }
532
533 #[test]
536 fn graph_reg_no_distractors_returns_empty() {
537 let target = EntityDescriptor::new("Foo", "class");
538 let registry = reg_with(vec![target.clone()]);
539 let desc = distinguishing_subgraph(&target, ®istry, &[]);
540 assert!(desc.attributes.is_empty());
541 assert!(desc.relation.is_none());
542 }
543
544 #[test]
545 fn graph_reg_falls_back_to_dale_reiter_when_attributes_suffice() {
546 let target =
547 EntityDescriptor::new("UserService", "class").with_attribute("layer", "domain");
548 let other = EntityDescriptor::new("AuthService", "class").with_attribute("layer", "infra");
549 let registry = reg_with(vec![target.clone(), other]);
550 let desc = distinguishing_subgraph(&target, ®istry, &[]);
551 assert_eq!(desc.attributes, vec!["domain".to_string()]);
552 assert!(desc.relation.is_none());
553 }
554
555 #[test]
556 fn graph_reg_adds_relation_when_attributes_dont_disambiguate() {
557 let target = EntityDescriptor::new("LoginHandler", "function")
560 .with_attribute("layer", "api")
561 .with_relation("calls", "AuthService");
562 let other = EntityDescriptor::new("LogoutHandler", "function")
563 .with_attribute("layer", "api")
564 .with_relation("calls", "SessionService");
565 let registry = reg_with(vec![target.clone(), other]);
566 let desc = distinguishing_subgraph(&target, ®istry, &[]);
567 assert_eq!(
570 desc.relation,
571 Some(("calls".to_string(), "AuthService".to_string()))
572 );
573 }
574
575 #[test]
576 fn graph_reg_skips_shared_relation_picks_next() {
577 let target = EntityDescriptor::new("LoginHandler", "function")
580 .with_relation("calls", "LogService")
581 .with_relation("tests", "LoginTests");
582 let other = EntityDescriptor::new("LogoutHandler", "function")
583 .with_relation("calls", "LogService")
584 .with_relation("tests", "LogoutTests");
585 let registry = reg_with(vec![target.clone(), other]);
586 let desc = distinguishing_subgraph(&target, ®istry, &[]);
587 assert_eq!(
588 desc.relation,
589 Some(("tests".to_string(), "LoginTests".to_string()))
590 );
591 }
592
593 #[test]
594 fn graph_reg_gives_up_when_nothing_distinguishes() {
595 let target = EntityDescriptor::new("Foo", "thing").with_relation("calls", "X");
598 let other = EntityDescriptor::new("Bar", "thing").with_relation("calls", "X");
599 let registry = reg_with(vec![target.clone(), other]);
600 let desc = distinguishing_subgraph(&target, ®istry, &[]);
601 assert!(desc.relation.is_none());
604 }
605
606 #[test]
607 fn graph_reg_combines_attributes_and_relation() {
608 let target = EntityDescriptor::new("LoginHandler", "function")
616 .with_attribute("layer", "api")
617 .with_relation("calls", "AuthService");
618 let d1 = EntityDescriptor::new("LogoutHandler", "function")
619 .with_attribute("layer", "api")
620 .with_relation("calls", "SessionService");
621 let d2 = EntityDescriptor::new("ProfileHandler", "function")
622 .with_attribute("layer", "web")
623 .with_relation("calls", "AuthService");
624 let registry = reg_with(vec![target.clone(), d1, d2]);
625 let desc = distinguishing_subgraph(&target, ®istry, &[]);
626 assert_eq!(desc.attributes, vec!["api".to_string()]);
627 assert_eq!(
628 desc.relation,
629 Some(("calls".to_string(), "AuthService".to_string()))
630 );
631 }
632}