Skip to main content

encounter_argumentation/
affordance_key.rs

1//! Canonical key for an (actor, affordance_name, bindings) triple
2//! used to index scheme instances against encounter affordances.
3//!
4//! Bindings are stored as a `BTreeMap` internally so the key's
5//! hash and equality are deterministic regardless of insertion
6//! order into the source `HashMap`.
7
8use std::collections::{BTreeMap, HashMap};
9
10/// Canonical identifier for a scheme instance seeded against a
11/// specific (actor, affordance, bindings) triple.
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub struct AffordanceKey {
14    actor: String,
15    affordance_name: String,
16    bindings: BTreeMap<String, String>,
17}
18
19impl AffordanceKey {
20    /// Construct a key from raw parts. Bindings are normalised into
21    /// a `BTreeMap` to give the key a deterministic hash.
22    #[must_use]
23    pub fn new(
24        actor: impl Into<String>,
25        affordance_name: impl Into<String>,
26        bindings: &HashMap<String, String>,
27    ) -> Self {
28        Self {
29            actor: actor.into(),
30            affordance_name: affordance_name.into(),
31            bindings: bindings.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
32        }
33    }
34
35    /// The actor component of the key.
36    #[must_use]
37    pub fn actor(&self) -> &str {
38        &self.actor
39    }
40
41    /// The affordance-name component of the key.
42    #[must_use]
43    pub fn affordance_name(&self) -> &str {
44        &self.affordance_name
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn new_key_round_trips_simple_bindings() {
54        let mut b = HashMap::new();
55        b.insert("expert".to_string(), "alice".to_string());
56        let k = AffordanceKey::new("alice", "assert_claim", &b);
57        assert_eq!(k.actor(), "alice");
58        assert_eq!(k.affordance_name(), "assert_claim");
59    }
60
61    #[test]
62    fn equal_bindings_in_different_insertion_orders_produce_equal_keys() {
63        let mut b1 = HashMap::new();
64        b1.insert("expert".to_string(), "alice".to_string());
65        b1.insert("claim".to_string(), "fortify_east".to_string());
66        let mut b2 = HashMap::new();
67        b2.insert("claim".to_string(), "fortify_east".to_string());
68        b2.insert("expert".to_string(), "alice".to_string());
69        let k1 = AffordanceKey::new("alice", "x", &b1);
70        let k2 = AffordanceKey::new("alice", "x", &b2);
71        assert_eq!(k1, k2);
72    }
73
74    #[test]
75    fn different_actors_produce_different_keys() {
76        let b = HashMap::new();
77        let k1 = AffordanceKey::new("alice", "x", &b);
78        let k2 = AffordanceKey::new("bob", "x", &b);
79        assert_ne!(k1, k2);
80    }
81
82    #[test]
83    fn equal_keys_produce_equal_hashes() {
84        use std::collections::hash_map::DefaultHasher;
85        use std::hash::{Hash, Hasher};
86        fn h<T: Hash>(t: &T) -> u64 {
87            let mut s = DefaultHasher::new();
88            t.hash(&mut s);
89            s.finish()
90        }
91        let mut b1 = HashMap::new();
92        b1.insert("a".to_string(), "1".to_string());
93        b1.insert("b".to_string(), "2".to_string());
94        let mut b2 = HashMap::new();
95        b2.insert("b".to_string(), "2".to_string());
96        b2.insert("a".to_string(), "1".to_string());
97        let k1 = AffordanceKey::new("x", "y", &b1);
98        let k2 = AffordanceKey::new("x", "y", &b2);
99        assert_eq!(h(&k1), h(&k2));
100    }
101}