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}