use std::sync::Arc;
use sim_cookbook::{
EmbeddedDir, RecipeCard, RecipeRun, RecipeSource, RecipeStore, recipes_from_embedded,
};
use sim_kernel::{Cx, Error, Lib, LibId, Result};
use crate::catalog::LibCatalog;
pub type LibFactory = Arc<dyn Fn() -> Box<dyn Lib + Send + Sync> + Send + Sync>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LifecycleAction {
Load,
Unload,
}
impl LifecycleAction {
pub fn as_str(self) -> &'static str {
match self {
Self::Load => "load",
Self::Unload => "unload",
}
}
}
pub struct LoadableLibEntry {
pub id: String,
pub source: String,
pub title: String,
pub order: i64,
pub recipes: Option<EmbeddedDir>,
pub catalog_lib: Box<dyn Lib + Send + Sync>,
pub factory: LibFactory,
}
pub struct LoadableLibList {
entries: Vec<LoadableLibEntry>,
}
impl LoadableLibList {
pub fn new(entries: Vec<LoadableLibEntry>) -> Self {
Self { entries }
}
pub fn entries(&self) -> &[LoadableLibEntry] {
&self.entries
}
pub fn entry(&self, id: &str) -> Option<&LoadableLibEntry> {
self.entries.iter().find(|entry| entry.id == id)
}
pub fn is_loaded(cx: &Cx, id: &str) -> bool {
Self::loaded_id(cx, id).is_some()
}
pub fn load(&self, cx: &mut Cx, id: &str) -> Result<String> {
if Self::is_loaded(cx, id) {
return Ok(format!("already loaded {id}"));
}
let entry = self
.entry(id)
.ok_or_else(|| Error::Eval(format!("unknown loadable lib `{id}`")))?;
let lib = (entry.factory)();
cx.load_lib(lib.as_ref())?;
Ok(format!("loaded {id}"))
}
pub fn unload(&self, cx: &mut Cx, id: &str) -> Result<String> {
let loaded_id = Self::loaded_id(cx, id)
.ok_or_else(|| Error::Eval(format!("lib `{id}` is not loaded")))?;
cx.unload_lib(loaded_id)?;
Ok(format!("unloaded {id}"))
}
fn loaded_id(cx: &Cx, id: &str) -> Option<LibId> {
let tail = id.rsplit('/').next().unwrap_or(id);
cx.registry()
.libs()
.iter()
.find(|loaded| {
loaded.manifest.id.as_qualified_str() == id
|| loaded.manifest.id.name.as_ref() == id
|| loaded.manifest.id.name.as_ref() == tail
})
.map(|loaded| loaded.id)
}
}
impl LibCatalog for LoadableLibList {
fn resolve(&self, name: &str) -> Option<&dyn Lib> {
self.entries
.iter()
.find(|entry| entry.id == name || entry.id.rsplit('/').next() == Some(name))
.map(|entry| entry.catalog_lib.as_ref() as &dyn Lib)
}
}
pub fn lifecycle_action(card: &RecipeCard) -> Option<(LifecycleAction, String)> {
let action = card
.tags
.iter()
.find_map(|tag| tag.strip_prefix("cookbook-action:"))?;
let lib = card
.tags
.iter()
.find_map(|tag| tag.strip_prefix("cookbook-lib:"))?;
let action = match action {
"load" => LifecycleAction::Load,
"unload" => LifecycleAction::Unload,
_ => return None,
};
Some((action, lib.to_owned()))
}
pub fn run_recipe_with_loadable_libs(
cx: &mut Cx,
directory: &LoadableLibList,
card: &RecipeCard,
) -> Result<RecipeRun> {
if let Some((action, lib)) = lifecycle_action(card) {
return Ok(run_lifecycle_action(cx, directory, action, &lib, &card.id));
}
crate::run::run_recipe_with_catalog(cx, directory, card)
}
pub fn run_lifecycle_action(
cx: &mut Cx,
directory: &LoadableLibList,
action: LifecycleAction,
lib: &str,
recipe: &str,
) -> RecipeRun {
let result = match action {
LifecycleAction::Load => directory.load(cx, lib),
LifecycleAction::Unload => directory.unload(cx, lib),
};
lifecycle_run(recipe, result)
}
fn lifecycle_run(recipe: &str, result: Result<String>) -> RecipeRun {
match result {
Ok(message) => RecipeRun {
recipe: recipe.to_owned(),
forms: 1,
results: vec![message],
checks: Vec::new(),
ok: true,
},
Err(err) => RecipeRun {
recipe: recipe.to_owned(),
forms: 1,
results: vec![err.to_string()],
checks: Vec::new(),
ok: false,
},
}
}
pub fn projected_recipe_store(cx: &Cx, directory: &LoadableLibList) -> Result<RecipeStore> {
let mut store = RecipeStore::new();
for entry in directory.entries() {
if LoadableLibList::is_loaded(cx, &entry.id) {
if let Some(recipes) = entry.recipes {
for mut card in recipes_from_embedded(recipes)
.map_err(|err| Error::Eval(format!("{} recipes: {err}", entry.id)))?
{
card.book_order = entry.order;
store.insert_card(card).map_err(Error::Eval)?;
}
}
store.insert_card(unload_card(entry)).map_err(Error::Eval)?;
} else {
store.insert_card(load_card(entry)).map_err(Error::Eval)?;
}
}
Ok(store)
}
fn load_card(entry: &LoadableLibEntry) -> RecipeCard {
lifecycle_card(
format!("cookbook/load/{}", entry.id),
"cookbook/loadable".to_owned(),
entry,
LifecycleAction::Load,
format!("Load {}", entry.id),
0,
0,
)
}
fn unload_card(entry: &LoadableLibEntry) -> RecipeCard {
lifecycle_card(
format!("{}/cookbook-lifecycle/unload", entry.id),
entry.id.clone(),
entry,
LifecycleAction::Unload,
format!("Unload {}", entry.id),
i64::MAX,
i64::MAX,
)
}
fn lifecycle_card(
id: String,
book: String,
entry: &LoadableLibEntry,
action: LifecycleAction,
title: String,
chapter_order: i64,
order: i64,
) -> RecipeCard {
RecipeCard {
id,
book,
chapter: "cookbook-lifecycle".to_owned(),
chapter_title: "Lifecycle".to_owned(),
chapter_summary: String::new(),
title,
codec: "lisp".to_owned(),
setup: format!("(cookbook/{}-lib {:?})", action.as_str(), entry.id).into_bytes(),
purpose: format!("{} the loadable lib `{}`.", action.as_str(), entry.id),
order,
chapter_order,
book_order: entry.order,
book_title: entry.title.clone(),
book_summary: String::new(),
tags: vec![
format!("cookbook-action:{}", action.as_str()),
format!("cookbook-lib:{}", entry.id),
format!("cookbook-source:{}", entry.source),
],
requires: Vec::new(),
expect: Vec::new(),
source: RecipeSource::Crate {
lib: "sim/cookbook".to_owned(),
},
}
}