Skip to main content

sim/runtime/
cookbook_directory.rs

1//! sim-nest's product cookbook loadable-lib directory.
2//!
3//! `sim-lib-cookbook` keeps a small standalone fixture directory. The umbrella
4//! crate owns the product directory because only this crate sees the
5//! constellation feature graph without adding back-edges to the cookbook lib.
6
7use std::sync::Arc;
8
9use sim_cookbook::EmbeddedDir;
10use sim_kernel::{CodecId, Lib};
11use sim_lib_cookbook::{
12    ConfigProvider, CookbookConfig, LoadableLibConfig, LoadableLibList, LoadableLibResolver,
13    ResolvedLoadable,
14};
15
16const PRODUCT_CODEC_BASE: u32 = 10_000;
17
18#[macro_use]
19mod audio_stream;
20#[macro_use]
21mod codecs;
22#[macro_use]
23mod data;
24#[macro_use]
25mod femm;
26#[macro_use]
27mod music;
28#[macro_use]
29mod numbers;
30#[macro_use]
31mod runtime_libs;
32
33macro_rules! loadable_libs {
34    ($m:ident) => {
35        cookbook_directory_codecs!($m);
36        cookbook_directory_numbers!($m);
37        cookbook_directory_runtime_libs!($m);
38        cookbook_directory_femm!($m);
39        cookbook_directory_music!($m);
40        cookbook_directory_audio_stream!($m);
41        cookbook_directory_data!($m);
42    };
43}
44
45/// Product default cookbook config for the enabled sim-nest feature set.
46pub fn default_cookbook_config() -> CookbookConfig {
47    CookbookConfig {
48        minimum_loaded: vec!["codec/lisp".to_owned(), "core".to_owned()],
49        loadable_libs: loadable_rows(),
50    }
51}
52
53/// Resolve the product default directory for the enabled sim-nest feature set.
54pub fn default_loadable_libs() -> (LoadableLibList, Vec<String>) {
55    ConfigProvider::new(default_cookbook_config(), &SimNestCookbookResolver).loadable_libs()
56}
57
58/// Loadable-lib ids compiled into a `cookbook-all` build.
59#[cfg(feature = "cookbook-all")]
60pub fn cookbook_all_lib_ids() -> Vec<&'static str> {
61    let mut ids = Vec::new();
62    macro_rules! push_id {
63        ($id:literal, $title:literal, $feature:literal, $recipes:expr, $make:expr) => {
64            #[cfg(feature = $feature)]
65            {
66                let _ = $title;
67                ids.push($id);
68            }
69        };
70    }
71    loadable_libs!(push_id);
72    ids
73}
74
75fn loadable_rows() -> Vec<LoadableLibConfig> {
76    let mut rows = Vec::new();
77    macro_rules! push_row {
78        ($id:literal, $title:literal, $feature:literal, $recipes:expr, $make:expr) => {
79            #[cfg(feature = $feature)]
80            {
81                let _ = $title;
82                rows.push(row($id));
83            }
84        };
85    }
86    loadable_libs!(push_row);
87    rows
88}
89
90fn row(id: &str) -> LoadableLibConfig {
91    LoadableLibConfig {
92        id: id.to_owned(),
93        source: format!("sim-nest:{id}"),
94    }
95}
96
97/// Resolver over the loadable libs linked into this sim-nest build.
98pub struct SimNestCookbookResolver;
99
100impl LoadableLibResolver for SimNestCookbookResolver {
101    fn resolve(&self, source: &str, id: &str) -> Option<ResolvedLoadable> {
102        if source != format!("sim-nest:{id}") {
103            return None;
104        }
105
106        macro_rules! resolve_if {
107            ($row_id:literal, $title:literal, $feature:literal, $recipes:expr, $make:expr) => {
108                #[cfg(feature = $feature)]
109                if id == $row_id {
110                    return Some(resolved($title, $recipes, $make));
111                }
112            };
113        }
114        loadable_libs!(resolve_if);
115        None
116    }
117}
118
119fn codec_id(offset: u32) -> CodecId {
120    CodecId(PRODUCT_CODEC_BASE + offset)
121}
122
123fn resolved<F>(title: &str, recipes: Option<EmbeddedDir>, make: F) -> ResolvedLoadable
124where
125    F: Fn() -> Box<dyn Lib + Send + Sync> + Send + Sync + 'static,
126{
127    ResolvedLoadable {
128        title: title.to_owned(),
129        recipes,
130        factory: Arc::new(make),
131    }
132}
133
134#[cfg(all(test, feature = "cookbook-all"))]
135mod tests {
136    use std::collections::{BTreeMap, BTreeSet};
137
138    #[test]
139    fn every_loadable_lib_has_a_directory_row() {
140        let cfg = super::default_cookbook_config();
141        assert_eq!(cfg.minimum_loaded, ["codec/lisp", "core"]);
142
143        let ids = super::cookbook_all_lib_ids();
144        let mut counts = BTreeMap::new();
145        for row in &cfg.loadable_libs {
146            *counts.entry(row.id.as_str()).or_insert(0usize) += 1;
147            assert_eq!(row.source, format!("sim-nest:{}", row.id));
148        }
149        for id in &ids {
150            assert_eq!(counts.get(id).copied(), Some(1), "{id} row count");
151        }
152        assert_eq!(counts.len(), ids.len());
153
154        let (dir, diags) = super::default_loadable_libs();
155        assert!(diags.is_empty(), "unresolved rows: {diags:?}");
156        let resolved = dir
157            .entries()
158            .iter()
159            .map(|entry| entry.id.as_str())
160            .collect::<BTreeSet<_>>();
161        for id in &ids {
162            assert!(
163                dir.entry(id).is_some(),
164                "loadable lib `{id}` missing a directory row"
165            );
166            assert!(
167                resolved.contains(id),
168                "loadable lib `{id}` missing a factory"
169            );
170        }
171        assert_eq!(dir.entries().len(), ids.len());
172    }
173
174    #[test]
175    fn cookbook_all_feature_matches_directory_features() {
176        let cargo_toml = include_str!("../../Cargo.toml");
177        let cookbook_features = parse_feature(cargo_toml, "cookbook-all");
178        let mut row_features = BTreeSet::new();
179        macro_rules! push_feature {
180            ($id:literal, $title:literal, $feature:literal, $recipes:expr, $make:expr) => {
181                #[cfg(feature = $feature)]
182                {
183                    let _ = ($id, $title);
184                    row_features.insert($feature);
185                }
186            };
187        }
188        loadable_libs!(push_feature);
189
190        for feature in &row_features {
191            assert!(
192                cookbook_features.contains(*feature),
193                "`cookbook-all` does not enable `{feature}`"
194            );
195        }
196
197        let no_directory_rows = BTreeSet::from([
198            "citizen",
199            "cookbook",
200            "exec",
201            "shape",
202            "discrete-rank",
203            "table-fs",
204            "table-http",
205        ]);
206        for feature in cookbook_features {
207            assert!(
208                row_features.contains(feature) || no_directory_rows.contains(feature),
209                "`cookbook-all` enables `{feature}` without a loadable-lib directory row"
210            );
211        }
212    }
213
214    fn parse_feature<'a>(cargo_toml: &'a str, feature: &str) -> BTreeSet<&'a str> {
215        let prefix = format!("{feature} = [");
216        let line = cargo_toml
217            .lines()
218            .find(|line| line.starts_with(&prefix))
219            .expect("feature line");
220        line[prefix.len()..]
221            .trim_end_matches(']')
222            .split(',')
223            .map(str::trim)
224            .filter_map(|part| part.strip_prefix('"')?.strip_suffix('"'))
225            .collect()
226    }
227}