1pub mod types;
2
3use std::collections::HashSet;
4use types::ModuleFile;
5
6#[derive(Debug, serde::Deserialize)]
7struct ModuleIndex {
8 modules: Vec<String>,
9}
10
11const MODULES_TOML: &str = include_str!("../../content/modules.toml");
13const GREP_TOML: &str = include_str!("../../content/grep.toml");
14const AWK_TOML: &str = include_str!("../../content/awk.toml");
15const SED_TOML: &str = include_str!("../../content/sed.toml");
16const FIND_TOML: &str = include_str!("../../content/find.toml");
17const XARGS_TOML: &str = include_str!("../../content/xargs.toml");
18const CUT_TOML: &str = include_str!("../../content/cut.toml");
19const SORT_TOML: &str = include_str!("../../content/sort.toml");
20const UNIQ_TOML: &str = include_str!("../../content/uniq.toml");
21const TR_TOML: &str = include_str!("../../content/tr.toml");
22
23fn raw_by_name(name: &str) -> Option<&'static str> {
24 match name {
25 "grep" => Some(GREP_TOML),
26 "awk" => Some(AWK_TOML),
27 "sed" => Some(SED_TOML),
28 "find" => Some(FIND_TOML),
29 "xargs" => Some(XARGS_TOML),
30 "cut" => Some(CUT_TOML),
31 "sort" => Some(SORT_TOML),
32 "uniq" => Some(UNIQ_TOML),
33 "tr" => Some(TR_TOML),
34 _ => None,
35 }
36}
37
38pub fn load_modules() -> Vec<ModuleFile> {
40 let index: ModuleIndex =
42 toml::from_str(MODULES_TOML).expect("content/modules.toml failed to parse");
43
44 let mut modules = Vec::with_capacity(index.modules.len());
45 for name in &index.modules {
46 let raw =
47 raw_by_name(name).unwrap_or_else(|| panic!("No embedded TOML for module '{name}'"));
48 let module: ModuleFile = toml::from_str(raw)
50 .unwrap_or_else(|e| panic!("Failed to parse content/{name}.toml: {e}"));
51 modules.push(module);
52 }
53
54 let mut seen_ids: HashSet<String> = HashSet::new();
56 for m in &modules {
57 for ex in &m.exercises {
58 if !seen_ids.insert(ex.id.clone()) {
59 panic!("Duplicate exercise ID '{}' found", ex.id);
60 }
61 }
62 }
63
64 for m in &modules {
66 for ex in &m.exercises {
67 if ex.match_mode == types::MatchMode::Regex {
68 regex_compile_check(&ex.id, &ex.expected_output);
69 }
70 }
71 }
72
73 #[cfg(debug_assertions)]
75 {
76 let total: usize = modules.iter().map(|m| m.exercises.len()).sum();
77 eprintln!("Loaded {} modules, {} exercises", modules.len(), total);
78 for m in &modules {
79 eprintln!(
80 " {} (v{}): {} exercises",
81 m.module.name,
82 m.module.version,
83 m.exercises.len()
84 );
85 }
86 }
87
88 modules
89}
90
91fn regex_compile_check(id: &str, pattern: &str) {
92 if pattern.trim().is_empty() {
97 panic!("Exercise '{id}' has match_mode=regex but empty expected_output");
98 }
99}