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 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
70// command_modules.LOADING.2-5, VALIDATION.1-4
71pub fn load_modules() -> Vec<ModuleFile> {
72    // command_modules.LOADING.5 — order from modules.toml
73    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        // command_modules.LOADING.3 — panic on bad TOML with clear message
81        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    // command_modules.VALIDATION.1 — duplicate exercise IDs cause panic
87    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    // command_modules.VALIDATION.3 — compile regex match_mode exercises at startup
97    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    // command_modules.VALIDATION.4 — log total count in debug builds
106    #[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    // Basic regex validity check using std — real regex matching in matcher.rs
125    // We use a simple heuristic: attempt to detect obviously invalid patterns.
126    // For full validation, matcher.rs will use its own regex engine if added later.
127    // For now we just ensure the pattern is non-empty.
128    if pattern.trim().is_empty() {
129        panic!("Exercise '{id}' has match_mode=regex but empty expected_output");
130    }
131}