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);
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], 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(),
67 print_err(&format!("Incorrect number of arguments to `@{}`", self), lineno);
71 if let CommandKind::Count = self {
72 if args[1].parse::<usize>().is_err() {
74 &format!("Second argument to @count must be a valid usize (got `{}`)", args[2]),
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",
98 static LINE_PATTERN: Lazy<Regex> = Lazy::new(|| {
101 \s(?P<invalid>!?)@(?P<negated>!?)
102 (?P<cmd>[A-Za-z]+(?:-[A-Za-z]+)*)
106 .ignore_whitespace(true)
112 fn print_err(msg: &str, lineno: usize) {
113 eprintln!("Invalid command: {} on line {}", msg, lineno)
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();
123 for (lineno, line) in file.split('\n').enumerate() {
124 let lineno = lineno + 1;
126 let cap = match LINE_PATTERN.captures(line) {
131 let negated = cap.name("negated").unwrap().as_str() == "!";
132 let cmd = cap.name("cmd").unwrap().as_str();
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,
141 print_err(&format!("Unrecognized command name `@{}`", cmd), lineno);
147 if let Some(m) = cap.name("invalid") {
148 if m.as_str() == "!" {
151 "`!@{0}{1}`, (help: try with `@!{1}`)",
152 if negated { "!" } else { "" },
162 let args = cap.name("args").map_or(Some(vec![]), |m| shlex::split(m.as_str()));
164 let args = match args {
169 "Invalid arguments to shlex::split: `{}`",
170 cap.name("args").unwrap().as_str()
179 if !cmd.validate(&args, lineno) {
184 commands.push(Command { negated, kind: cmd, args, lineno })
187 if !errors { Ok(commands) } else { Err(()) }
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
199 let val = cache.value();
200 let results = select(val, &command.args[0]).unwrap();
203 // @has <jsonpath> <value> = check *any* item matched by path equals value
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(
213 "{} matched to {:?} but didn't have {:?}",
227 CommandKind::IsMany => {
228 // @ismany <path> <jsonpath> <value>...
229 let (query, values) = if let [query, values @ ..] = &command.args[..] {
232 unreachable!("Checked in CommandKind::validate")
234 let val = cache.value();
235 let got_values = select(val, &query).unwrap();
236 assert!(!command.negated, "`@!ismany` is not supported");
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(
246 "Expected {} values, but `{}` matched to {} values ({:?})",
247 expected_values.len(),
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),
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(
275 "`{}` matched to `{:?}` with length {}, but expected length {}",
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(
297 "{} matched to {:?}, but expected {:?}",
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();
317 "Expected 1 match for `{}` (because of @set): matched to {:?}",
321 match results.len() {
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]);
330 "Got multiple results in `@set` for `{}`: {:?}",
331 &command.args[2], results,
338 if result == command.negated {
340 Err(CkError::FailedCheck(
342 "`@!{} {}` matched when it shouldn't",
344 command.args.join(" ")
349 // FIXME: In the future, try 'peeling back' each step, and see at what level the match failed
350 Err(CkError::FailedCheck(
352 "`@{} {}` didn't match when it should",
354 command.args.join(" ")
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)
371 Cow::Owned(serde_json::from_str(s).expect(&format!("Cannot convert `{}` to json", s)))