]> git.lizzy.rs Git - rust.git/blob - src/tools/jsondocck/src/main.rs
e3334e559db65cb7eac656dfe15a040e9c23f5ef
[rust.git] / src / tools / jsondocck / src / main.rs
1 use jsonpath_lib::select;
2 use lazy_static::lazy_static;
3 use regex::{Regex, RegexBuilder};
4 use serde_json::Value;
5 use std::borrow::Cow;
6 use std::{env, fmt, fs};
7
8 mod cache;
9 mod config;
10 mod error;
11
12 use cache::Cache;
13 use config::parse_config;
14 use error::CkError;
15
16 fn main() -> Result<(), String> {
17     let config = parse_config(env::args().collect());
18
19     let mut failed = Vec::new();
20     let mut cache = Cache::new(&config.doc_dir);
21     let commands = get_commands(&config.template)
22         .map_err(|_| format!("Jsondocck failed for {}", &config.template))?;
23
24     for command in commands {
25         if let Err(e) = check_command(command, &mut cache) {
26             failed.push(e);
27         }
28     }
29
30     if failed.is_empty() {
31         Ok(())
32     } else {
33         for i in failed {
34             eprintln!("{}", i);
35         }
36         Err(format!("Jsondocck failed for {}", &config.template))
37     }
38 }
39
40 #[derive(Debug)]
41 pub struct Command {
42     negated: bool,
43     kind: CommandKind,
44     args: Vec<String>,
45     lineno: usize,
46 }
47
48 #[derive(Debug)]
49 pub enum CommandKind {
50     Has,
51     Count,
52     Is,
53     Set,
54 }
55
56 impl CommandKind {
57     fn validate(&self, args: &[String], command_num: usize, lineno: usize) -> bool {
58         let count = match self {
59             CommandKind::Has => (1..=3).contains(&args.len()),
60             CommandKind::Count | CommandKind::Is => 3 == args.len(),
61             CommandKind::Set => 4 == args.len(),
62         };
63
64         if !count {
65             print_err(&format!("Incorrect number of arguments to `@{}`", self), lineno);
66             return false;
67         }
68
69         if args[0] == "-" && command_num == 0 {
70             print_err(&format!("Tried to use the previous path in the first command"), lineno);
71             return false;
72         }
73
74         if let CommandKind::Count = self {
75             if args[2].parse::<usize>().is_err() {
76                 print_err(&format!("Third argument to @count must be a valid usize"), lineno);
77                 return false;
78             }
79         }
80
81         true
82     }
83 }
84
85 impl fmt::Display for CommandKind {
86     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87         let text = match self {
88             CommandKind::Has => "has",
89             CommandKind::Count => "count",
90             CommandKind::Is => "is",
91             CommandKind::Set => "set",
92         };
93         write!(f, "{}", text)
94     }
95 }
96
97 lazy_static! {
98     static ref LINE_PATTERN: Regex = RegexBuilder::new(
99         r#"
100         \s(?P<invalid>!?)@(?P<negated>!?)
101         (?P<cmd>[A-Za-z]+(?:-[A-Za-z]+)*)
102         (?P<args>.*)$
103     "#
104     )
105     .ignore_whitespace(true)
106     .unicode(true)
107     .build()
108     .unwrap();
109 }
110
111 fn print_err(msg: &str, lineno: usize) {
112     eprintln!("Invalid command: {} on line {}", msg, lineno)
113 }
114
115 /// Get a list of commands from a file. Does the work of ensuring the commands
116 /// are syntactically valid.
117 fn get_commands(template: &str) -> Result<Vec<Command>, ()> {
118     let mut commands = Vec::new();
119     let mut errors = false;
120     let file = fs::read_to_string(template).unwrap();
121
122     for (lineno, line) in file.split('\n').enumerate() {
123         let lineno = lineno + 1;
124
125         let cap = match LINE_PATTERN.captures(line) {
126             Some(c) => c,
127             None => continue,
128         };
129
130         let negated = cap.name("negated").unwrap().as_str() == "!";
131         let cmd = cap.name("cmd").unwrap().as_str();
132
133         let cmd = match cmd {
134             "has" => CommandKind::Has,
135             "count" => CommandKind::Count,
136             "is" => CommandKind::Is,
137             "set" => CommandKind::Set,
138             _ => {
139                 print_err(&format!("Unrecognized command name `@{}`", cmd), lineno);
140                 errors = true;
141                 continue;
142             }
143         };
144
145         if let Some(m) = cap.name("invalid") {
146             if m.as_str() == "!" {
147                 print_err(
148                     &format!(
149                         "`!@{0}{1}`, (help: try with `@!{1}`)",
150                         if negated { "!" } else { "" },
151                         cmd,
152                     ),
153                     lineno,
154                 );
155                 errors = true;
156                 continue;
157             }
158         }
159
160         let args = cap.name("args").map_or(Some(vec![]), |m| shlex::split(m.as_str()));
161
162         let args = match args {
163             Some(args) => args,
164             None => {
165                 print_err(
166                     &format!(
167                         "Invalid arguments to shlex::split: `{}`",
168                         cap.name("args").unwrap().as_str()
169                     ),
170                     lineno,
171                 );
172                 errors = true;
173                 continue;
174             }
175         };
176
177         if !cmd.validate(&args, commands.len(), lineno) {
178             errors = true;
179             continue;
180         }
181
182         commands.push(Command { negated, kind: cmd, args, lineno })
183     }
184
185     if !errors { Ok(commands) } else { Err(()) }
186 }
187
188 /// Performs the actual work of ensuring a command passes. Generally assumes the command
189 /// is syntactically valid.
190 fn check_command(command: Command, cache: &mut Cache) -> Result<(), CkError> {
191     // FIXME: Be more granular about why, (e.g. syntax error, count not equal)
192     let result = match command.kind {
193         CommandKind::Has => {
194             match command.args.len() {
195                 // @has <path> = file existence
196                 1 => cache.get_file(&command.args[0]).is_ok(),
197                 // @has <path> <jsonpath> = check path exists
198                 2 => {
199                     let val = cache.get_value(&command.args[0])?;
200
201                     match select(&val, &command.args[1]) {
202                         Ok(results) => !results.is_empty(),
203                         Err(_) => false,
204                     }
205                 }
206                 // @has <path> <jsonpath> <value> = check *any* item matched by path equals value
207                 3 => {
208                     let val = cache.get_value(&command.args[0])?;
209                     match select(&val, &command.args[1]) {
210                         Ok(results) => {
211                             let pat = string_to_value(&command.args[2], cache);
212                             results.contains(&pat.as_ref())
213                         }
214                         Err(_) => false,
215                     }
216                 }
217                 _ => unreachable!(),
218             }
219         }
220         CommandKind::Count => {
221             // @count <path> <jsonpath> <count> = Check that the jsonpath matches exactly [count] times
222             assert_eq!(command.args.len(), 3);
223             let expected: usize = command.args[2].parse().unwrap();
224
225             let val = cache.get_value(&command.args[0])?;
226             match select(&val, &command.args[1]) {
227                 Ok(results) => results.len() == expected,
228                 Err(_) => false,
229             }
230         }
231         CommandKind::Is => {
232             // @has <path> <jsonpath> <value> = check *exactly one* item matched by path, and it equals value
233             assert_eq!(command.args.len(), 3);
234             let val = cache.get_value(&command.args[0])?;
235             match select(&val, &command.args[1]) {
236                 Ok(results) => {
237                     let pat = string_to_value(&command.args[2], cache);
238                     results.len() == 1 && results[0] == pat.as_ref()
239                 }
240                 Err(_) => false,
241             }
242         }
243         // FIXME, Figure out semantics for @!set
244         CommandKind::Set => {
245             // @set <name> = <path> <jsonpath>
246             assert_eq!(command.args.len(), 4);
247             assert_eq!(command.args[1], "=", "Expected an `=`");
248             let val = cache.get_value(&command.args[2])?;
249
250             match select(&val, &command.args[3]) {
251                 Ok(results) => {
252                     assert_eq!(results.len(), 1);
253                     let r = cache.variables.insert(command.args[0].clone(), results[0].clone());
254                     assert!(r.is_none(), "Name collision: {} is duplicated", command.args[0]);
255                     true
256                 }
257                 Err(_) => false,
258             }
259         }
260     };
261
262     if result == command.negated {
263         if command.negated {
264             Err(CkError::FailedCheck(
265                 format!(
266                     "`@!{} {}` matched when it shouldn't",
267                     command.kind,
268                     command.args.join(" ")
269                 ),
270                 command,
271             ))
272         } else {
273             // FIXME: In the future, try 'peeling back' each step, and see at what level the match failed
274             Err(CkError::FailedCheck(
275                 format!(
276                     "`@{} {}` didn't match when it should",
277                     command.kind,
278                     command.args.join(" ")
279                 ),
280                 command,
281             ))
282         }
283     } else {
284         Ok(())
285     }
286 }
287
288 fn string_to_value<'a>(s: &str, cache: &'a Cache) -> Cow<'a, Value> {
289     if s.starts_with("$") {
290         Cow::Borrowed(&cache.variables[&s[1..]])
291     } else {
292         Cow::Owned(serde_json::from_str(s).unwrap())
293     }
294 }