diff options
Diffstat (limited to 'src/tools/jsondocck')
-rw-r--r-- | src/tools/jsondocck/src/cache.rs | 70 | ||||
-rw-r--r-- | src/tools/jsondocck/src/main.rs | 122 |
2 files changed, 90 insertions, 102 deletions
diff --git a/src/tools/jsondocck/src/cache.rs b/src/tools/jsondocck/src/cache.rs index a188750c5..f9e542327 100644 --- a/src/tools/jsondocck/src/cache.rs +++ b/src/tools/jsondocck/src/cache.rs @@ -1,77 +1,31 @@ -use crate::error::CkError; +use crate::config::Config; use serde_json::Value; use std::collections::HashMap; -use std::io; -use std::path::{Path, PathBuf}; +use std::path::Path; use fs_err as fs; #[derive(Debug)] pub struct Cache { - root: PathBuf, - files: HashMap<PathBuf, String>, - values: HashMap<PathBuf, Value>, + value: Value, pub variables: HashMap<String, Value>, - last_path: Option<PathBuf>, } impl Cache { /// Create a new cache, used to read files only once and otherwise store their contents. - pub fn new(doc_dir: &str) -> Cache { + pub fn new(config: &Config) -> Cache { + let root = Path::new(&config.doc_dir); + let filename = Path::new(&config.template).file_stem().unwrap(); + let file_path = root.join(&Path::with_extension(Path::new(filename), "json")); + let content = fs::read_to_string(&file_path).expect("failed to read JSON file"); + Cache { - root: Path::new(doc_dir).to_owned(), - files: HashMap::new(), - values: HashMap::new(), + value: serde_json::from_str::<Value>(&content).expect("failed to convert from JSON"), variables: HashMap::new(), - last_path: None, } } - fn resolve_path(&mut self, path: &String) -> PathBuf { - if path != "-" { - let resolve = self.root.join(path); - self.last_path = Some(resolve.clone()); - resolve - } else { - self.last_path - .as_ref() - // FIXME: Point to a line number - .expect("No last path set. Make sure to specify a full path before using `-`") - .clone() - } - } - - fn read_file(&mut self, path: PathBuf) -> Result<String, io::Error> { - if let Some(f) = self.files.get(&path) { - return Ok(f.clone()); - } - - let file = fs::read_to_string(&path)?; - - self.files.insert(path, file.clone()); - - Ok(file) - } - - /// Get the text from a file. If called multiple times, the file will only be read once - pub fn get_file(&mut self, path: &String) -> Result<String, io::Error> { - let path = self.resolve_path(path); - self.read_file(path) - } - - /// Parse the JSON from a file. If called multiple times, the file will only be read once. - pub fn get_value(&mut self, path: &String) -> Result<Value, CkError> { - let path = self.resolve_path(path); - - if let Some(v) = self.values.get(&path) { - return Ok(v.clone()); - } - - let content = self.read_file(path.clone())?; - let val = serde_json::from_str::<Value>(&content)?; - - self.values.insert(path, val.clone()); - - Ok(val) + pub fn value(&self) -> &Value { + &self.value } } diff --git a/src/tools/jsondocck/src/main.rs b/src/tools/jsondocck/src/main.rs index c44624666..76770fe36 100644 --- a/src/tools/jsondocck/src/main.rs +++ b/src/tools/jsondocck/src/main.rs @@ -17,7 +17,7 @@ fn main() -> Result<(), String> { let config = parse_config(env::args().collect()); let mut failed = Vec::new(); - let mut cache = Cache::new(&config.doc_dir); + let mut cache = Cache::new(&config); let commands = get_commands(&config.template) .map_err(|_| format!("Jsondocck failed for {}", &config.template))?; @@ -50,15 +50,17 @@ pub enum CommandKind { Has, Count, Is, + IsMany, Set, } impl CommandKind { - fn validate(&self, args: &[String], command_num: usize, lineno: usize) -> bool { + fn validate(&self, args: &[String], lineno: usize) -> bool { let count = match self { - CommandKind::Has => (1..=3).contains(&args.len()), - CommandKind::Count | CommandKind::Is => 3 == args.len(), - CommandKind::Set => 4 == args.len(), + CommandKind::Has => (1..=2).contains(&args.len()), + CommandKind::IsMany => args.len() >= 2, + CommandKind::Count | CommandKind::Is => 2 == args.len(), + CommandKind::Set => 3 == args.len(), }; if !count { @@ -66,15 +68,10 @@ impl CommandKind { return false; } - if args[0] == "-" && command_num == 0 { - print_err(&format!("Tried to use the previous path in the first command"), lineno); - return false; - } - if let CommandKind::Count = self { - if args[2].parse::<usize>().is_err() { + if args[1].parse::<usize>().is_err() { print_err( - &format!("Third argument to @count must be a valid usize (got `{}`)", args[2]), + &format!("Second argument to @count must be a valid usize (got `{}`)", args[2]), lineno, ); return false; @@ -89,6 +86,7 @@ impl fmt::Display for CommandKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let text = match self { CommandKind::Has => "has", + CommandKind::IsMany => "ismany", CommandKind::Count => "count", CommandKind::Is => "is", CommandKind::Set => "set", @@ -137,6 +135,7 @@ fn get_commands(template: &str) -> Result<Vec<Command>, ()> { "has" => CommandKind::Has, "count" => CommandKind::Count, "is" => CommandKind::Is, + "ismany" => CommandKind::IsMany, "set" => CommandKind::Set, _ => { print_err(&format!("Unrecognized command name `@{}`", cmd), lineno); @@ -177,7 +176,7 @@ fn get_commands(template: &str) -> Result<Vec<Command>, ()> { } }; - if !cmd.validate(&args, commands.len(), lineno) { + if !cmd.validate(&args, lineno) { errors = true; continue; } @@ -195,26 +194,24 @@ fn check_command(command: Command, cache: &mut Cache) -> Result<(), CkError> { let result = match command.kind { CommandKind::Has => { match command.args.len() { - // @has <path> = file existence - 1 => cache.get_file(&command.args[0]).is_ok(), - // @has <path> <jsonpath> = check path exists - 2 => { - let val = cache.get_value(&command.args[0])?; - let results = select(&val, &command.args[1]).unwrap(); + // @has <jsonpath> = check path exists + 1 => { + let val = cache.value(); + let results = select(val, &command.args[0]).unwrap(); !results.is_empty() } - // @has <path> <jsonpath> <value> = check *any* item matched by path equals value - 3 => { - let val = cache.get_value(&command.args[0])?; - let results = select(&val, &command.args[1]).unwrap(); - let pat = string_to_value(&command.args[2], cache); + // @has <jsonpath> <value> = check *any* item matched by path equals value + 2 => { + let val = cache.value().clone(); + let results = select(&val, &command.args[0]).unwrap(); + let pat = string_to_value(&command.args[1], cache); let has = results.contains(&pat.as_ref()); // Give better error for when @has check fails if !command.negated && !has { return Err(CkError::FailedCheck( format!( "{} matched to {:?} but didn't have {:?}", - &command.args[1], + &command.args[0], results, pat.as_ref() ), @@ -227,19 +224,56 @@ fn check_command(command: Command, cache: &mut Cache) -> Result<(), CkError> { _ => unreachable!(), } } - CommandKind::Count => { - // @count <path> <jsonpath> <count> = Check that the jsonpath matches exactly [count] times - assert_eq!(command.args.len(), 3); - let expected: usize = command.args[2].parse().unwrap(); + CommandKind::IsMany => { + // @ismany <path> <jsonpath> <value>... + let (query, values) = if let [query, values @ ..] = &command.args[..] { + (query, values) + } else { + unreachable!("Checked in CommandKind::validate") + }; + let val = cache.value(); + let got_values = select(val, &query).unwrap(); + assert!(!command.negated, "`@!ismany` is not supported"); - let val = cache.get_value(&command.args[0])?; - let results = select(&val, &command.args[1]).unwrap(); + // Serde json doesn't implement Ord or Hash for Value, so we must + // use a Vec here. While in theory that makes setwize equality + // O(n^2), in practice n will never be large enought to matter. + let expected_values = + values.iter().map(|v| string_to_value(v, cache)).collect::<Vec<_>>(); + if expected_values.len() != got_values.len() { + return Err(CkError::FailedCheck( + format!( + "Expected {} values, but `{}` matched to {} values ({:?})", + expected_values.len(), + query, + got_values.len(), + got_values + ), + command, + )); + }; + for got_value in got_values { + if !expected_values.iter().any(|exp| &**exp == got_value) { + return Err(CkError::FailedCheck( + format!("`{}` has match {:?}, which was not expected", query, got_value), + command, + )); + } + } + true + } + CommandKind::Count => { + // @count <jsonpath> <count> = Check that the jsonpath matches exactly [count] times + assert_eq!(command.args.len(), 2); + let expected: usize = command.args[1].parse().unwrap(); + let val = cache.value(); + let results = select(val, &command.args[0]).unwrap(); let eq = results.len() == expected; if !command.negated && !eq { return Err(CkError::FailedCheck( format!( "`{}` matched to `{:?}` with length {}, but expected length {}", - &command.args[1], + &command.args[0], results, results.len(), expected @@ -251,17 +285,17 @@ fn check_command(command: Command, cache: &mut Cache) -> Result<(), CkError> { } } CommandKind::Is => { - // @has <path> <jsonpath> <value> = check *exactly one* item matched by path, and it equals value - assert_eq!(command.args.len(), 3); - let val = cache.get_value(&command.args[0])?; - let results = select(&val, &command.args[1]).unwrap(); - let pat = string_to_value(&command.args[2], cache); + // @has <jsonpath> <value> = check *exactly one* item matched by path, and it equals value + assert_eq!(command.args.len(), 2); + let val = cache.value().clone(); + let results = select(&val, &command.args[0]).unwrap(); + let pat = string_to_value(&command.args[1], cache); let is = results.len() == 1 && results[0] == pat.as_ref(); if !command.negated && !is { return Err(CkError::FailedCheck( format!( "{} matched to {:?}, but expected {:?}", - &command.args[1], + &command.args[0], results, pat.as_ref() ), @@ -272,16 +306,16 @@ fn check_command(command: Command, cache: &mut Cache) -> Result<(), CkError> { } } CommandKind::Set => { - // @set <name> = <path> <jsonpath> - assert_eq!(command.args.len(), 4); + // @set <name> = <jsonpath> + assert_eq!(command.args.len(), 3); assert_eq!(command.args[1], "=", "Expected an `=`"); - let val = cache.get_value(&command.args[2])?; - let results = select(&val, &command.args[3]).unwrap(); + let val = cache.value().clone(); + let results = select(&val, &command.args[2]).unwrap(); assert_eq!( results.len(), 1, "Expected 1 match for `{}` (because of @set): matched to {:?}", - command.args[3], + command.args[2], results ); match results.len() { @@ -294,7 +328,7 @@ fn check_command(command: Command, cache: &mut Cache) -> Result<(), CkError> { _ => { panic!( "Got multiple results in `@set` for `{}`: {:?}", - &command.args[3], results + &command.args[2], results, ); } } |