use std::collections::HashSet;
use sim_config::{ConfigView, EffectiveConfig};
use sim_cookbook::EmbeddedDir;
use sim_kernel::{Expr, Symbol};
use sim_value::access::field_any;
use crate::loadable::{LibFactory, LoadableLibEntry, LoadableLibList};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CookbookConfig {
pub minimum_loaded: Vec<String>,
pub loadable_libs: Vec<LoadableLibConfig>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CookbookOverrides {
pub minimum_loaded: Option<Vec<String>>,
pub add: Vec<LoadableLibConfig>,
pub hide: Vec<String>,
pub order: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LoadableLibConfig {
pub id: String,
pub source: String,
}
pub struct ResolvedLoadable {
pub title: String,
pub recipes: Option<EmbeddedDir>,
pub factory: LibFactory,
}
pub trait LoadableLibResolver {
fn resolve(&self, source: &str, id: &str) -> Option<ResolvedLoadable>;
}
pub struct ConfigProvider<'a, R: LoadableLibResolver + ?Sized> {
config: CookbookConfig,
resolver: &'a R,
}
impl<'a, R: LoadableLibResolver + ?Sized> ConfigProvider<'a, R> {
pub fn new(config: CookbookConfig, resolver: &'a R) -> Self {
Self { config, resolver }
}
pub fn minimum_loaded(&self) -> &[String] {
&self.config.minimum_loaded
}
pub fn config(&self) -> &CookbookConfig {
&self.config
}
pub fn loadable_libs(&self) -> (LoadableLibList, Vec<String>) {
let mut entries = Vec::new();
let mut diagnostics = Vec::new();
let mut seen = HashSet::new();
for (index, cfg) in self.config.loadable_libs.iter().enumerate() {
if !seen.insert(cfg.id.as_str()) {
diagnostics.push(format!("duplicate loadable-lib id `{}`", cfg.id));
continue;
}
match self.resolver.resolve(&cfg.source, &cfg.id) {
Some(resolved) => entries.push(LoadableLibEntry {
id: cfg.id.clone(),
source: cfg.source.clone(),
title: resolved.title,
order: index as i64,
recipes: resolved.recipes,
catalog_lib: (resolved.factory)(),
factory: resolved.factory,
}),
None => diagnostics.push(format!(
"unknown loadable-lib source `{}` for `{}`",
cfg.source, cfg.id
)),
}
}
(LoadableLibList::new(entries), diagnostics)
}
}
pub struct ConfigCookbookProvider<'a, R: LoadableLibResolver + ?Sized> {
inner: ConfigProvider<'a, R>,
}
impl<'a, R: LoadableLibResolver + ?Sized> ConfigCookbookProvider<'a, R> {
pub fn new(effective: &EffectiveConfig, resolver: &'a R) -> Self {
Self::new_with_base(effective, built_in_config(), resolver)
}
pub fn new_with_base(
effective: &EffectiveConfig,
base: CookbookConfig,
resolver: &'a R,
) -> Self {
Self {
inner: ConfigProvider::new(
cookbook_config_from_effective_with_base(effective, base),
resolver,
),
}
}
pub fn config(&self) -> &CookbookConfig {
self.inner.config()
}
pub fn minimum_loaded(&self) -> &[String] {
self.inner.minimum_loaded()
}
pub fn loadable_libs(&self) -> (LoadableLibList, Vec<String>) {
self.inner.loadable_libs()
}
}
pub fn cookbook_lib_symbol() -> Symbol {
Symbol::qualified("sim", "cookbook")
}
pub fn cookbook_config_from_effective(effective: &EffectiveConfig) -> CookbookConfig {
cookbook_config_from_effective_with_base(effective, built_in_config())
}
pub fn cookbook_config_from_effective_with_base(
effective: &EffectiveConfig,
base: CookbookConfig,
) -> CookbookConfig {
apply_overrides(base, &cookbook_overrides_from_effective(effective))
}
pub fn cookbook_overrides_from_effective(effective: &EffectiveConfig) -> CookbookOverrides {
let Some(table) = effective.dir.table(&cookbook_lib_symbol()) else {
return CookbookOverrides::default();
};
let view = ConfigView::new(table);
CookbookOverrides {
minimum_loaded: optional_string_array(&view, "minimum_loaded"),
add: loadable_lib_rows(&view),
hide: string_array(&view, "hide"),
order: string_array(&view, "order"),
}
}
pub fn apply_overrides(base: CookbookConfig, overrides: &CookbookOverrides) -> CookbookConfig {
let CookbookConfig {
minimum_loaded,
loadable_libs,
} = base;
let minimum_loaded = overrides.minimum_loaded.clone().unwrap_or(minimum_loaded);
let mut rows: Vec<LoadableLibConfig> = loadable_libs
.into_iter()
.filter(|row| !overrides.hide.contains(&row.id))
.collect();
for add in &overrides.add {
match rows.iter_mut().find(|row| row.id == add.id) {
Some(row) => *row = add.clone(),
None => rows.push(add.clone()),
}
}
if !overrides.order.is_empty() {
let mut indexed = rows.into_iter().enumerate().collect::<Vec<_>>();
indexed.sort_by_key(|(index, row)| {
(
overrides
.order
.iter()
.position(|id| id == &row.id)
.unwrap_or(usize::MAX),
*index,
)
});
rows = indexed.into_iter().map(|(_, row)| row).collect();
}
CookbookConfig {
minimum_loaded,
loadable_libs: rows,
}
}
pub fn built_in_config() -> CookbookConfig {
CookbookConfig {
minimum_loaded: vec!["codec/lisp".to_owned()],
loadable_libs: vec![
loadable("numbers/i64"),
loadable("numbers/arith"),
loadable("numbers/bigint"),
loadable("numbers/bool"),
loadable("numbers/f64"),
loadable("numbers/rational"),
loadable("numbers/complex"),
loadable("numbers/func"),
loadable("numbers/cas"),
loadable("numbers/tensor"),
loadable("numbers/tensor-bcast"),
loadable("discrete"),
loadable("organ/binding"),
loadable("organ/control"),
loadable("organ/sequence"),
loadable("organ/pattern"),
loadable("codec/algol"),
loadable("codec/scheme-r7rs-small"),
loadable("midi/digest"),
],
}
}
fn loadable(id: &str) -> LoadableLibConfig {
LoadableLibConfig {
id: id.to_owned(),
source: format!("symbol:{id}"),
}
}
fn string_array(view: &ConfigView<'_>, key: &str) -> Vec<String> {
view.list(key)
.unwrap_or_default()
.iter()
.filter_map(string_value)
.collect()
}
fn optional_string_array(view: &ConfigView<'_>, key: &str) -> Option<Vec<String>> {
view.get(key).map(|value| match value {
Expr::List(items) => items.iter().filter_map(string_value).collect(),
_ => Vec::new(),
})
}
fn string_value(item: &Expr) -> Option<String> {
match item {
Expr::String(value) => Some(value.clone()),
_ => None,
}
}
fn loadable_lib_rows(view: &ConfigView<'_>) -> Vec<LoadableLibConfig> {
view.list("loadable_lib")
.unwrap_or_default()
.iter()
.map(|entry| LoadableLibConfig {
id: string_at(entry, "id").unwrap_or_default().to_owned(),
source: string_at(entry, "source").unwrap_or_default().to_owned(),
})
.collect()
}
fn string_at<'a>(entry: &'a Expr, key: &str) -> Option<&'a str> {
field_any(entry, key).and_then(|value| match value {
Expr::String(text) => Some(text.as_str()),
_ => None,
})
}