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::loadable::{LifecycleAction, LoadableLibList, run_lifecycle_action};
18use crate::run::{decode_setup, require_eval_capability, run_recipe};
19
20/// Which cookbook operation a [`CookbookOp`] performs.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum OpKind {
23    /// List the available cookbooks (`cookbook:books`).
24    Books,
25    /// List the chapters of a cookbook (`cookbook:chapters`).
26    Chapters,
27    /// List recipes (`cookbook:list`).
28    List,
29    /// Show one recipe (`cookbook:show`).
30    Show,
31    /// Show a recipe's setup steps (`cookbook:setup`).
32    Setup,
33    /// Search recipes (`cookbook:search`).
34    Search,
35    /// Suggest the next recipe (`cookbook:next`).
36    Next,
37    /// Reload the recipe store (`cookbook:reload`).
38    Reload,
39    /// Run a recipe (`cookbook:run`); capability gated.
40    Run,
41}
42
43impl OpKind {
44    /// Every op kind, for registration.
45    pub const ALL: [OpKind; 9] = [
46        OpKind::Books,
47        OpKind::Chapters,
48        OpKind::List,
49        OpKind::Show,
50        OpKind::Setup,
51        OpKind::Search,
52        OpKind::Next,
53        OpKind::Reload,
54        OpKind::Run,
55    ];
56
57    fn name(self) -> &'static str {
58        match self {
59            OpKind::Books => "books",
60            OpKind::Chapters => "chapters",
61            OpKind::List => "list",
62            OpKind::Show => "show",
63            OpKind::Setup => "setup",
64            OpKind::Search => "search",
65            OpKind::Next => "next",
66            OpKind::Reload => "reload",
67            OpKind::Run => "run",
68        }
69    }
70
71    /// The `cookbook:<name>` symbol this op registers under.
72    pub fn symbol(self) -> Symbol {
73        Symbol::qualified("cookbook", self.name())
74    }
75}
76
77/// A registered cookbook operation over a shared recipe store.
78pub struct CookbookOp {
79    store: Arc<Mutex<RecipeStore>>,
80    kind: OpKind,
81}
82
83impl CookbookOp {
84    /// Build the op for `kind` over `store`.
85    pub fn new(store: Arc<Mutex<RecipeStore>>, kind: OpKind) -> Self {
86        Self { store, kind }
87    }
88
89    fn lookup_card(&self, id: &str) -> Option<RecipeCard> {
90        self.store
91            .lock()
92            .expect("recipe store lock")
93            .card(id)
94            .cloned()
95    }
96}
97
98/// A registered lifecycle operation over a loadable-lib directory.
99pub struct CookbookLifecycleOp {
100    directory: Arc<LoadableLibList>,
101    action: LifecycleAction,
102}
103
104impl CookbookLifecycleOp {
105    /// Build a lifecycle op for `action` over `directory`.
106    pub fn new(directory: Arc<LoadableLibList>, action: LifecycleAction) -> Self {
107        Self { directory, action }
108    }
109
110    /// The `cookbook:<load-lib|unload-lib>` symbol this op registers under.
111    pub fn symbol(action: LifecycleAction) -> Symbol {
112        Symbol::qualified("cookbook", lifecycle_op_name(action))
113    }
114}
115
116impl Object for CookbookLifecycleOp {
117    fn display(&self, _cx: &mut Cx) -> Result<String> {
118        Ok(format!(
119            "#<function {}>",
120            Self::symbol(self.action).as_qualified_str()
121        ))
122    }
123
124    fn as_any(&self) -> &dyn std::any::Any {
125        self
126    }
127}
128
129impl ObjectCompat for CookbookLifecycleOp {
130    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
131        cx.resolve_class(&Symbol::qualified("core", "Function"))
132    }
133
134    fn as_callable(&self) -> Option<&dyn Callable> {
135        Some(self)
136    }
137}
138
139impl Callable for CookbookLifecycleOp {
140    fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
141        let args = args.into_vec();
142        let op = format!("cookbook:{}", lifecycle_op_name(self.action));
143        let lib = arg_string(cx, &args, &op)?;
144        let recipe = format!("cookbook/{}", lifecycle_op_name(self.action));
145        let run = run_lifecycle_action(cx, &self.directory, self.action, &lib, &recipe);
146        run_value(cx, &run)
147    }
148}
149
150impl Object for CookbookOp {
151    fn display(&self, _cx: &mut Cx) -> Result<String> {
152        Ok(format!("#<function {}>", self.kind.symbol()))
153    }
154
155    fn as_any(&self) -> &dyn std::any::Any {
156        self
157    }
158}
159
160impl ObjectCompat for CookbookOp {
161    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
162        cx.resolve_class(&Symbol::qualified("core", "Function"))
163    }
164
165    fn as_callable(&self) -> Option<&dyn Callable> {
166        Some(self)
167    }
168}
169
170impl Callable for CookbookOp {
171    fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
172        let args = args.into_vec();
173        match self.kind {
174            OpKind::Books => {
175                let ids = self.with_store(|store| {
176                    view(store)
177                        .books
178                        .iter()
179                        .map(|b| b.id.clone())
180                        .collect::<Vec<String>>()
181                });
182                ids_value(cx, &ids)
183            }
184            OpKind::Chapters => {
185                let book = arg_string(cx, &args, "cookbook:chapters")?;
186                let names = self.with_store(|store| {
187                    view(store)
188                        .books
189                        .into_iter()
190                        .find(|b| b.id == book)
191                        .map(|b| {
192                            b.chapters
193                                .into_iter()
194                                .map(|c| c.name)
195                                .collect::<Vec<String>>()
196                        })
197                        .unwrap_or_default()
198                });
199                ids_value(cx, &names)
200            }
201            OpKind::List => {
202                let ids = self.with_store(|store| {
203                    ordered_cards(store)
204                        .iter()
205                        .map(|c| c.id.clone())
206                        .collect::<Vec<String>>()
207                });
208                ids_value(cx, &ids)
209            }
210            OpKind::Show => {
211                let id = arg_string(cx, &args, "cookbook:show")?;
212                let card = self.lookup_card(&id).ok_or_else(|| unknown(&id))?;
213                recipe_value(cx, &card)
214            }
215            OpKind::Setup => {
216                let id = arg_string(cx, &args, "cookbook:setup")?;
217                let card = self.lookup_card(&id).ok_or_else(|| unknown(&id))?;
218                let expr = decode_setup(cx, &card)?;
219                cx.factory().expr(expr)
220            }
221            OpKind::Search => {
222                let query = arg_string(cx, &args, "cookbook:search")?;
223                let ids = self.with_store(|store| {
224                    search(store, &query)
225                        .iter()
226                        .map(|c| c.id.clone())
227                        .collect::<Vec<String>>()
228                });
229                ids_value(cx, &ids)
230            }
231            OpKind::Next => {
232                let id = arg_string(cx, &args, "cookbook:next")?;
233                let next_id = self.with_store(|store| next(store, &id).map(|c| c.id.clone()));
234                match next_id {
235                    Some(next_id) => cx.factory().string(next_id),
236                    None => cx.factory().nil(),
237                }
238            }
239            OpKind::Reload => {
240                let count = self.store.lock().expect("recipe store lock").len();
241                count_value(cx, count)
242            }
243            OpKind::Run => {
244                let id = arg_string(cx, &args, "cookbook:run")?;
245                let card = self.lookup_card(&id).ok_or_else(|| unknown(&id))?;
246                require_eval_capability(cx)?;
247                let run = run_recipe(cx, &card)?;
248                run_value(cx, &run)
249            }
250        }
251    }
252}
253
254impl CookbookOp {
255    fn with_store<T>(&self, f: impl FnOnce(&RecipeStore) -> T) -> T {
256        let store = self.store.lock().expect("recipe store lock");
257        f(&store)
258    }
259}
260
261fn unknown(id: &str) -> Error {
262    Error::Eval(format!("unknown recipe `{id}`"))
263}
264
265fn arg_string(cx: &mut Cx, args: &[Value], op: &str) -> Result<String> {
266    let value = args
267        .first()
268        .ok_or_else(|| Error::Eval(format!("{op} expects one argument")))?;
269    match value.object().as_expr(cx)? {
270        Expr::String(s) => Ok(s),
271        Expr::Symbol(sym) => Ok(sym.to_string()),
272        other => Err(Error::Eval(format!(
273            "{op} expects a string id, got {other:?}"
274        ))),
275    }
276}
277
278fn lifecycle_op_name(action: LifecycleAction) -> &'static str {
279    match action {
280        LifecycleAction::Load => "load-lib",
281        LifecycleAction::Unload => "unload-lib",
282    }
283}