zap_model/
plan.rs

1use log::*;
2use pest::error::Error as PestError;
3use pest::error::ErrorVariant;
4use pest::iterators::Pairs;
5use pest::Parser;
6use std::collections::HashMap;
7use std::path::PathBuf;
8
9use crate::ExecutableTask;
10
11#[derive(Parser)]
12#[grammar = "plan.pest"]
13struct PlanParser;
14
15#[derive(Clone, Debug)]
16pub struct Plan {
17    pub tasks: Vec<ExecutableTask>,
18}
19
20impl Plan {
21    pub fn new() -> Self {
22        Self { tasks: vec![] }
23    }
24
25    pub fn from_str(buf: &str) -> Result<Self, PestError<Rule>> {
26        let mut parser = PlanParser::parse(Rule::planfile, buf)?;
27        let mut plan = Plan::new();
28
29        while let Some(parsed) = parser.next() {
30            match parsed.as_rule() {
31                Rule::task => {
32                    let mut raw_task = None;
33                    let mut parameters: HashMap<String, String> = HashMap::new();
34
35                    for pair in parsed.into_inner() {
36                        match pair.as_rule() {
37                            Rule::string => {
38                                let path = PathBuf::from(parse_str(&mut pair.into_inner())?);
39
40                                match crate::task::Task::from_path(&path) {
41                                    Ok(task) => raw_task = Some(task),
42                                    Err(err) => {
43                                        error!("Failed to parse task: {:?}", err);
44                                    }
45                                }
46                            }
47                            Rule::kwarg => {
48                                let (key, val) = parse_kwarg(&mut pair.into_inner())?;
49                                parameters.insert(key, val);
50                            }
51                            _ => {}
52                        }
53                    }
54
55                    if let Some(task) = raw_task {
56                        plan.tasks.push(ExecutableTask::new(task, parameters));
57                    }
58                }
59                _ => {}
60            }
61        }
62
63        Ok(plan)
64    }
65
66    pub fn from_path(path: &PathBuf) -> Result<Self, PestError<Rule>> {
67        use std::fs::File;
68        use std::io::Read;
69
70        match File::open(path) {
71            Ok(mut file) => {
72                let mut contents = String::new();
73
74                if let Err(e) = file.read_to_string(&mut contents) {
75                    return Err(PestError::new_from_pos(
76                        ErrorVariant::CustomError {
77                            message: format!("{}", e),
78                        },
79                        pest::Position::from_start(""),
80                    ));
81                } else {
82                    return Self::from_str(&contents);
83                }
84            }
85            Err(e) => {
86                return Err(PestError::new_from_pos(
87                    ErrorVariant::CustomError {
88                        message: format!("{}", e),
89                    },
90                    pest::Position::from_start(""),
91                ));
92            }
93        }
94    }
95}
96
97fn parse_kwarg(parser: &mut Pairs<Rule>) -> Result<(String, String), PestError<Rule>> {
98    let mut identifier = None;
99    let mut arg = None;
100
101    while let Some(parsed) = parser.next() {
102        match parsed.as_rule() {
103            Rule::identifier => identifier = Some(parsed.as_str().to_string()),
104            Rule::arg => arg = Some(parse_str(&mut parsed.into_inner())?),
105            _ => {}
106        }
107    }
108
109    if identifier.is_some() && arg.is_some() {
110        return Ok((identifier.unwrap(), arg.unwrap()));
111    }
112    Err(PestError::new_from_pos(
113        ErrorVariant::CustomError {
114            message: "Could not parse keyword arguments for parameters".to_string(),
115        },
116        /* TODO: Find a better thing to report */
117        pest::Position::from_start(""),
118    ))
119}
120
121/**
122 * Parser utility function to fish out the _actual_ string value for something
123 * that is looking like a string Rule
124 */
125fn parse_str(parser: &mut Pairs<Rule>) -> Result<String, PestError<Rule>> {
126    while let Some(parsed) = parser.next() {
127        match parsed.as_rule() {
128            Rule::string => {
129                return parse_str(&mut parsed.into_inner());
130            }
131            Rule::triple_quoted => {
132                return parse_str(&mut parsed.into_inner());
133            }
134            Rule::single_quoted => {
135                return parse_str(&mut parsed.into_inner());
136            }
137            Rule::inner_single_str => {
138                return Ok(parsed.as_str().to_string());
139            }
140            Rule::inner_triple_str => {
141                return Ok(parsed.as_str().to_string());
142            }
143            _ => {}
144        }
145    }
146    return Err(PestError::new_from_pos(
147        ErrorVariant::CustomError {
148            message: "Could not parse out a string value".to_string(),
149        },
150        /* TODO: Find a better thing to report */
151        pest::Position::from_start(""),
152    ));
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn parse_simple_plan() {
161        let buf = r#"/*
162 * This zplan just loads a couple tasks and then executes them
163 *
164 * It is expected to be run from the root of the project tree.
165 */
166
167task '../tasks/echo.ztask' {
168    msg = 'Hello from the wonderful world of zplans!'
169}
170
171task '../tasks/echo.ztask' {
172    msg = 'This can actually take inline shells too: $(date)'
173}"#;
174        let _plan = PlanParser::parse(Rule::planfile, buf)
175            .unwrap()
176            .next()
177            .unwrap();
178    }
179
180    #[test]
181    fn parse_plan_fn() {
182        let buf = r#"task '../tasks/echo.ztask' {
183                        msg = 'Hello from the wonderful world of zplans!'
184                    }
185
186                    task '../tasks/echo.ztask' {
187                        msg = 'This can actually take inline shells too: $(date)'
188                    }"#;
189        let plan = Plan::from_str(buf).expect("Failed to parse the plan");
190        assert_eq!(plan.tasks.len(), 2);
191    }
192}