Skip to main content

bubbles/library/
mod.rs

1//! [`FunctionLibrary`] — built-in functions and host-registration API.
2
3use std::collections::HashMap;
4
5use crate::error::{DialogueError, Result};
6use crate::value::Value;
7
8/// A boxed host function callable from dialogue expressions.
9pub type HostFn = Box<dyn Fn(Vec<Value>) -> Result<Value> + Send + Sync + 'static>;
10
11/// Registry of named functions available to expression evaluation.
12///
13/// Built-in functions (`random`, `dice`, `random_range`, …) are pre-registered.
14/// Hosts can add their own via [`FunctionLibrary::register`].
15pub struct FunctionLibrary {
16    fns: HashMap<String, HostFn>,
17}
18
19impl FunctionLibrary {
20    /// Creates a library with the built-in functions pre-registered.
21    #[must_use]
22    pub fn new() -> Self {
23        let mut lib = Self {
24            fns: HashMap::new(),
25        };
26        lib.register_builtins();
27        lib
28    }
29
30    /// Registers a named function.
31    ///
32    /// Replaces any existing function with the same name.
33    pub fn register<F>(&mut self, name: impl Into<String>, f: F)
34    where
35        F: Fn(Vec<Value>) -> Result<Value> + Send + Sync + 'static,
36    {
37        self.fns.insert(name.into(), Box::new(f));
38    }
39
40    /// Calls the function named `name` with `args`, or returns an error if not found.
41    ///
42    /// # Errors
43    /// Returns [`DialogueError::Function`] if the function is unknown or the call fails.
44    pub fn call(&self, name: &str, args: Vec<Value>) -> Result<Value> {
45        self.fns.get(name).map_or_else(
46            || {
47                Err(DialogueError::Function {
48                    name: name.to_owned(),
49                    message: "unknown function".into(),
50                })
51            },
52            |f| f(args),
53        )
54    }
55
56    fn register_builtins(&mut self) {
57        #[cfg(feature = "rand")]
58        self.register_rand_builtins();
59        self.register_math_builtins();
60    }
61
62    fn register_math_builtins(&mut self) {
63        self.register("round", |args| {
64            let n = require_one_number("round", &args)?;
65            Ok(Value::Number(n.round()))
66        });
67        self.register("floor", |args| {
68            let n = require_one_number("floor", &args)?;
69            Ok(Value::Number(n.floor()))
70        });
71        self.register("ceil", |args| {
72            let n = require_one_number("ceil", &args)?;
73            Ok(Value::Number(n.ceil()))
74        });
75        self.register("min", |args| {
76            let (a, b) = require_two_numbers("min", &args)?;
77            Ok(Value::Number(a.min(b)))
78        });
79        self.register("max", |args| {
80            let (a, b) = require_two_numbers("max", &args)?;
81            Ok(Value::Number(a.max(b)))
82        });
83        self.register("abs", |args| {
84            let n = require_one_number("abs", &args)?;
85            Ok(Value::Number(n.abs()))
86        });
87        self.register("clamp", |args| match args.as_slice() {
88            [Value::Number(v), Value::Number(lo), Value::Number(hi)] => {
89                Ok(Value::Number(v.clamp(*lo, *hi)))
90            }
91            _ => Err(DialogueError::Function {
92                name: "clamp".into(),
93                message: format!("expected 3 number arguments, got {args:?}"),
94            }),
95        });
96        self.register("string", |args| {
97            args.into_iter().next().map_or_else(
98                || {
99                    Err(DialogueError::Function {
100                        name: "string".into(),
101                        message: "expected 1 argument".into(),
102                    })
103                },
104                |v| Ok(Value::Text(v.to_string())),
105            )
106        });
107        self.register("int", |args| {
108            let n = require_one_number("int", &args)?;
109            Ok(Value::Number(n.trunc()))
110        });
111    }
112
113    #[cfg(feature = "rand")]
114    #[allow(
115        clippy::cast_possible_truncation,
116        clippy::cast_sign_loss,
117        clippy::cast_precision_loss
118    )]
119    fn register_rand_builtins(&mut self) {
120        use rand::Rng as _;
121        self.register("random", |_args| {
122            Ok(Value::Number(rand::rng().random::<f64>()))
123        });
124        self.register("random_range", |args| {
125            let (lo, hi) = require_two_numbers("random_range", &args)?;
126            let lo = lo as i64;
127            let hi = hi as i64;
128            if lo > hi {
129                return Err(DialogueError::Function {
130                    name: "random_range".into(),
131                    message: format!("lo ({lo}) > hi ({hi})"),
132                });
133            }
134            Ok(Value::Number(f64::from(
135                rand::rng().random_range(lo as i32..=hi as i32),
136            )))
137        });
138        self.register("dice", |args| {
139            let (sides, count) = require_two_numbers("dice", &args)?;
140            let sides = sides as u64;
141            let count = count as u64;
142            if sides == 0 {
143                return Err(DialogueError::Function {
144                    name: "dice".into(),
145                    message: "sides must be > 0".into(),
146                });
147            }
148            let total: u64 = (0..count)
149                .map(|_| rand::rng().random_range(1..=sides))
150                .sum();
151            Ok(Value::Number(total as f64))
152        });
153    }
154}
155
156impl Default for FunctionLibrary {
157    fn default() -> Self {
158        Self::new()
159    }
160}
161
162// ── argument helpers ──────────────────────────────────────────────────────────
163
164fn require_one_number(name: &str, args: &[Value]) -> Result<f64> {
165    match args {
166        [Value::Number(n)] => Ok(*n),
167        _ => Err(DialogueError::Function {
168            name: name.to_owned(),
169            message: format!("expected 1 number argument, got {args:?}"),
170        }),
171    }
172}
173
174fn require_two_numbers(name: &str, args: &[Value]) -> Result<(f64, f64)> {
175    match args {
176        [Value::Number(a), Value::Number(b)] => Ok((*a, *b)),
177        _ => Err(DialogueError::Function {
178            name: name.to_owned(),
179            message: format!("expected 2 number arguments, got {args:?}"),
180        }),
181    }
182}
183
184#[cfg(test)]
185mod tests;