]> git.lizzy.rs Git - rust.git/blob - src/tools/jsondocck/src/main.rs
Auto merge of #97870 - eggyal:inplace_fold_spec, r=wesleywiser
[rust.git] / src / tools / jsondocck / src / main.rs
1 use jsonpath_lib::select;
2 use once_cell::sync::Lazy;
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);
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     IsMany,
54     Set,
55 }
56
57 impl CommandKind {
58     fn validate(&self, args: &[String], lineno: usize) -> bool {
59         let count = match self {
60             CommandKind::Has => (1..=2).contains(&args.len()),
61             CommandKind::IsMany => args.len() >= 2,
62             CommandKind::Count | CommandKind::Is => 2 == args.len(),
63             CommandKind::Set => 3 == args.len(),
64         };
65
66         if !count {
67             print_err(&format!("Incorrect number of arguments to `@{}`", self), lineno);
68             return false;
69         }
70
71         if let CommandKind::Count = self {
72             if args[1].parse::<usize>().is_err() {
73                 print_err(
74                     &format!("Second argument to @count must be a valid usize (got `{}`)", args[2]),
75                     lineno,
76                 );
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::IsMany => "ismany",
90             CommandKind::Count => "count",
91             CommandKind::Is => "is",
92             CommandKind::Set => "set",
93         };
94         write!(f, "{}", text)
95     }
96 }
97
98 static LINE_PATTERN: Lazy<Regex> = Lazy::new(|| {
99     RegexBuilder::new(
100         r#"
101         \s(?P<invalid>!?)@(?P<negated>!?)
102         (?P<cmd>[A-Za-z]+(?:-[A-Za-z]+)*)
103         (?P<args>.*)$
104     "#,
105     )
106     .ignore_whitespace(true)
107     .unicode(true)
108     .build()
109     .unwrap()
110 });
111
112 fn print_err(msg: &str, lineno: usize) {
113     eprintln!("Invalid command: {} on line {}", msg, lineno)
114 }
115
116 /// Get a list of commands from a file. Does the work of ensuring the commands
117 /// are syntactically valid.
118 fn get_commands(template: &str) -> Result<Vec<Command>, ()> {
119     let mut commands = Vec::new();
120     let mut errors = false;
121     let file = fs::read_to_string(template).unwrap();
122
123     for (lineno, line) in file.split('\n').enumerate() {
124         let lineno = lineno + 1;
125
126         let cap = match LINE_PATTERN.captures(line) {
127             Some(c) => c,
128             None => continue,
129         };
130
131         let negated = cap.name("negated").unwrap().as_str() == "!";
132         let cmd = cap.name("cmd").unwrap().as_str();
133
134         let cmd = match cmd {
135             "has" => CommandKind::Has,
136             "count" => CommandKind::Count,
137             "is" => CommandKind::Is,
138             "ismany" => CommandKind::IsMany,
139             "set" => CommandKind::Set,
140             _ => {
141                 print_err(&format!("Unrecognized command name `@{}`", cmd), lineno);
142                 errors = true;
143                 continue;
144             }
145         };
146
147         if let Some(m) = cap.name("invalid") {
148             if m.as_str() == "!" {
149                 print_err(
150                     &format!(
151                         "`!@{0}{1}`, (help: try with `@!{1}`)",
152                         if negated { "!" } else { "" },
153                         cmd,
154                     ),
155                     lineno,
156                 );
157                 errors = true;
158                 continue;
159             }
160         }
161
162         let args = cap.name("args").map_or(Some(vec![]), |m| shlex::split(m.as_str()));
163
164         let args = match args {
165             Some(args) => args,
166             None => {
167                 print_err(
168                     &format!(
169                         "Invalid arguments to shlex::split: `{}`",
170                         cap.name("args").unwrap().as_str()
171                     ),
172                     lineno,
173                 );
174                 errors = true;
175                 continue;
176             }
177         };
178
179         if !cmd.validate(&args, lineno) {
180             errors = true;
181             continue;
182         }
183
184         commands.push(Command { negated, kind: cmd, args, lineno })
185     }
186
187     if !errors { Ok(commands) } else { Err(()) }
188 }
189
190 /// Performs the actual work of ensuring a command passes. Generally assumes the command
191 /// is syntactically valid.
192 fn check_command(command: Command, cache: &mut Cache) -> Result<(), CkError> {
193     // FIXME: Be more granular about why, (e.g. syntax error, count not equal)
194     let result = match command.kind {
195         CommandKind::Has => {
196             match command.args.len() {
197                 // @has <jsonpath> = check path exists
198                 1 => {
199                     let val = cache.value();
200                     let results = select(val, &command.args[0]).unwrap();
201                     !results.is_empty()
202                 }
203                 // @has <jsonpath> <value> = check *any* item matched by path equals value
204                 2 => {
205                     let val = cache.value().clone();
206                     let results = select(&val, &command.args[0]).unwrap();
207                     let pat = string_to_value(&command.args[1], cache);
208                     let has = results.contains(&pat.as_ref());
209                     // Give better error for when @has check fails
210                     if !command.negated && !has {
211                         return Err(CkError::FailedCheck(
212                             format!(
213                                 "{} matched to {:?} but didn't have {:?}",
214                                 &command.args[0],
215                                 results,
216                                 pat.as_ref()
217                             ),
218                             command,
219                         ));
220                     } else {
221                         has
222                     }
223                 }
224                 _ => unreachable!(),
225             }
226         }
227         CommandKind::IsMany => {
228             // @ismany <path> <jsonpath> <value>...
229             let (query, values) = if let [query, values @ ..] = &command.args[..] {
230                 (query, values)
231             } else {
232                 unreachable!("Checked in CommandKind::validate")
233             };
234             let val = cache.value();
235             let got_values = select(val, &query).unwrap();
236             assert!(!command.negated, "`@!ismany` is not supported");
237
238             // Serde json doesn't implement Ord or Hash for Value, so we must
239             // use a Vec here. While in theory that makes setwize equality
240             // O(n^2), in practice n will never be large enought to matter.
241             let expected_values =
242                 values.iter().map(|v| string_to_value(v, cache)).collect::<Vec<_>>();
243             if expected_values.len() != got_values.len() {
244                 return Err(CkError::FailedCheck(
245                     format!(
246                         "Expected {} values, but `{}` matched to {} values ({:?})",
247                         expected_values.len(),
248                         query,
249                         got_values.len(),
250                         got_values
251                     ),
252                     command,
253                 ));
254             };
255             for got_value in got_values {
256                 if !expected_values.iter().any(|exp| &**exp == got_value) {
257                     return Err(CkError::FailedCheck(
258                         format!("`{}` has match {:?}, which was not expected", query, got_value),
259                         command,
260                     ));
261                 }
262             }
263             true
264         }
265         CommandKind::Count => {
266             // @count <jsonpath> <count> = Check that the jsonpath matches exactly [count] times
267             assert_eq!(command.args.len(), 2);
268             let expected: usize = command.args[1].parse().unwrap();
269             let val = cache.value();
270             let results = select(val, &command.args[0]).unwrap();
271             let eq = results.len() == expected;
272             if !command.negated && !eq {
273                 return Err(CkError::FailedCheck(
274                     format!(
275                         "`{}` matched to `{:?}` with length {}, but expected length {}",
276                         &command.args[0],
277                         results,
278                         results.len(),
279                         expected
280                     ),
281                     command,
282                 ));
283             } else {
284                 eq
285             }
286         }
287         CommandKind::Is => {
288             // @has <jsonpath> <value> = check *exactly one* item matched by path, and it equals value
289             assert_eq!(command.args.len(), 2);
290             let val = cache.value().clone();
291             let results = select(&val, &command.args[0]).unwrap();
292             let pat = string_to_value(&command.args[1], cache);
293             let is = results.len() == 1 && results[0] == pat.as_ref();
294             if !command.negated && !is {
295                 return Err(CkError::FailedCheck(
296                     format!(
297                         "{} matched to {:?}, but expected {:?}",
298                         &command.args[0],
299                         results,
300                         pat.as_ref()
301                     ),
302                     command,
303                 ));
304             } else {
305                 is
306             }
307         }
308         CommandKind::Set => {
309             // @set <name> = <jsonpath>
310             assert_eq!(command.args.len(), 3);
311             assert_eq!(command.args[1], "=", "Expected an `=`");
312             let val = cache.value().clone();
313             let results = select(&val, &command.args[2]).unwrap();
314             assert_eq!(
315                 results.len(),
316                 1,
317                 "Expected 1 match for `{}` (because of @set): matched to {:?}",
318                 command.args[2],
319                 results
320             );
321             match results.len() {
322                 0 => false,
323                 1 => {
324                     let r = cache.variables.insert(command.args[0].clone(), results[0].clone());
325                     assert!(r.is_none(), "Name collision: {} is duplicated", command.args[0]);
326                     true
327                 }
328                 _ => {
329                     panic!(
330                         "Got multiple results in `@set` for `{}`: {:?}",
331                         &command.args[2], results,
332                     );
333                 }
334             }
335         }
336     };
337
338     if result == command.negated {
339         if command.negated {
340             Err(CkError::FailedCheck(
341                 format!(
342                     "`@!{} {}` matched when it shouldn't",
343                     command.kind,
344                     command.args.join(" ")
345                 ),
346                 command,
347             ))
348         } else {
349             // FIXME: In the future, try 'peeling back' each step, and see at what level the match failed
350             Err(CkError::FailedCheck(
351                 format!(
352                     "`@{} {}` didn't match when it should",
353                     command.kind,
354                     command.args.join(" ")
355                 ),
356                 command,
357             ))
358         }
359     } else {
360         Ok(())
361     }
362 }
363
364 fn string_to_value<'a>(s: &str, cache: &'a Cache) -> Cow<'a, Value> {
365     if s.starts_with("$") {
366         Cow::Borrowed(&cache.variables.get(&s[1..]).unwrap_or_else(|| {
367             // FIXME(adotinthevoid): Show line number
368             panic!("No variable: `{}`. Current state: `{:?}`", &s[1..], cache.variables)
369         }))
370     } else {
371         Cow::Owned(serde_json::from_str(s).expect(&format!("Cannot convert `{}` to json", s)))
372     }
373 }