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 LS_TOML: &str = include_str!("../../content/ls.toml");
14const CAT_TOML: &str = include_str!("../../content/cat.toml");
15const HEAD_TOML: &str = include_str!("../../content/head.toml");
16const TAIL_TOML: &str = include_str!("../../content/tail.toml");
17const GREP_TOML: &str = include_str!("../../content/grep.toml");
18const FIND_TOML: &str = include_str!("../../content/find.toml");
19const CUT_TOML: &str = include_str!("../../content/cut.toml");
20const SORT_TOML: &str = include_str!("../../content/sort.toml");
21const UNIQ_TOML: &str = include_str!("../../content/uniq.toml");
22const WC_TOML: &str = include_str!("../../content/wc.toml");
23const TR_TOML: &str = include_str!("../../content/tr.toml");
24const SED_TOML: &str = include_str!("../../content/sed.toml");
25const AWK_TOML: &str = include_str!("../../content/awk.toml");
26const PASTE_TOML: &str = include_str!("../../content/paste.toml");
27const TEE_TOML: &str = include_str!("../../content/tee.toml");
28const DIFF_TOML: &str = include_str!("../../content/diff.toml");
29const XARGS_TOML: &str = include_str!("../../content/xargs.toml");
30const TAR_TOML: &str = include_str!("../../content/tar.toml");
31const CHMOD_TOML: &str = include_str!("../../content/chmod.toml");
32const BC_TOML: &str = include_str!("../../content/bc.toml");
33const GIT_TOML: &str = include_str!("../../content/git.toml");
34const JQ_TOML: &str = include_str!("../../content/jq.toml");
35const MAKE_TOML: &str = include_str!("../../content/make.toml");
36const LOG_PROCESSING_TOML: &str = include_str!("../../content/log-processing.toml");
37const TEXT_PROCESSING_TOML: &str = include_str!("../../content/text-processing.toml");
38
39fn raw_by_name(name: &str) -> Option<&'static str> {
40 match name {
41 "ls" => Some(LS_TOML),
42 "cat" => Some(CAT_TOML),
43 "head" => Some(HEAD_TOML),
44 "tail" => Some(TAIL_TOML),
45 "grep" => Some(GREP_TOML),
46 "find" => Some(FIND_TOML),
47 "cut" => Some(CUT_TOML),
48 "sort" => Some(SORT_TOML),
49 "uniq" => Some(UNIQ_TOML),
50 "wc" => Some(WC_TOML),
51 "tr" => Some(TR_TOML),
52 "sed" => Some(SED_TOML),
53 "awk" => Some(AWK_TOML),
54 "paste" => Some(PASTE_TOML),
55 "tee" => Some(TEE_TOML),
56 "diff" => Some(DIFF_TOML),
57 "xargs" => Some(XARGS_TOML),
58 "tar" => Some(TAR_TOML),
59 "chmod" => Some(CHMOD_TOML),
60 "bc" => Some(BC_TOML),
61 "git" => Some(GIT_TOML),
62 "jq" => Some(JQ_TOML),
63 "make" => Some(MAKE_TOML),
64 "log-processing" => Some(LOG_PROCESSING_TOML),
65 "text-processing" => Some(TEXT_PROCESSING_TOML),
66 _ => None,
67 }
68}
69
70pub fn load_modules() -> Vec<ModuleFile> {
72 let index: ModuleIndex =
74 toml::from_str(MODULES_TOML).expect("content/modules.toml failed to parse");
75
76 let mut modules = Vec::with_capacity(index.modules.len());
77 for name in &index.modules {
78 let raw =
79 raw_by_name(name).unwrap_or_else(|| panic!("No embedded TOML for module '{name}'"));
80 let module: ModuleFile = toml::from_str(raw)
82 .unwrap_or_else(|e| panic!("Failed to parse content/{name}.toml: {e}"));
83 modules.push(module);
84 }
85
86 let mut seen_ids: HashSet<String> = HashSet::new();
88 for m in &modules {
89 for ex in &m.exercises {
90 if !seen_ids.insert(ex.id.clone()) {
91 panic!("Duplicate exercise ID '{}' found", ex.id);
92 }
93 }
94 }
95
96 for m in &modules {
98 for ex in &m.exercises {
99 if ex.match_mode == types::MatchMode::Regex {
100 regex_compile_check(&ex.id, &ex.expected_output);
101 }
102 }
103 }
104
105 #[cfg(debug_assertions)]
107 {
108 let total: usize = modules.iter().map(|m| m.exercises.len()).sum();
109 eprintln!("Loaded {} modules, {} exercises", modules.len(), total);
110 for m in &modules {
111 eprintln!(
112 " {} (v{}): {} exercises",
113 m.module.name,
114 m.module.version,
115 m.exercises.len()
116 );
117 }
118 }
119
120 modules
121}
122
123fn regex_compile_check(id: &str, pattern: &str) {
124 if pattern.trim().is_empty() {
129 panic!("Exercise '{id}' has match_mode=regex but empty expected_output");
130 }
131}