1use std::sync::Arc;
4
5use sim_cookbook::{
6 EmbeddedDir, RecipeCard, RecipeRun, RecipeSource, RecipeStore, recipes_from_embedded,
7};
8use sim_kernel::{Cx, Error, Lib, LibId, Result};
9
10use crate::catalog::LibCatalog;
11
12pub type LibFactory = Arc<dyn Fn() -> Box<dyn Lib + Send + Sync> + Send + Sync>;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum LifecycleAction {
18 Load,
20 Unload,
22}
23
24impl LifecycleAction {
25 pub fn as_str(self) -> &'static str {
27 match self {
28 Self::Load => "load",
29 Self::Unload => "unload",
30 }
31 }
32}
33
34pub struct LoadableLibEntry {
36 pub id: String,
38 pub source: String,
40 pub title: String,
42 pub order: i64,
44 pub recipes: Option<EmbeddedDir>,
46 pub catalog_lib: Box<dyn Lib + Send + Sync>,
48 pub factory: LibFactory,
50}
51
52pub struct LoadableLibList {
54 entries: Vec<LoadableLibEntry>,
55}
56
57impl LoadableLibList {
58 pub fn new(entries: Vec<LoadableLibEntry>) -> Self {
60 Self { entries }
61 }
62
63 pub fn entries(&self) -> &[LoadableLibEntry] {
65 &self.entries
66 }
67
68 pub fn entry(&self, id: &str) -> Option<&LoadableLibEntry> {
70 self.entries.iter().find(|entry| entry.id == id)
71 }
72
73 pub fn is_loaded(cx: &Cx, id: &str) -> bool {
75 Self::loaded_id(cx, id).is_some()
76 }
77
78 pub fn load(&self, cx: &mut Cx, id: &str) -> Result<String> {
83 if Self::is_loaded(cx, id) {
84 return Ok(format!("already loaded {id}"));
85 }
86 let entry = self
87 .entry(id)
88 .ok_or_else(|| Error::Eval(format!("unknown loadable lib `{id}`")))?;
89 let lib = (entry.factory)();
90 cx.load_lib(lib.as_ref())?;
91 Ok(format!("loaded {id}"))
92 }
93
94 pub fn unload(&self, cx: &mut Cx, id: &str) -> Result<String> {
99 let loaded_id = Self::loaded_id(cx, id)
100 .ok_or_else(|| Error::Eval(format!("lib `{id}` is not loaded")))?;
101 cx.unload_lib(loaded_id)?;
102 Ok(format!("unloaded {id}"))
103 }
104
105 fn loaded_id(cx: &Cx, id: &str) -> Option<LibId> {
106 let tail = id.rsplit('/').next().unwrap_or(id);
107 cx.registry()
108 .libs()
109 .iter()
110 .find(|loaded| {
111 loaded.manifest.id.as_qualified_str() == id
112 || loaded.manifest.id.name.as_ref() == id
113 || loaded.manifest.id.name.as_ref() == tail
114 })
115 .map(|loaded| loaded.id)
116 }
117}
118
119impl LibCatalog for LoadableLibList {
120 fn resolve(&self, name: &str) -> Option<&dyn Lib> {
121 self.entries
122 .iter()
123 .find(|entry| entry.id == name || entry.id.rsplit('/').next() == Some(name))
124 .map(|entry| entry.catalog_lib.as_ref() as &dyn Lib)
125 }
126}
127
128pub fn lifecycle_action(card: &RecipeCard) -> Option<(LifecycleAction, String)> {
130 let action = card
131 .tags
132 .iter()
133 .find_map(|tag| tag.strip_prefix("cookbook-action:"))?;
134 let lib = card
135 .tags
136 .iter()
137 .find_map(|tag| tag.strip_prefix("cookbook-lib:"))?;
138 let action = match action {
139 "load" => LifecycleAction::Load,
140 "unload" => LifecycleAction::Unload,
141 _ => return None,
142 };
143 Some((action, lib.to_owned()))
144}
145
146pub fn run_recipe_with_loadable_libs(
152 cx: &mut Cx,
153 directory: &LoadableLibList,
154 card: &RecipeCard,
155) -> Result<RecipeRun> {
156 if let Some((action, lib)) = lifecycle_action(card) {
157 return Ok(run_lifecycle_action(cx, directory, action, &lib, &card.id));
158 }
159 crate::run::run_recipe_with_catalog(cx, directory, card)
160}
161
162pub fn run_lifecycle_action(
164 cx: &mut Cx,
165 directory: &LoadableLibList,
166 action: LifecycleAction,
167 lib: &str,
168 recipe: &str,
169) -> RecipeRun {
170 let result = match action {
171 LifecycleAction::Load => directory.load(cx, lib),
172 LifecycleAction::Unload => directory.unload(cx, lib),
173 };
174 lifecycle_run(recipe, result)
175}
176
177fn lifecycle_run(recipe: &str, result: Result<String>) -> RecipeRun {
178 match result {
179 Ok(message) => RecipeRun {
180 recipe: recipe.to_owned(),
181 forms: 1,
182 results: vec![message],
183 checks: Vec::new(),
184 ok: true,
185 },
186 Err(err) => RecipeRun {
187 recipe: recipe.to_owned(),
188 forms: 1,
189 results: vec![err.to_string()],
190 checks: Vec::new(),
191 ok: false,
192 },
193 }
194}
195
196pub fn projected_recipe_store(cx: &Cx, directory: &LoadableLibList) -> Result<RecipeStore> {
202 let mut store = RecipeStore::new();
203 for entry in directory.entries() {
204 if LoadableLibList::is_loaded(cx, &entry.id) {
205 if let Some(recipes) = entry.recipes {
206 for mut card in recipes_from_embedded(recipes)
207 .map_err(|err| Error::Eval(format!("{} recipes: {err}", entry.id)))?
208 {
209 card.book_order = entry.order;
210 store.insert_card(card).map_err(Error::Eval)?;
211 }
212 }
213 store.insert_card(unload_card(entry)).map_err(Error::Eval)?;
214 } else {
215 store.insert_card(load_card(entry)).map_err(Error::Eval)?;
216 }
217 }
218 Ok(store)
219}
220
221fn load_card(entry: &LoadableLibEntry) -> RecipeCard {
222 lifecycle_card(
223 format!("cookbook/load/{}", entry.id),
224 "cookbook/loadable".to_owned(),
225 entry,
226 LifecycleAction::Load,
227 format!("Load {}", entry.id),
228 0,
229 0,
230 )
231}
232
233fn unload_card(entry: &LoadableLibEntry) -> RecipeCard {
234 lifecycle_card(
235 format!("{}/cookbook-lifecycle/unload", entry.id),
236 entry.id.clone(),
237 entry,
238 LifecycleAction::Unload,
239 format!("Unload {}", entry.id),
240 i64::MAX,
241 i64::MAX,
242 )
243}
244
245fn lifecycle_card(
246 id: String,
247 book: String,
248 entry: &LoadableLibEntry,
249 action: LifecycleAction,
250 title: String,
251 chapter_order: i64,
252 order: i64,
253) -> RecipeCard {
254 RecipeCard {
255 id,
256 book,
257 chapter: "cookbook-lifecycle".to_owned(),
258 chapter_title: "Lifecycle".to_owned(),
259 chapter_summary: String::new(),
260 title,
261 codec: "lisp".to_owned(),
262 setup: format!("(cookbook/{}-lib {:?})", action.as_str(), entry.id).into_bytes(),
263 purpose: format!("{} the loadable lib `{}`.", action.as_str(), entry.id),
264 order,
265 chapter_order,
266 book_order: entry.order,
267 book_title: entry.title.clone(),
268 book_summary: String::new(),
269 tags: vec![
270 format!("cookbook-action:{}", action.as_str()),
271 format!("cookbook-lib:{}", entry.id),
272 format!("cookbook-source:{}", entry.source),
273 ],
274 requires: Vec::new(),
275 expect: Vec::new(),
276 source: RecipeSource::Crate {
277 lib: "sim/cookbook".to_owned(),
278 },
279 }
280}