clitest_lib/parser/
mod.rs

1use crate::script::{Script, ScriptError, ScriptErrorType, ScriptFile, ScriptLocation};
2use std::{
3    collections::{BTreeMap, HashMap},
4    sync::Arc,
5};
6
7pub mod v0;
8
9#[derive(thiserror::Error, Debug)]
10pub enum ScriptReadError {
11    #[error("Error reading script file {file}: {error}")]
12    ParseError {
13        file: ScriptFile,
14        error: ScriptError,
15    },
16    #[error("Error reading script file {file}: {error}")]
17    IoError {
18        file: ScriptFile,
19        error: std::io::Error,
20    },
21}
22
23#[derive(Default)]
24pub struct Scripts {
25    pub scripts: BTreeMap<ScriptFile, Script>,
26}
27
28pub fn parse_script(file: ScriptFile, script: &str) -> Result<Script, ScriptError> {
29    let version = script.lines().next().unwrap_or_default();
30    match version {
31        "#!/usr/bin/env clitest --v0" => v0::parse_script(file, script),
32        _ => Err(ScriptError::new(
33            ScriptErrorType::InvalidVersion,
34            ScriptLocation::new(file, 1),
35        )),
36    }
37}
38
39pub fn parse_script_file(
40    parent: Option<ScriptFile>,
41    file: ScriptFile,
42) -> Result<Script, Vec<ScriptReadError>> {
43    let mut errors = Vec::new();
44    let path = parent
45        .map(|p| Arc::new(p.file.parent().unwrap().join(&*file.file)))
46        .unwrap_or(file.file.clone());
47    let script_contents = match std::fs::read_to_string(path.as_ref()) {
48        Ok(contents) => contents,
49        Err(e) => {
50            errors.push(ScriptReadError::IoError {
51                file: file.clone(),
52                error: e,
53            });
54            return Err(errors);
55        }
56    };
57    let script = parse_script(file.clone(), &script_contents);
58    let mut script = match script {
59        Ok(script) => script,
60        Err(e) => {
61            errors.push(ScriptReadError::ParseError {
62                file: file.clone(),
63                error: e,
64            });
65            return Err(errors);
66        }
67    };
68
69    let mut includes = HashMap::new();
70    for (location, include_path) in script.includes() {
71        // Create a new ScriptFile for the included path
72        // Note: This assumes relative paths from the current script's directory
73        match parse_script_file(Some(location.file.clone()), ScriptFile::new(&include_path)) {
74            Ok(script) => {
75                includes.insert(include_path.clone(), script);
76            }
77            Err(e) => {
78                errors.extend(e);
79            }
80        }
81    }
82
83    script.includes = Arc::new(includes);
84
85    if errors.is_empty() {
86        Ok(script)
87    } else {
88        Err(errors)
89    }
90}
91
92pub fn parse_script_files<T>(files: &[T]) -> Result<Scripts, Vec<ScriptReadError>>
93where
94    for<'a> &'a T: Into<ScriptFile>,
95{
96    let mut errors = Vec::new();
97    let mut scripts = Scripts::default();
98
99    // Add initial files to the queue
100    for file in files {
101        let file = file.into();
102        let script = match parse_script_file(None, file.clone()) {
103            Ok(script) => script,
104            Err(e) => {
105                errors.extend(e);
106                continue;
107            }
108        };
109        scripts.scripts.insert(file, script);
110    }
111
112    if errors.is_empty() {
113        Ok(scripts)
114    } else {
115        Err(errors)
116    }
117}