bop/host.rs
1//! Ready-made [`BopHost`] building blocks for embedders.
2//!
3//! Most embedders only need two things beyond the default trait
4//! impl: a way to plug in a module resolver, and a way to capture
5//! `print` output. This module provides both as composable
6//! pieces so you can mix-and-match without re-implementing the
7//! whole trait from scratch.
8//!
9//! ```no_run
10//! use bop::host::{StringModuleHost, resolve_from_map};
11//! use bop::BopLimits;
12//!
13//! // Map of module-name → source, resolved in-process (no I/O).
14//! let mut host = StringModuleHost::new([
15//! ("greetings", "fn hello() { print(\"hi\") }"),
16//! ]);
17//! bop::run("use greetings\nhello()", &mut host, &BopLimits::standard())
18//! .unwrap();
19//! ```
20//!
21//! The helpers intentionally stay minimal — they don't own the
22//! terminal, they don't touch the filesystem, they don't parse
23//! environment variables. Embedders that want richer behaviour
24//! should implement [`BopHost`] directly or wrap these helpers.
25
26#[cfg(feature = "no_std")]
27use alloc::{borrow::ToOwned, string::String, vec::Vec};
28
29#[cfg(not(feature = "no_std"))]
30use std::collections::BTreeMap;
31#[cfg(feature = "no_std")]
32use alloc::collections::BTreeMap;
33
34use crate::error::BopError;
35use crate::value::Value;
36use crate::BopHost;
37
38/// Build a [`BopHost::resolve_module`] implementation from an
39/// in-memory table of `(module_path, source)` pairs.
40///
41/// The returned closure takes the same `&str` argument the
42/// `BopHost` trait passes in and returns the matching source
43/// wrapped in `Some(Ok(..))`, or `None` when the module name
44/// isn't in the table. Embedders with additional lookup logic
45/// (filesystem, HTTP, asset bundle) can layer this helper on top
46/// of their own fallback chain — see [`StringModuleHost`] for a
47/// minimal all-in-one example.
48pub fn resolve_from_map<I, K, V>(entries: I) -> impl Fn(&str) -> Option<Result<String, BopError>>
49where
50 I: IntoIterator<Item = (K, V)>,
51 K: Into<String>,
52 V: Into<String>,
53{
54 let map: BTreeMap<String, String> = entries
55 .into_iter()
56 .map(|(k, v)| (k.into(), v.into()))
57 .collect();
58 move |name: &str| map.get(name).map(|s| Ok(s.clone()))
59}
60
61/// A minimal [`BopHost`] that captures `print` output and serves
62/// modules from an in-memory string table.
63///
64/// Useful for tests, playgrounds, and embedders that want
65/// resolver-backed imports without writing a full `BopHost`
66/// from scratch. For embedders that only need module resolution
67/// (and have their own print handling), use [`resolve_from_map`]
68/// directly inside a custom trait impl instead.
69pub struct StringModuleHost {
70 /// Each `print(...)` invocation appends one entry. Leave
71 /// public so tests can assert on output without going
72 /// through an accessor.
73 pub prints: Vec<String>,
74 modules: BTreeMap<String, String>,
75}
76
77impl StringModuleHost {
78 /// Build a host preloaded with the given module map.
79 ///
80 /// The iterator yields `(name, source)` pairs. Later entries
81 /// with the same name overwrite earlier ones.
82 pub fn new<I, K, V>(modules: I) -> Self
83 where
84 I: IntoIterator<Item = (K, V)>,
85 K: Into<String>,
86 V: Into<String>,
87 {
88 Self {
89 prints: Vec::new(),
90 modules: modules
91 .into_iter()
92 .map(|(k, v)| (k.into(), v.into()))
93 .collect(),
94 }
95 }
96
97 /// Register (or replace) a single module after construction.
98 pub fn insert_module(&mut self, name: impl Into<String>, source: impl Into<String>) {
99 self.modules.insert(name.into(), source.into());
100 }
101
102 /// Join all accumulated prints with `\n` — convenient for
103 /// tests that want to assert on the combined output.
104 pub fn output(&self) -> String {
105 self.prints.join("\n")
106 }
107}
108
109impl BopHost for StringModuleHost {
110 fn call(&mut self, _name: &str, _args: &[Value], _line: u32) -> Option<Result<Value, BopError>> {
111 None
112 }
113
114 fn on_print(&mut self, message: &str) {
115 self.prints.push(message.to_owned());
116 }
117
118 fn resolve_module(&mut self, name: &str) -> Option<Result<String, BopError>> {
119 self.modules.get(name).map(|s| Ok(s.clone()))
120 }
121}