Skip to main content

chronon_executor/
registry.rs

1//! In-memory [`ScriptRegistry`] with optional link-time inventory discovery.
2
3use std::collections::HashMap;
4
5use chronon_core::{ChrononError, Result};
6
7use crate::descriptor::{InvokeFn, ScriptDescriptor};
8
9struct StoredScript {
10    name: String,
11    invoke: InvokeFn,
12    signature_json: String,
13    signature_hash: u64,
14}
15
16/// In-memory script registry with optional link-time inventory discovery.
17///
18/// Populated at boot via [`Self::register_from_inventory`] or manual [`Self::register`]
19/// calls; read by [`crate::execute_script`] and [`crate::Executor`].
20pub struct ScriptRegistry {
21    scripts: HashMap<String, StoredScript>,
22}
23
24impl Default for ScriptRegistry {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30impl ScriptRegistry {
31    /// Creates an empty registry with no registered scripts.
32    ///
33    /// # Examples
34    ///
35    /// ```
36    /// use chronon_executor::ScriptRegistry;
37    ///
38    /// let registry = ScriptRegistry::new();
39    /// assert!(registry.is_empty());
40    /// ```
41    pub fn new() -> Self {
42        Self {
43            scripts: HashMap::new(),
44        }
45    }
46
47    /// Populate from all `#[chronon::script]` descriptors linked into the binary.
48    pub fn from_inventory() -> Self {
49        let mut registry = Self::new();
50        registry.register_from_inventory();
51        registry
52    }
53
54    /// Register every script descriptor collected via link-time inventory.
55    pub fn register_from_inventory(&mut self) {
56        for desc in quark::inventory::iter::<ScriptDescriptor> {
57            self.register(ScriptDescriptor::with_signature(
58                desc.name,
59                desc.invoke,
60                desc.signature_json,
61                desc.signature_hash,
62            ));
63        }
64    }
65
66    /// Inserts or replaces a script descriptor keyed by [`ScriptDescriptor::name`].
67    ///
68    /// Duplicate names overwrite the previous handler without error.
69    pub fn register(&mut self, desc: ScriptDescriptor) {
70        self.scripts.insert(
71            desc.name.to_string(),
72            StoredScript {
73                name: desc.name.to_string(),
74                invoke: desc.invoke,
75                signature_json: desc.signature_json.to_string(),
76                signature_hash: desc.signature_hash,
77            },
78        );
79    }
80
81    /// Returns a borrowed view of the script named `name`, if registered.
82    pub fn get(&self, name: &str) -> Option<ScriptDescriptorRef<'_>> {
83        self.scripts.get(name).map(|s| ScriptDescriptorRef {
84            name: s.name.as_str(),
85            invoke: s.invoke,
86            signature_json: s.signature_json.as_str(),
87            signature_hash: s.signature_hash,
88        })
89    }
90
91    /// Like [`Self::get`], but returns [`ChrononError::ScriptNotFound`] when absent.
92    pub fn get_or_err(&self, name: &str) -> Result<ScriptDescriptorRef<'_>> {
93        self.get(name)
94            .ok_or_else(|| ChrononError::ScriptNotFound(name.to_string()))
95    }
96
97    /// Returns the number of registered scripts.
98    pub fn len(&self) -> usize {
99        self.scripts.len()
100    }
101
102    /// Returns `true` when no scripts are registered.
103    pub fn is_empty(&self) -> bool {
104        self.scripts.is_empty()
105    }
106
107    /// Returns `true` when a script with the given name is registered.
108    pub fn contains(&self, name: &str) -> bool {
109        self.scripts.contains_key(name)
110    }
111
112    /// Returns all registered scripts sorted by name.
113    pub fn list(&self) -> Vec<ScriptDescriptorRef<'_>> {
114        let mut v: Vec<_> = self.scripts.values().map(stored_to_ref).collect();
115        v.sort_by_key(|d| d.name);
116        v
117    }
118}
119
120/// Borrowed view of a registered script suitable for invocation.
121#[derive(Clone, Copy)]
122pub struct ScriptDescriptorRef<'a> {
123    /// Registered script name (matches job `script_name`).
124    pub name: &'a str,
125    /// Handler function pointer collected from inventory or manual registration.
126    pub invoke: InvokeFn,
127    /// JSON schema string for handler parameters (from `chronon-macros` when available).
128    pub signature_json: &'a str,
129    /// Compile-time hash of the signature for drift detection.
130    pub signature_hash: u64,
131}
132
133fn stored_to_ref(s: &StoredScript) -> ScriptDescriptorRef<'_> {
134    ScriptDescriptorRef {
135        name: s.name.as_str(),
136        invoke: s.invoke,
137        signature_json: s.signature_json.as_str(),
138        signature_hash: s.signature_hash,
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use crate::descriptor::ScriptDescriptor;
146    use chronon_core::{Result, ScriptContext};
147    use serde_json::Value;
148    use std::future::Future;
149    use std::pin::Pin;
150
151    fn stub_invoke(
152        _ctx: Box<dyn ScriptContext>,
153        _params: Value,
154    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> {
155        Box::pin(async { Ok(()) })
156    }
157
158    #[test]
159    fn register_and_lookup() {
160        let mut reg = ScriptRegistry::new();
161        reg.register(ScriptDescriptor::new("hello", stub_invoke));
162        assert!(reg.contains("hello"));
163        assert_eq!(reg.len(), 1);
164    }
165}