Skip to main content

orleans_rust_client/
request_context.rs

1//! Request-context propagation.
2//!
3//! Orleans exposes an ambient `RequestContext` whose entries flow with a grain
4//! call. The bridge copies the entries supplied here into Orleans'
5//! `RequestContext` before invoking the grain, and clears them afterwards so
6//! values never leak between calls.
7
8use std::collections::BTreeMap;
9
10/// An ordered set of string key/value pairs propagated with a grain call.
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
12pub struct RequestContext {
13    entries: BTreeMap<String, String>,
14}
15
16impl RequestContext {
17    /// Create an empty context.
18    #[must_use]
19    pub fn new() -> Self {
20        Self::default()
21    }
22
23    /// Builder-style insert.
24    #[must_use]
25    pub fn with(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
26        self.entries.insert(key.into(), value.into());
27        self
28    }
29
30    /// Insert or replace an entry.
31    pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
32        self.entries.insert(key.into(), value.into());
33        self
34    }
35
36    /// Look up an entry.
37    #[must_use]
38    pub fn get(&self, key: &str) -> Option<&str> {
39        self.entries.get(key).map(String::as_str)
40    }
41
42    /// Whether the context carries no entries.
43    #[must_use]
44    pub fn is_empty(&self) -> bool {
45        self.entries.is_empty()
46    }
47
48    /// Number of entries.
49    #[must_use]
50    pub fn len(&self) -> usize {
51        self.entries.len()
52    }
53
54    /// Iterate over the entries in key order.
55    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
56        self.entries.iter().map(|(k, v)| (k.as_str(), v.as_str()))
57    }
58
59    /// Return a new context with `other`'s entries overlaid on top of this
60    /// one's. Used to apply per-call overrides over client defaults.
61    #[must_use]
62    pub fn merged_with(&self, other: &RequestContext) -> RequestContext {
63        let mut merged = self.clone();
64        for (k, v) in &other.entries {
65            merged.entries.insert(k.clone(), v.clone());
66        }
67        merged
68    }
69
70    pub(crate) fn into_map(self) -> std::collections::HashMap<String, String> {
71        self.entries.into_iter().collect()
72    }
73}
74
75impl<K, V> FromIterator<(K, V)> for RequestContext
76where
77    K: Into<String>,
78    V: Into<String>,
79{
80    fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
81        Self {
82            entries: iter
83                .into_iter()
84                .map(|(k, v)| (k.into(), v.into()))
85                .collect(),
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn builder_and_accessors() {
96        let ctx = RequestContext::new().with("a", "1").with("b", "2");
97        assert_eq!(ctx.len(), 2);
98        assert!(!ctx.is_empty());
99        assert_eq!(ctx.get("a"), Some("1"));
100        assert_eq!(ctx.get("missing"), None);
101    }
102
103    #[test]
104    fn iter_is_key_ordered() {
105        let ctx = RequestContext::new().with("z", "1").with("a", "2");
106        let keys: Vec<&str> = ctx.iter().map(|(k, _)| k).collect();
107        assert_eq!(keys, vec!["a", "z"]);
108    }
109
110    #[test]
111    fn merged_with_overlays_other() {
112        let base = RequestContext::new().with("a", "1").with("b", "1");
113        let over = RequestContext::new().with("b", "2").with("c", "3");
114        let merged = base.merged_with(&over);
115        assert_eq!(merged.get("a"), Some("1"));
116        assert_eq!(merged.get("b"), Some("2"));
117        assert_eq!(merged.get("c"), Some("3"));
118    }
119
120    #[test]
121    fn into_map_preserves_entries() {
122        let map = RequestContext::from_iter([("k", "v")]).into_map();
123        assert_eq!(map.get("k"), Some(&"v".to_owned()));
124    }
125
126    #[test]
127    fn set_mutates_in_place_and_overwrites() {
128        let mut ctx = RequestContext::new();
129        assert!(ctx.is_empty());
130        ctx.set("a", "1").set("b", "2").set("a", "overwritten");
131        assert_eq!(ctx.len(), 2);
132        assert_eq!(ctx.get("a"), Some("overwritten"));
133        assert_eq!(ctx.get("b"), Some("2"));
134    }
135}