Skip to main content

rust_dix/
key.rs

1//! Service keys for keyed registrations.
2//!
3//! A key is either a **typed value** (`Eq + Hash + Debug`, any type) or the
4//! special [`ServiceKey::any_key`] sentinel used for "fallback for any key"
5//! registrations (MEDI `KeyedService.AnyKey` parity).
6//!
7//! String keys used by `keyed_singleton("name", …)` are just typed keys whose
8//! value is a `String`, so existing string-keyed code keeps working while
9//! typed keys enable MEDI-style keyed services (enums, integers, …).
10
11use std::any::Any;
12use std::fmt;
13use std::hash::{DefaultHasher, Hash, Hasher};
14use std::sync::Arc;
15
16/// Erased key value. Implemented for every `T: Any + Send + Sync + Eq + Hash + Debug`.
17#[doc(hidden)]
18pub trait ErasedKeyValue: Any + Send + Sync + fmt::Debug {
19    fn as_any(&self) -> &dyn Any;
20    fn key_eq(&self, other: &dyn Any) -> bool;
21    fn key_hash(&self) -> u64;
22}
23
24impl<T> ErasedKeyValue for T
25where
26    T: Any + Send + Sync + Eq + Hash + fmt::Debug,
27{
28    fn as_any(&self) -> &dyn Any {
29        self
30    }
31
32    fn key_eq(&self, other: &dyn Any) -> bool {
33        other.downcast_ref::<T>() == Some(self)
34    }
35
36    fn key_hash(&self) -> u64 {
37        let mut hasher = DefaultHasher::new();
38        self.hash(&mut hasher);
39        hasher.finish()
40    }
41}
42
43#[derive(Clone)]
44enum KeyRepr {
45    Value(Arc<dyn ErasedKeyValue>),
46    Any,
47}
48
49/// Key of a keyed service registration / resolution.
50///
51/// Construct a value key with [`ServiceKey::new`] (or `From<&str>`/`From<String>`
52/// for string keys), and the fallback sentinel with [`ServiceKey::any_key`].
53///
54/// # String keys
55///
56/// String-keyed registration APIs (`keyed_singleton("name", …)`) store the key
57/// as a `String`. Use `ServiceKey::from("name")` / `"name".into()` to build an
58/// equal key. `ServiceKey::new("name")` is a **different** key type (`&str`),
59/// because key equality includes the value type — use it only for intentional
60/// `&'static str`-typed keys.
61///
62/// # Reading a requested key
63///
64/// `AnyKey` fallback factories receive `&ServiceKey`. The erased key exposes
65/// `Debug`/`Display` and equality/hash; there is intentionally **no** typed
66/// downcast accessor (operations must be expressible through the `ServiceKey`
67/// API itself, keeping the erased-key representation private).
68#[derive(Clone)]
69pub struct ServiceKey(KeyRepr);
70
71impl ServiceKey {
72    /// Create a typed key value.
73    ///
74    /// ```
75    /// use rust_dix::ServiceKey;
76    ///
77    /// #[derive(Debug, PartialEq, Eq, Hash)]
78    /// enum Db { Read, Write }
79    ///
80    /// let key = ServiceKey::new(Db::Read);
81    /// assert_ne!(key, ServiceKey::new(Db::Write));
82    /// ```
83    pub fn new<T>(value: T) -> Self
84    where
85        T: Any + Send + Sync + Eq + Hash + fmt::Debug + 'static,
86    {
87        Self(KeyRepr::Value(Arc::new(value)))
88    }
89
90    /// The `AnyKey` fallback sentinel (MEDI `KeyedService.AnyKey`).
91    ///
92    /// Registering a service under this key makes it a fallback for **every**
93    /// key that has no exact registration. Resolving a single service *with*
94    /// this sentinel is not allowed (use [`ServiceKey`]-keyed lookups or
95    /// enumeration instead).
96    pub fn any_key() -> Self {
97        Self(KeyRepr::Any)
98    }
99
100    /// Whether this key is the [`ServiceKey::any_key`] sentinel.
101    pub fn is_any(&self) -> bool {
102        matches!(self.0, KeyRepr::Any)
103    }
104
105    /// Borrow the key as `&str` for **string-like** keys: `String` and
106    /// `&'static str` (including `ServiceKey::new("literal")`). Returns `None`
107    /// for other typed keys and the `AnyKey` sentinel.
108    ///
109    /// This is the compatibility bridge used by the default
110    /// `IServiceResolver` typed-key methods, which fall back to the legacy
111    /// string methods. Note that the fallback maps a typed `&'static str` key to
112    /// a **`String` key**; in fully typed containers those two keys are *not*
113    /// equal, so typed-key consumers should use the `*_key` methods.
114    pub fn as_str(&self) -> Option<&str> {
115        match &self.0 {
116            KeyRepr::Value(v) => {
117                let any = v.as_any();
118                if let Some(s) = any.downcast_ref::<String>() {
119                    Some(s.as_str())
120                } else if let Some(s) = any.downcast_ref::<&'static str>() {
121                    Some(*s)
122                } else {
123                    None
124                }
125            }
126            KeyRepr::Any => None,
127        }
128    }
129
130    /// Stable hash used for per-requested-key cycle markers.
131    pub(crate) fn stable_hash(&self) -> u64 {
132        let mut hasher = DefaultHasher::new();
133        self.hash(&mut hasher);
134        hasher.finish()
135    }
136}
137
138impl From<&str> for ServiceKey {
139    fn from(value: &str) -> Self {
140        Self::new(value.to_owned())
141    }
142}
143
144impl From<String> for ServiceKey {
145    fn from(value: String) -> Self {
146        Self::new(value)
147    }
148}
149
150impl From<&String> for ServiceKey {
151    fn from(value: &String) -> Self {
152        Self::new(value.clone())
153    }
154}
155
156impl From<&ServiceKey> for ServiceKey {
157    fn from(value: &ServiceKey) -> Self {
158        value.clone()
159    }
160}
161
162impl fmt::Debug for ServiceKey {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        match &self.0 {
165            KeyRepr::Value(v) => write!(f, "{v:?}"),
166            KeyRepr::Any => f.write_str("AnyKey"),
167        }
168    }
169}
170
171impl fmt::Display for ServiceKey {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        match &self.0 {
174            KeyRepr::Value(v) => {
175                // String keys display without quotes (nicer error messages);
176                // other typed keys fall back to their `Debug` representation.
177                if let Some(s) = v.as_any().downcast_ref::<String>() {
178                    f.write_str(s)
179                } else {
180                    write!(f, "{v:?}")
181                }
182            }
183            KeyRepr::Any => f.write_str("AnyKey"),
184        }
185    }
186}
187
188impl PartialEq for ServiceKey {
189    fn eq(&self, other: &Self) -> bool {
190        match (&self.0, &other.0) {
191            (KeyRepr::Any, KeyRepr::Any) => true,
192            (KeyRepr::Value(a), KeyRepr::Value(b)) => {
193                a.as_any().type_id() == b.as_any().type_id() && a.key_eq(b.as_any())
194            }
195            _ => false,
196        }
197    }
198}
199
200impl Eq for ServiceKey {}
201
202impl Hash for ServiceKey {
203    fn hash<H: Hasher>(&self, state: &mut H) {
204        match &self.0 {
205            KeyRepr::Any => state.write_u8(0),
206            KeyRepr::Value(v) => {
207                state.write_u8(1);
208                v.as_any().type_id().hash(state);
209                v.key_hash().hash(state);
210            }
211        }
212    }
213}
214
215/// MEDI-inspired namespace for the `AnyKey` sentinel.
216///
217/// `KeyedService::any_key()` equals `ServiceKey::any_key()`.
218#[derive(Debug, Clone, Copy, Default)]
219pub struct KeyedService;
220
221impl KeyedService {
222    /// The `AnyKey` fallback sentinel (MEDI `KeyedService.AnyKey`).
223    pub fn any_key() -> ServiceKey {
224        ServiceKey::any_key()
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use std::collections::HashSet;
232
233    #[derive(Debug, PartialEq, Eq, Hash)]
234    enum Db {
235        Read,
236        Write,
237    }
238
239    #[test]
240    fn typed_keys_compare_and_hash() {
241        let mut set = HashSet::new();
242        set.insert(ServiceKey::new(Db::Read));
243        set.insert(ServiceKey::new(Db::Write));
244        set.insert(ServiceKey::new(Db::Read));
245        assert_eq!(set.len(), 2);
246        assert_eq!(ServiceKey::new(Db::Read), ServiceKey::new(Db::Read));
247        assert_ne!(ServiceKey::new(Db::Read), ServiceKey::new(Db::Write));
248    }
249
250    #[test]
251    fn string_keys_are_typed_keys() {
252        assert_eq!(ServiceKey::from("a"), ServiceKey::from("a".to_string()));
253        assert_ne!(ServiceKey::from("a"), ServiceKey::from("b"));
254        // Different key types with the same debug/display must not collide.
255        assert_ne!(ServiceKey::new(1_u32), ServiceKey::new(1_u64));
256    }
257
258    #[test]
259    fn any_key_identity() {
260        assert!(ServiceKey::any_key().is_any());
261        assert_eq!(ServiceKey::any_key(), KeyedService::any_key());
262        assert_ne!(ServiceKey::any_key(), ServiceKey::from("AnyKey"));
263        assert_ne!(ServiceKey::any_key(), ServiceKey::new("AnyKey"));
264    }
265
266    #[test]
267    fn as_str_only_for_string_like_keys() {
268        assert_eq!(ServiceKey::from("x").as_str(), Some("x"));
269        assert_eq!(ServiceKey::new("y").as_str(), Some("y"));
270        assert_eq!(ServiceKey::new(1_u32).as_str(), None);
271        assert_eq!(ServiceKey::any_key().as_str(), None);
272    }
273
274    #[test]
275    fn debug_and_display_are_stable_enough_for_errors() {
276        assert_eq!(format!("{:?}", ServiceKey::any_key()), "AnyKey");
277        assert_eq!(format!("{}", ServiceKey::from("x")), "x");
278        assert_eq!(format!("{:?}", ServiceKey::from("x")), "\"x\"");
279    }
280
281    #[test]
282    fn string_literal_typed_key_is_a_distinct_type() {
283        // `ServiceKey::new("x")` is an `&'static str` key, not the `String` key
284        // produced by string-keyed registration APIs.
285        assert_ne!(ServiceKey::new("x"), ServiceKey::from("x"));
286    }
287}