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 pest::Position::from_start(""),
118 ))
119}
120
121fn 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 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}