Skip to main content

chronon_core/
context.rs

1//! Script execution context ports.
2//!
3//! When a job is scheduled, Chronon stores `actor_json` on the job record. When a worker
4//! dispatches the script, it calls [`ContextFactory::build`] to reconstruct handler context from
5//! that JSON. Script handlers (including those defined with `#[chronon::script]`) receive the
6//! result as `Box<dyn ScriptContext>`.
7//!
8//! # Choosing a factory
9//!
10//! | Approach | When to use |
11//! |----------|-------------|
12//! | [`JsonScriptContextFactory`] | Examples, smoke tests, and handlers that only need [`ScriptContext::label`] and [`ScriptContext::actor_json`] |
13//! | Custom [`ContextFactory`] | Production apps that map actor JSON to sessions, permissions, database access, or other application identity |
14//!
15//! Install the factory at runtime boot on `ChrononBuilder` (see `chronon-runtime`).
16
17use serde_json::Value;
18
19use crate::error::{ChrononError, Result};
20
21/// Opaque execution context for script handlers.
22///
23/// The runtime passes this as the first argument to `#[chronon::script]` handlers and to registered
24/// invoke functions. Use [`label`](Self::label) for logs and [`actor_json`](Self::actor_json) when
25/// the handler only needs the captured actor payload.
26pub trait ScriptContext: Send {
27    /// Debug label for logs and tests.
28    fn label(&self) -> &str;
29
30    /// Actor JSON captured at schedule time and restored at dispatch.
31    fn actor_json(&self) -> &Value;
32}
33
34/// Builds a [`ScriptContext`] from JSON captured at schedule time.
35///
36/// Installed on `ChrononBuilder` at boot. The executor calls
37/// [`Self::build`] for every dispatched run using the job's `actor_json`.
38pub trait ContextFactory: Send + Sync {
39    /// Reconstruct handler context from actor JSON stored on the job.
40    ///
41    /// Returns [`IdentityError`] (mapped to [`ChrononError::Internal`]) when the payload
42    /// cannot be decoded into application identity.
43    fn build(&self, actor_json: &Value) -> Result<Box<dyn ScriptContext>>;
44}
45
46/// Identity reconstruction failure.
47#[derive(Debug, thiserror::Error)]
48#[error("identity error: {0}")]
49pub struct IdentityError(pub String);
50
51impl From<IdentityError> for ChrononError {
52    fn from(value: IdentityError) -> Self {
53        Self::Internal(value.0)
54    }
55}
56
57/// No-op context for tests and benchmarks.
58#[derive(Debug, Default)]
59pub struct NoOpScriptContext {
60    actor_json: Value,
61}
62
63impl ScriptContext for NoOpScriptContext {
64    fn label(&self) -> &'static str {
65        "noop"
66    }
67
68    fn actor_json(&self) -> &Value {
69        &self.actor_json
70    }
71}
72
73/// Factory that always returns [`NoOpScriptContext`].
74#[derive(Debug, Default, Clone, Copy)]
75pub struct NoOpContextFactory;
76
77impl ContextFactory for NoOpContextFactory {
78    fn build(&self, _actor_json: &Value) -> Result<Box<dyn ScriptContext>> {
79        Ok(Box::new(NoOpScriptContext::default()))
80    }
81}
82
83/// Default factory that wraps actor JSON in a labeled [`ScriptContext`].
84///
85/// Suitable for examples and handlers that only need [`ScriptContext::label`] and
86/// [`ScriptContext::actor_json`]. For application-specific identity (database sessions,
87/// permission checks, typed actors), implement [`ContextFactory`] instead.
88///
89/// # Examples
90///
91/// ```
92/// use chronon_core::{ContextFactory, JsonScriptContextFactory};
93/// use serde_json::json;
94///
95/// let factory = JsonScriptContextFactory;
96/// let ctx = factory.build(&json!({"user": "alice"})).unwrap();
97/// assert!(ctx.label().contains("alice"));
98/// assert_eq!(ctx.actor_json()["user"], "alice");
99/// ```
100#[derive(Debug, Default, Clone, Copy)]
101pub struct JsonScriptContextFactory;
102
103struct JsonContext {
104    actor_json: Value,
105    label: String,
106}
107
108impl ScriptContext for JsonContext {
109    fn label(&self) -> &str {
110        &self.label
111    }
112
113    fn actor_json(&self) -> &Value {
114        &self.actor_json
115    }
116}
117
118impl ContextFactory for JsonScriptContextFactory {
119    fn build(&self, actor_json: &Value) -> Result<Box<dyn ScriptContext>> {
120        Ok(Box::new(JsonContext {
121            actor_json: actor_json.clone(),
122            label: actor_json.to_string(),
123        }))
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use serde_json::json;
131
132    struct TestContext {
133        label: String,
134        actor_json: Value,
135    }
136
137    impl ScriptContext for TestContext {
138        fn label(&self) -> &str {
139            &self.label
140        }
141
142        fn actor_json(&self) -> &Value {
143            &self.actor_json
144        }
145    }
146
147    struct TestFactory;
148
149    impl ContextFactory for TestFactory {
150        fn build(&self, actor_json: &Value) -> Result<Box<dyn ScriptContext>> {
151            if actor_json.get("System").is_some() {
152                Ok(Box::new(TestContext {
153                    label: "system".into(),
154                    actor_json: actor_json.clone(),
155                }))
156            } else {
157                Err(IdentityError("missing System".into()).into())
158            }
159        }
160    }
161
162    #[test]
163    fn factory_builds_context() {
164        let factory = TestFactory;
165        let actor = json!({"System": {"operation": "t"}});
166        let ctx = factory.build(&actor).expect("ok");
167        assert_eq!(ctx.label(), "system");
168        assert_eq!(ctx.actor_json(), &actor);
169    }
170
171    #[test]
172    fn json_factory_stores_actor_json() {
173        let factory = JsonScriptContextFactory;
174        let actor = json!({"user": "alice"});
175        let ctx = factory.build(&actor).expect("ok");
176        assert_eq!(ctx.actor_json(), &actor);
177        assert!(ctx.label().contains("alice"));
178    }
179}