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::fs::File;
8use std::io::Read;
9use std::path::PathBuf;
10
11#[derive(Parser)]
12#[grammar = "task.pest"]
13struct TaskParser;
14
15#[derive(Clone, Debug)]
23pub struct Script {
24 pub inline: Option<String>,
28 pub file: Option<PathBuf>,
34}
35
36impl Script {
37 fn new() -> Self {
38 Self {
39 inline: None,
40 file: None,
41 }
42 }
43
44 pub fn has_file(&self) -> bool {
45 self.file.is_some()
46 }
47
48 pub fn as_bytes(&self, parameters: Option<&HashMap<String, String>>) -> Option<Vec<u8>> {
56 use handlebars::Handlebars;
57
58 if self.inline.is_some() && self.file.is_some() {
59 warn!("Both inline and file structs are defined for this script, only file will be used!\n({})",
60 self.inline.as_ref().unwrap());
61 }
62
63 if let Some(path) = &self.file {
64 match File::open(path) {
65 Ok(mut file) => {
66 let mut buf = vec![];
67
68 if let Ok(count) = file.read_to_end(&mut buf) {
69 debug!("Read {} bytes of {}", count, path.display());
70 return Some(buf);
71 } else {
72 error!("Failed to read the file {}", path.display());
73 }
74 }
75 Err(err) => {
76 error!("Failed to open the file at {}, {:?}", path.display(), err);
77 }
78 }
79 }
80
81 if let Some(inline) = &self.inline {
82 if parameters.is_none() {
84 return Some(inline.as_bytes().to_vec());
85 }
86
87 let parameters = parameters.unwrap();
88
89 let mut hb = Handlebars::new();
90 hb.register_escape_fn(handlebars::no_escape);
91 match hb.render_template(inline, ¶meters) {
92 Ok(rendered) => {
93 return Some(rendered.as_bytes().to_vec());
94 }
95 Err(err) => {
96 error!("Failed to render command ({:?}): {}", err, inline);
97 return Some(inline.as_bytes().to_vec());
98 }
99 }
100 }
101
102 None
103 }
104}
105
106#[derive(Clone, Debug)]
107pub struct Task {
108 pub name: String,
109 pub script: Script,
110}
111
112impl Task {
113 pub fn new(name: &str) -> Self {
114 Task {
115 name: name.to_string(),
116 script: Script::new(),
117 }
118 }
119
120 fn parse(parser: &mut Pairs<Rule>) -> Result<Self, PestError<Rule>> {
121 let mut task: Option<Self> = None;
122 let mut inline = None;
123 let mut file = None;
124
125 while let Some(parsed) = parser.next() {
126 match parsed.as_rule() {
127 Rule::identifier => {
128 task = Some(Task::new(parsed.as_str()));
129 }
130 Rule::script => {
131 for pair in parsed.into_inner() {
132 match pair.as_rule() {
133 Rule::script_inline => {
134 inline = Some(parse_str(&mut pair.into_inner())?);
135 }
136 Rule::script_file => {
137 let path = parse_str(&mut pair.into_inner())?;
138 file = Some(PathBuf::from(path));
139 }
140 _ => {}
141 }
142 }
143 }
144 _ => {}
145 }
146 }
147
148 if let Some(mut task) = task {
149 task.script.inline = inline;
150 task.script.file = file;
151
152 return Ok(task);
153 } else {
154 return Err(PestError::new_from_pos(
155 ErrorVariant::CustomError {
156 message: "Could not find a valid task definition".to_string(),
157 },
158 pest::Position::from_start(""),
160 ));
161 }
162 }
163
164 pub fn from_str(buf: &str) -> Result<Self, PestError<Rule>> {
165 let mut parser = TaskParser::parse(Rule::taskfile, buf)?;
166 while let Some(parsed) = parser.next() {
167 match parsed.as_rule() {
168 Rule::task => {
169 return Task::parse(&mut parsed.into_inner());
170 }
171 _ => {}
172 }
173 }
174
175 Err(PestError::new_from_pos(
176 ErrorVariant::CustomError {
177 message: "Could not find a valid task definition".to_string(),
178 },
179 pest::Position::from_start(buf),
180 ))
181 }
182
183 pub fn from_path(path: &PathBuf) -> Result<Self, PestError<Rule>> {
184 match File::open(path) {
185 Ok(mut file) => {
186 let mut contents = String::new();
187
188 if let Err(e) = file.read_to_string(&mut contents) {
189 return Err(PestError::new_from_pos(
190 ErrorVariant::CustomError {
191 message: format!("{}", e),
192 },
193 pest::Position::from_start(""),
194 ));
195 } else {
196 return Self::from_str(&contents);
197 }
198 }
199 Err(e) => {
200 return Err(PestError::new_from_pos(
201 ErrorVariant::CustomError {
202 message: format!("{}", e),
203 },
204 pest::Position::from_start(""),
205 ));
206 }
207 }
208 }
209}
210
211fn parse_str(parser: &mut Pairs<Rule>) -> Result<String, PestError<Rule>> {
216 while let Some(parsed) = parser.next() {
217 match parsed.as_rule() {
218 Rule::string => {
219 return parse_str(&mut parsed.into_inner());
220 }
221 Rule::triple_quoted => {
222 return parse_str(&mut parsed.into_inner());
223 }
224 Rule::single_quoted => {
225 return parse_str(&mut parsed.into_inner());
226 }
227 Rule::inner_single_str => {
228 return Ok(parsed.as_str().to_string());
229 }
230 Rule::inner_triple_str => {
231 return Ok(parsed.as_str().to_string());
232 }
233 _ => {}
234 }
235 }
236 return Err(PestError::new_from_pos(
237 ErrorVariant::CustomError {
238 message: "Could not parse out a string value".to_string(),
239 },
240 pest::Position::from_start(""),
242 ));
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 #[test]
249 fn parse_task_with_comments() {
250 let buf = r#"
251 /*
252 * This is a simple one
253 */
254 task Hey {
255 script {
256 inline = 'echo "hi"'
257 }
258 }"#;
259 let _task = TaskParser::parse(Rule::taskfile, buf)
260 .unwrap()
261 .next()
262 .unwrap();
263 }
264
265 #[test]
266 fn parse_simple_script_task() {
267 let buf = r#"task Install {
268 parameters {
269 package {
270 required = true
271 help = 'Name of package to be installed'
272 type = string
273 }
274 }
275 script {
276 inline = 'zypper in -y {{package}}'
277 }
278 }"#;
279 let _task = TaskParser::parse(Rule::task, buf).unwrap().next().unwrap();
280 }
281
282 #[test]
283 fn parse_no_parameters() {
284 let buf = r#"task PrintEnv {
285 script {
286 inline = 'env'
287 }
288 }"#;
289 let _task = TaskParser::parse(Rule::task, buf).unwrap().next().unwrap();
290 }
291
292 #[test]
293 fn parse_task_fn() {
294 let buf = r#"task PrintEnv {
295 script {
296 inline = 'env'
297 }
298 }"#;
299 let task = Task::from_str(buf).expect("Failed to parse the task");
300 assert_eq!(task.name, "PrintEnv");
301
302 let script = task.script;
303
304 assert_eq!(script.as_bytes(None).unwrap(), "env".as_bytes());
305 }
306
307 #[test]
308 fn parse_task_fn_with_triple_quotes() {
309 let buf = r#"task PrintEnv {
310 script {
311 inline = '''env'''
312 }
313 }"#;
314 let task = Task::from_str(buf).expect("Failed to parse the task");
315 assert_eq!(task.name, "PrintEnv");
316
317 let script = task.script;
318 assert_eq!(script.as_bytes(None).unwrap(), "env".as_bytes());
319 }
320}