Skip to main content

harn_hostlib/
registry.rs

1//! Registration plumbing.
2//!
3//! Each module exposes a [`HostlibCapability`] implementation that pushes
4//! its builtins into a [`BuiltinRegistry`]. The registry can then either
5//! be wired into a real [`harn_vm::Vm`] (production path) or introspected
6//! by tests to assert the exposed surface without touching the VM.
7
8use std::sync::Arc;
9
10use harn_vm::{Vm, VmError, VmValue};
11
12use crate::error::HostlibError;
13
14/// Sync builtin handler signature. Mirrors the closure type accepted by
15/// [`harn_vm::Vm::register_builtin`]; we keep it `Send + Sync` so capability
16/// instances can be shared across threads if an embedder ever wants that.
17pub type SyncHandler = Arc<dyn Fn(&[VmValue]) -> Result<VmValue, HostlibError> + Send + Sync>;
18
19/// One registered builtin. The name is what Harn scripts call (e.g.
20/// `hostlib_ast_parse_file`); `module` and `method` are the canonical
21/// schema-directory coordinates (`schemas/<module>/<method>.request.json`).
22#[derive(Clone)]
23pub struct RegisteredBuiltin {
24    /// Builtin name as Harn scripts see it.
25    pub name: &'static str,
26    /// Module bucket (e.g. `"ast"`, `"tools"`).
27    pub module: &'static str,
28    /// Method name within the module (e.g. `"parse_file"`, `"search"`).
29    pub method: &'static str,
30    /// Handler invoked when Harn calls the builtin.
31    pub handler: SyncHandler,
32}
33
34impl std::fmt::Debug for RegisteredBuiltin {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        f.debug_struct("RegisteredBuiltin")
37            .field("name", &self.name)
38            .field("module", &self.module)
39            .field("method", &self.method)
40            .finish()
41    }
42}
43
44/// Mutable collector each capability writes into during `register`.
45#[derive(Default)]
46pub struct BuiltinRegistry {
47    builtins: Vec<RegisteredBuiltin>,
48}
49
50impl BuiltinRegistry {
51    /// Construct an empty registry.
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Push one builtin. Capabilities call this from `register_builtins`.
57    pub fn register(&mut self, builtin: RegisteredBuiltin) {
58        self.builtins.push(builtin);
59    }
60
61    /// Convenience: register a builtin whose body is the `unimplemented`
62    /// scaffold error.
63    pub fn register_unimplemented(
64        &mut self,
65        name: &'static str,
66        module: &'static str,
67        method: &'static str,
68    ) {
69        let handler: SyncHandler =
70            Arc::new(move |_args| Err(HostlibError::Unimplemented { builtin: name }));
71        self.register(RegisteredBuiltin {
72            name,
73            module,
74            method,
75            handler,
76        });
77    }
78
79    /// Iterate over every registered builtin.
80    pub fn iter(&self) -> impl Iterator<Item = &RegisteredBuiltin> {
81        self.builtins.iter()
82    }
83
84    /// Total count.
85    pub fn len(&self) -> usize {
86        self.builtins.len()
87    }
88
89    /// True when nothing has been registered yet.
90    pub fn is_empty(&self) -> bool {
91        self.builtins.is_empty()
92    }
93
94    /// Look up a builtin by its Harn-visible name.
95    pub fn find(&self, name: &str) -> Option<&RegisteredBuiltin> {
96        self.builtins.iter().find(|b| b.name == name)
97    }
98}
99
100/// One module's worth of builtins. Kept tiny on purpose: capabilities exist
101/// purely so tests can reason about the surface without booting a VM, and
102/// so embedders can opt into individual modules.
103pub trait HostlibCapability: 'static {
104    /// Module name (matches the `schemas/<module>/` directory).
105    fn module_name(&self) -> &'static str;
106
107    /// Push every builtin this module exposes into `registry`.
108    fn register_builtins(&self, registry: &mut BuiltinRegistry);
109}
110
111/// Composes capabilities and emits VM registrations.
112///
113/// `HostlibRegistry` is the type embedders interact with. It owns the
114/// capability instances and the populated [`BuiltinRegistry`] together so
115/// the same surface can be inspected by tests *and* wired into a VM.
116pub struct HostlibRegistry {
117    builtins: BuiltinRegistry,
118    modules: Vec<&'static str>,
119}
120
121impl Default for HostlibRegistry {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127impl HostlibRegistry {
128    /// Construct an empty registry. Most callers want [`crate::install_default`]
129    /// instead, which pre-populates every shipped capability.
130    pub fn new() -> Self {
131        Self {
132            builtins: BuiltinRegistry::new(),
133            modules: Vec::new(),
134        }
135    }
136
137    /// Add one capability to the registry. Returns `self` for chaining.
138    #[must_use]
139    pub fn with<C: HostlibCapability>(mut self, capability: C) -> Self {
140        let module = capability.module_name();
141        capability.register_builtins(&mut self.builtins);
142        self.modules.push(module);
143        self
144    }
145
146    /// Wire every registered builtin into the supplied VM.
147    pub fn register_into_vm(&mut self, vm: &mut Vm) {
148        for builtin in self.builtins.iter().cloned().collect::<Vec<_>>() {
149            let handler = builtin.handler.clone();
150            vm.register_builtin(
151                builtin.name,
152                move |args, _out| -> Result<VmValue, VmError> {
153                    handler(args).map_err(VmError::from)
154                },
155            );
156        }
157    }
158
159    /// Borrow the underlying [`BuiltinRegistry`] for introspection (e.g.
160    /// schema-drift tests).
161    pub fn builtins(&self) -> &BuiltinRegistry {
162        &self.builtins
163    }
164
165    /// List the module names that have been registered, in insertion order.
166    pub fn modules(&self) -> &[&'static str] {
167        &self.modules
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn unimplemented_builtins_route_through_error() {
177        let mut registry = BuiltinRegistry::new();
178        registry.register_unimplemented("hostlib_demo", "demo", "ping");
179        let entry = registry.find("hostlib_demo").expect("registered");
180        let err = (entry.handler)(&[]).expect_err("should be unimplemented");
181        assert!(
182            matches!(err, HostlibError::Unimplemented { builtin } if builtin == "hostlib_demo")
183        );
184    }
185
186    #[test]
187    fn registry_records_modules_in_order() {
188        struct First;
189        impl HostlibCapability for First {
190            fn module_name(&self) -> &'static str {
191                "first"
192            }
193            fn register_builtins(&self, _registry: &mut BuiltinRegistry) {}
194        }
195        struct Second;
196        impl HostlibCapability for Second {
197            fn module_name(&self) -> &'static str {
198                "second"
199            }
200            fn register_builtins(&self, _registry: &mut BuiltinRegistry) {}
201        }
202
203        let registry = HostlibRegistry::new().with(First).with(Second);
204        assert_eq!(registry.modules(), &["first", "second"]);
205    }
206}