sim/runtime/
cookbook_directory.rs1use 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
45pub 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
53pub fn default_loadable_libs() -> (LoadableLibList, Vec<String>) {
55 ConfigProvider::new(default_cookbook_config(), &SimNestCookbookResolver).loadable_libs()
56}
57
58#[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
97pub 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}