Skip to main content

sim_lib_cookbook/
ops.rs

1//! The `cookbook:` operations as registered callables.
2//!
3//! Each op is a [`CookbookOp`] closing over the shared [`RecipeStore`]. Listing
4//! and showing ops extract the data they need under the store lock, drop it,
5//! then build a runtime value (so the lock is never held across `Cx` work or a
6//! recipe run, avoiding re-entrancy deadlocks). `cookbook:run` is capability
7//! gated.
8
9use std::sync::{Arc, Mutex};
10
11use sim_cookbook::{RecipeCard, RecipeStore, next, ordered_cards, search, view};
12use sim_kernel::{
13    Args, Callable, ClassRef, Cx, Error, Expr, Object, ObjectCompat, Result, Symbol, Value,
14};
15
16use crate::build::{count_value, ids_value, recipe_value, run_value};
17use crate::run::{decode_setup, require_eval_capability, run_recipe};
18
19/// Which cookbook operation a [`CookbookOp`] performs.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum OpKind {
22    /// List the available cookbooks (`cookbook:books`).
23    Books,
24    /// List the chapters of a cookbook (`cookbook:chapters`).
25    Chapters,
26    /// List recipes (`cookbook:list`).
27    List,
28    /// Show one recipe (`cookbook:show`).
29    Show,
30    /// Show a recipe's setup steps (`cookbook:setup`).
31    Setup,
32    /// Search recipes (`cookbook:search`).
33    Search,
34    /// Suggest the next recipe (`cookbook:next`).
35    Next,
36    /// Reload the recipe store (`cookbook:reload`).
37    Reload,
38    /// Run a recipe (`cookbook:run`); capability gated.
39    Run,
40}
41
42impl OpKind {
43    /// Every op kind, for registration.
44    pub const ALL: [OpKind; 9] = [
45        OpKind::Books,
46        OpKind::Chapters,
47        OpKind::List,
48        OpKind::Show,
49        OpKind::Setup,
50        OpKind::Search,
51        OpKind::Next,
52        OpKind::Reload,
53        OpKind::Run,
54    ];
55
56    fn name(self) -> &'static str {
57        match self {
58            OpKind::Books => "books",
59            OpKind::Chapters => "chapters",
60            OpKind::List => "list",
61            OpKind::Show => "show",
62            OpKind::Setup => "setup",
63            OpKind::Search => "search",
64            OpKind::Next => "next",
65            OpKind::Reload => "reload",
66            OpKind::Run => "run",
67        }
68    }
69
70    /// The `cookbook:<name>` symbol this op registers under.
71    pub fn symbol(self) -> Symbol {
72        Symbol::qualified("cookbook", self.name())
73    }
74}
75
76/// A registered cookbook operation over a shared recipe store.
77pub struct CookbookOp {
78    store: Arc<Mutex<RecipeStore>>,
79    kind: OpKind,
80}
81
82impl CookbookOp {
83    /// Build the op for `kind` over `store`.
84    pub fn new(store: Arc<Mutex<RecipeStore>>, kind: OpKind) -> Self {
85        Self { store, kind }
86    }
87
88    fn lookup_card(&self, id: &str) -> Option<RecipeCard> {
89        self.store
90            .lock()
91            .expect("recipe store lock")
92            .card(id)
93            .cloned()
94    }
95}
96
97impl Object for CookbookOp {
98    fn display(&self, _cx: &mut Cx) -> Result<String> {
99        Ok(format!("#<function {}>", self.kind.symbol()))
100    }
101
102    fn as_any(&self) -> &dyn std::any::Any {
103        self
104    }
105}
106
107impl ObjectCompat for CookbookOp {
108    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
109        cx.resolve_class(&Symbol::qualified("core", "Function"))
110    }
111
112    fn as_callable(&self) -> Option<&dyn Callable> {
113        Some(self)
114    }
115}
116
117impl Callable for CookbookOp {
118    fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
119        let args = args.into_vec();
120        match self.kind {
121            OpKind::Books => {
122                let ids = self.with_store(|store| {
123                    view(store)
124                        .books
125                        .iter()
126                        .map(|b| b.id.clone())
127                        .collect::<Vec<String>>()
128                });
129                ids_value(cx, &ids)
130            }
131            OpKind::Chapters => {
132                let book = arg_string(cx, &args, "cookbook:chapters")?;
133                let names = self.with_store(|store| {
134                    view(store)
135                        .books
136                        .into_iter()
137                        .find(|b| b.id == book)
138                        .map(|b| {
139                            b.chapters
140                                .into_iter()
141                                .map(|c| c.name)
142                                .collect::<Vec<String>>()
143                        })
144                        .unwrap_or_default()
145                });
146                ids_value(cx, &names)
147            }
148            OpKind::List => {
149                let ids = self.with_store(|store| {
150                    ordered_cards(store)
151                        .iter()
152                        .map(|c| c.id.clone())
153                        .collect::<Vec<String>>()
154                });
155                ids_value(cx, &ids)
156            }
157            OpKind::Show => {
158                let id = arg_string(cx, &args, "cookbook:show")?;
159                let card = self.lookup_card(&id).ok_or_else(|| unknown(&id))?;
160                recipe_value(cx, &card)
161            }
162            OpKind::Setup => {
163                let id = arg_string(cx, &args, "cookbook:setup")?;
164                let card = self.lookup_card(&id).ok_or_else(|| unknown(&id))?;
165                let expr = decode_setup(cx, &card)?;
166                cx.factory().expr(expr)
167            }
168            OpKind::Search => {
169                let query = arg_string(cx, &args, "cookbook:search")?;
170                let ids = self.with_store(|store| {
171                    search(store, &query)
172                        .iter()
173                        .map(|c| c.id.clone())
174                        .collect::<Vec<String>>()
175                });
176                ids_value(cx, &ids)
177            }
178            OpKind::Next => {
179                let id = arg_string(cx, &args, "cookbook:next")?;
180                let next_id = self.with_store(|store| next(store, &id).map(|c| c.id.clone()));
181                match next_id {
182                    Some(next_id) => cx.factory().string(next_id),
183                    None => cx.factory().nil(),
184                }
185            }
186            OpKind::Reload => {
187                let count = self.store.lock().expect("recipe store lock").len();
188                count_value(cx, count)
189            }
190            OpKind::Run => {
191                let id = arg_string(cx, &args, "cookbook:run")?;
192                let card = self.lookup_card(&id).ok_or_else(|| unknown(&id))?;
193                require_eval_capability(cx)?;
194                let run = run_recipe(cx, &card)?;
195                run_value(cx, &run)
196            }
197        }
198    }
199}
200
201impl CookbookOp {
202    fn with_store<T>(&self, f: impl FnOnce(&RecipeStore) -> T) -> T {
203        let store = self.store.lock().expect("recipe store lock");
204        f(&store)
205    }
206}
207
208fn unknown(id: &str) -> Error {
209    Error::Eval(format!("unknown recipe `{id}`"))
210}
211
212fn arg_string(cx: &mut Cx, args: &[Value], op: &str) -> Result<String> {
213    let value = args
214        .first()
215        .ok_or_else(|| Error::Eval(format!("{op} expects one argument")))?;
216    match value.object().as_expr(cx)? {
217        Expr::String(s) => Ok(s),
218        Expr::Symbol(sym) => Ok(sym.to_string()),
219        other => Err(Error::Eval(format!(
220            "{op} expects a string id, got {other:?}"
221        ))),
222    }
223}