Skip to main content

cli_tutor/content/
mod.rs

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
11// command_modules.LOADING.1 — all TOML files embedded at compile time
12const 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
38// command_modules.LOADING.2-5, VALIDATION.1-4
39pub fn load_modules() -> Vec<ModuleFile> {
40    // command_modules.LOADING.5 — order from modules.toml
41    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        // command_modules.LOADING.3 — panic on bad TOML with clear message
49        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    // command_modules.VALIDATION.1 — duplicate exercise IDs cause panic
55    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    // command_modules.VALIDATION.3 — compile regex match_mode exercises at startup
65    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    // command_modules.VALIDATION.4 — log total count in debug builds
74    #[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    // Basic regex validity check using std — real regex matching in matcher.rs
93    // We use a simple heuristic: attempt to detect obviously invalid patterns.
94    // For full validation, matcher.rs will use its own regex engine if added later.
95    // For now we just ensure the pattern is non-empty.
96    if pattern.trim().is_empty() {
97        panic!("Exercise '{id}' has match_mode=regex but empty expected_output");
98    }
99}