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}