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");
22const WC_TOML: &str = include_str!("../../content/wc.toml");
24const TAR_TOML: &str = include_str!("../../content/tar.toml");
25const CHMOD_TOML: &str = include_str!("../../content/chmod.toml");
26const GIT_TOML: &str = include_str!("../../content/git.toml");
27const JQ_TOML: &str = include_str!("../../content/jq.toml");
28const MAKE_TOML: &str = include_str!("../../content/make.toml");
29
30fn raw_by_name(name: &str) -> Option<&'static str> {
31 match name {
32 "grep" => Some(GREP_TOML),
33 "awk" => Some(AWK_TOML),
34 "sed" => Some(SED_TOML),
35 "find" => Some(FIND_TOML),
36 "xargs" => Some(XARGS_TOML),
37 "cut" => Some(CUT_TOML),
38 "sort" => Some(SORT_TOML),
39 "uniq" => Some(UNIQ_TOML),
40 "tr" => Some(TR_TOML),
41 "wc" => Some(WC_TOML),
42 "tar" => Some(TAR_TOML),
43 "chmod" => Some(CHMOD_TOML),
44 "git" => Some(GIT_TOML),
45 "jq" => Some(JQ_TOML),
46 "make" => Some(MAKE_TOML),
47 _ => None,
48 }
49}
50
51pub fn load_modules() -> Vec<ModuleFile> {
53 let index: ModuleIndex =
55 toml::from_str(MODULES_TOML).expect("content/modules.toml failed to parse");
56
57 let mut modules = Vec::with_capacity(index.modules.len());
58 for name in &index.modules {
59 let raw =
60 raw_by_name(name).unwrap_or_else(|| panic!("No embedded TOML for module '{name}'"));
61 let module: ModuleFile = toml::from_str(raw)
63 .unwrap_or_else(|e| panic!("Failed to parse content/{name}.toml: {e}"));
64 modules.push(module);
65 }
66
67 let mut seen_ids: HashSet<String> = HashSet::new();
69 for m in &modules {
70 for ex in &m.exercises {
71 if !seen_ids.insert(ex.id.clone()) {
72 panic!("Duplicate exercise ID '{}' found", ex.id);
73 }
74 }
75 }
76
77 for m in &modules {
79 for ex in &m.exercises {
80 if ex.match_mode == types::MatchMode::Regex {
81 regex_compile_check(&ex.id, &ex.expected_output);
82 }
83 }
84 }
85
86 #[cfg(debug_assertions)]
88 {
89 let total: usize = modules.iter().map(|m| m.exercises.len()).sum();
90 eprintln!("Loaded {} modules, {} exercises", modules.len(), total);
91 for m in &modules {
92 eprintln!(
93 " {} (v{}): {} exercises",
94 m.module.name,
95 m.module.version,
96 m.exercises.len()
97 );
98 }
99 }
100
101 modules
102}
103
104fn regex_compile_check(id: &str, pattern: &str) {
105 if pattern.trim().is_empty() {
110 panic!("Exercise '{id}' has match_mode=regex but empty expected_output");
111 }
112}