Skip to main content

boson_core/
identity.rs

1//! Identity reconstruction at the task handler boundary.
2//!
3//! When a job is enqueued, Boson stores `actor_json` on the job record. When a worker dispatches
4//! the job, it calls [`ExecutionContextFactory::build`] to reconstruct handler context from that
5//! JSON. Task handlers (including those defined with `#[boson::task]`) receive the result as
6//! `Box<dyn ExecutionContext>`.
7//!
8//! # In task handlers
9//!
10//! Use [`ExecutionContext::label`] for logs and [`ExecutionContext::actor_json`] when the handler
11//! only needs the captured actor payload. You do not configure the factory inside the handler.
12//!
13//! # Choosing a factory (integrator)
14//!
15//! Install the factory once at worker boot via
16//! [`BosonBuilder::execution_context_factory`](https://docs.rs/boson-runtime/latest/boson_runtime/struct.BosonBuilder.html#method.execution_context_factory).
17//! See the [`boson`](https://docs.rs/uf-boson) crate
18//! [Getting started](https://docs.rs/uf-boson/latest/boson/index.html#getting-started) for a full boot example.
19//!
20//! | Approach | When to use |
21//! |----------|-------------|
22//! | [`JsonExecutionContextFactory`] | Examples, smoke tests, and handlers that only need [`ExecutionContext::label`] and [`ExecutionContext::actor_json`] |
23//! | Custom [`ExecutionContextFactory`] | Production apps that map actor JSON to sessions, permissions, database access, or other application identity |
24//!
25//! # Custom factory sketch
26//!
27//! ```rust,no_run
28//! use boson_core::{ExecutionContext, ExecutionContextFactory, IdentityError};
29//! use serde_json::Value;
30//!
31//! struct AppContext {
32//!     actor_json: Value,
33//! }
34//!
35//! impl ExecutionContext for AppContext {
36//!     fn label(&self) -> &str {
37//!         "app"
38//!     }
39//!     fn actor_json(&self) -> &Value {
40//!         &self.actor_json
41//!     }
42//! }
43//!
44//! struct AppFactory;
45//!
46//! impl ExecutionContextFactory for AppFactory {
47//!     fn build(&self, actor_json: &Value) -> Result<Box<dyn ExecutionContext>, IdentityError> {
48//!         // Validate actor_json and construct application-specific context.
49//!         Ok(Box::new(AppContext {
50//!             actor_json: actor_json.clone(),
51//!         }))
52//!     }
53//! }
54//! ```
55
56use serde_json::Value;
57
58use crate::error::IdentityError;
59
60/// Opaque execution context for task handlers.
61///
62/// The runtime passes this as the first argument to `#[boson::task]` handlers and to registered
63/// invoke functions. Use [`label`](Self::label) for logs and [`actor_json`](Self::actor_json) when
64/// the handler only needs the captured actor payload.
65pub trait ExecutionContext: Send {
66    /// Debug label for logs and tests.
67    fn label(&self) -> &str;
68
69    /// Actor JSON captured at enqueue time and restored at dispatch.
70    fn actor_json(&self) -> &Value;
71}
72
73/// Builds handler execution context from captured actor JSON at enqueue time.
74///
75/// Implement this trait (or use [`JsonExecutionContextFactory`]) and pass the factory to
76/// [`BosonBuilder::execution_context_factory`](https://docs.rs/boson-runtime/latest/boson_runtime/struct.BosonBuilder.html#method.execution_context_factory) when booting the runtime.
77///
78/// # Example
79///
80/// Most apps start with [`JsonExecutionContextFactory`]. Custom factories validate `actor_json` and
81/// attach sessions or permissions — see [Custom factory sketch](crate::identity#custom-factory-sketch).
82///
83/// ```ignore
84/// use std::sync::Arc;
85///
86/// use boson_backend_mem::MemQueueBackend;
87/// use boson_core::JsonExecutionContextFactory;
88/// use boson_runtime::Boson;
89///
90/// # fn main() -> boson_core::Result<()> {
91/// let _boson = Boson::builder()
92///     .queue_backend(Arc::new(MemQueueBackend::new()))
93///     .execution_context_factory(JsonExecutionContextFactory)
94///     .build()?;
95/// # Ok(())
96/// # }
97/// ```
98pub trait ExecutionContextFactory: Send + Sync {
99    /// Build context for task dispatch from stored `actor_json`.
100    ///
101    /// # Errors
102    ///
103    /// Returns [`IdentityError`] when actor JSON cannot be mapped to application context.
104    fn build(&self, actor_json: &Value) -> Result<Box<dyn ExecutionContext>, IdentityError>;
105}
106
107/// Default factory that wraps actor JSON in a labeled [`ExecutionContext`].
108///
109/// Suitable for examples and handlers that only need [`ExecutionContext::label`] and
110/// [`ExecutionContext::actor_json`]. For application-specific identity (database sessions,
111/// permission checks, typed actors), implement [`ExecutionContextFactory`] instead.
112///
113/// The default [`label`](ExecutionContext::label) keeps short actor JSON as-is (up to 64
114/// characters) and replaces longer payloads with a stable `json:<hash>` form so logs stay
115/// compact. The full actor payload remains available via [`actor_json`](ExecutionContext::actor_json).
116///
117/// # Example
118///
119/// Pass to [`BosonBuilder::execution_context_factory`](https://docs.rs/boson-runtime/latest/boson_runtime/struct.BosonBuilder.html#method.execution_context_factory) at worker boot:
120///
121/// ```ignore
122/// use std::sync::Arc;
123///
124/// use boson_backend_mem::MemQueueBackend;
125/// use boson_core::JsonExecutionContextFactory;
126/// use boson_runtime::Boson;
127///
128/// # fn main() -> boson_core::Result<()> {
129/// let _boson = Boson::builder()
130///     .queue_backend(Arc::new(MemQueueBackend::new()))
131///     .execution_context_factory(JsonExecutionContextFactory)
132///     .auto_registry()
133///     .build()?;
134/// # Ok(())
135/// # }
136/// ```
137#[derive(Debug, Default, Clone, Copy)]
138pub struct JsonExecutionContextFactory;
139
140struct JsonContext {
141    actor_json: Value,
142    label: String,
143}
144
145impl ExecutionContext for JsonContext {
146    fn label(&self) -> &str {
147        &self.label
148    }
149
150    fn actor_json(&self) -> &Value {
151        &self.actor_json
152    }
153}
154
155impl ExecutionContextFactory for JsonExecutionContextFactory {
156    fn build(&self, actor_json: &Value) -> Result<Box<dyn ExecutionContext>, IdentityError> {
157        Ok(Box::new(JsonContext {
158            actor_json: actor_json.clone(),
159            label: compact_actor_label(actor_json),
160        }))
161    }
162}
163
164/// Compact log label for actor JSON: keep short payloads; hash long ones.
165fn compact_actor_label(actor_json: &Value) -> String {
166    use std::collections::hash_map::DefaultHasher;
167    use std::hash::{Hash, Hasher};
168
169    const MAX_LEN: usize = 64;
170    let full = actor_json.to_string();
171    if full.len() <= MAX_LEN {
172        return full;
173    }
174    let mut hasher = DefaultHasher::new();
175    full.hash(&mut hasher);
176    format!("json:{:016x}", hasher.finish())
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    struct TestContext {
184        label: String,
185        actor_json: Value,
186    }
187
188    impl ExecutionContext for TestContext {
189        fn label(&self) -> &str {
190            &self.label
191        }
192
193        fn actor_json(&self) -> &Value {
194            &self.actor_json
195        }
196    }
197
198    struct TestFactory;
199
200    impl ExecutionContextFactory for TestFactory {
201        fn build(&self, actor_json: &Value) -> Result<Box<dyn ExecutionContext>, IdentityError> {
202            if actor_json.get("System").is_some() {
203                Ok(Box::new(TestContext {
204                    label: "system".into(),
205                    actor_json: actor_json.clone(),
206                }))
207            } else {
208                Err(IdentityError::InvalidActor("missing System".into()))
209            }
210        }
211    }
212
213    #[test]
214    fn factory_builds_context() {
215        let factory = TestFactory;
216        let actor = serde_json::json!({"System": {"operation": "t"}});
217        let ctx = factory.build(&actor).expect("ok");
218        assert_eq!(ctx.label(), "system");
219        assert_eq!(ctx.actor_json(), &actor);
220    }
221
222    #[test]
223    fn json_factory_stores_actor_json() {
224        let factory = JsonExecutionContextFactory;
225        let actor = serde_json::json!({"user": "alice"});
226        let ctx = factory.build(&actor).expect("ok");
227        assert_eq!(ctx.actor_json(), &actor);
228        assert!(ctx.label().contains("alice"));
229    }
230
231    #[test]
232    fn json_factory_compacts_long_labels() {
233        let factory = JsonExecutionContextFactory;
234        let actor = serde_json::json!({
235            "user": "alice",
236            "note": "x".repeat(80),
237        });
238        let ctx = factory.build(&actor).expect("ok");
239        assert_eq!(ctx.actor_json(), &actor);
240        assert!(ctx.label().starts_with("json:"));
241        assert!(ctx.label().len() < 40);
242    }
243}