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