1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum OpKind {
23 Books,
25 Chapters,
27 List,
29 Show,
31 Setup,
33 Search,
35 Next,
37 Reload,
39 Run,
41}
42
43impl OpKind {
44 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 pub fn symbol(self) -> Symbol {
73 Symbol::qualified("cookbook", self.name())
74 }
75}
76
77pub struct CookbookOp {
79 store: Arc<Mutex<RecipeStore>>,
80 kind: OpKind,
81}
82
83impl CookbookOp {
84 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
98pub struct CookbookLifecycleOp {
100 directory: Arc<LoadableLibList>,
101 action: LifecycleAction,
102}
103
104impl CookbookLifecycleOp {
105 pub fn new(directory: Arc<LoadableLibList>, action: LifecycleAction) -> Self {
107 Self { directory, action }
108 }
109
110 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}