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::run::{decode_setup, require_eval_capability, run_recipe};
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum OpKind {
22 Books,
24 Chapters,
26 List,
28 Show,
30 Setup,
32 Search,
34 Next,
36 Reload,
38 Run,
40}
41
42impl OpKind {
43 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 pub fn symbol(self) -> Symbol {
72 Symbol::qualified("cookbook", self.name())
73 }
74}
75
76pub struct CookbookOp {
78 store: Arc<Mutex<RecipeStore>>,
79 kind: OpKind,
80}
81
82impl CookbookOp {
83 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}