1 use jsonpath_lib::select;
2 use once_cell::sync::Lazy;
3 use regex::{Regex, RegexBuilder};
6 use std::{env, fmt, fs};
13 use config::parse_config;
16 fn main() -> Result<(), String> {
17 let config = parse_config(env::args().collect());
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))?;
24 for command in commands {
25 if let Err(e) = check_command(command, &mut cache) {
30 if failed.is_empty() {
36 Err(format!("Jsondocck failed for {}", &config.template))
49 pub enum 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(),
67 print_err(&format!("Incorrect number of arguments to `@{}`", self), lineno);
71 if args[0] == "-" && command_num == 0 {
72 print_err(&format!("Tried to use the previous path in the first command"), lineno);
76 if let CommandKind::Count = self {
77 if args[2].parse::<usize>().is_err() {
79 &format!("Third argument to @count must be a valid usize (got `{}`)", args[2]),
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",
103 static LINE_PATTERN: Lazy<Regex> = Lazy::new(|| {
106 \s(?P<invalid>!?)@(?P<negated>!?)
107 (?P<cmd>[A-Za-z]+(?:-[A-Za-z]+)*)
111 .ignore_whitespace(true)
117 fn print_err(msg: &str, lineno: usize) {
118 eprintln!("Invalid command: {} on line {}", msg, lineno)
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();
128 for (lineno, line) in file.split('\n').enumerate() {
129 let lineno = lineno + 1;
131 let cap = match LINE_PATTERN.captures(line) {
136 let negated = cap.name("negated").unwrap().as_str() == "!";
137 let cmd = cap.name("cmd").unwrap().as_str();
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,
146 print_err(&format!("Unrecognized command name `@{}`", cmd), lineno);
152 if let Some(m) = cap.name("invalid") {
153 if m.as_str() == "!" {
156 "`!@{0}{1}`, (help: try with `@!{1}`)",
157 if negated { "!" } else { "" },
167 let args = cap.name("args").map_or(Some(vec![]), |m| shlex::split(m.as_str()));
169 let args = match args {
174 "Invalid arguments to shlex::split: `{}`",
175 cap.name("args").unwrap().as_str()
184 if !cmd.validate(&args, commands.len(), lineno) {
189 commands.push(Command { negated, kind: cmd, args, lineno })
192 if !errors { Ok(commands) } else { Err(()) }
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
206 let val = cache.get_value(&command.args[0])?;
207 let results = select(&val, &command.args[1]).unwrap();
210 // @has <path> <jsonpath> <value> = check *any* item matched by path equals value
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(
220 "{} matched to {:?} but didn't have {:?}",
234 CommandKind::IsMany => {
235 // @ismany <path> <jsonpath> <value>...
236 let (path, query, values) = if let [path, query, values @ ..] = &command.args[..] {
237 (path, query, values)
239 unreachable!("Checked in CommandKind::validate")
241 let val = cache.get_value(path)?;
242 let got_values = select(&val, &query).unwrap();
243 assert!(!command.negated, "`@!ismany` is not supported");
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(
253 "Expected {} values, but `{}` matched to {} values ({:?})",
254 expected_values.len(),
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),
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();
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(
283 "`{}` matched to `{:?}` with length {}, but expected length {}",
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(
305 "{} matched to {:?}, but expected {:?}",
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();
325 "Expected 1 match for `{}` (because of @set): matched to {:?}",
329 match results.len() {
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]);
338 "Got multiple results in `@set` for `{}`: {:?}",
339 &command.args[3], results
346 if result == command.negated {
348 Err(CkError::FailedCheck(
350 "`@!{} {}` matched when it shouldn't",
352 command.args.join(" ")
357 // FIXME: In the future, try 'peeling back' each step, and see at what level the match failed
358 Err(CkError::FailedCheck(
360 "`@{} {}` didn't match when it should",
362 command.args.join(" ")
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)
379 Cow::Owned(serde_json::from_str(s).expect(&format!("Cannot convert `{}` to json", s)))