1 use jsonpath_lib::select;
2 use lazy_static::lazy_static;
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 {
57 fn validate(&self, args: &[String], command_num: usize, lineno: usize) -> bool {
58 let count = match self {
59 CommandKind::Has => (1..=3).contains(&args.len()),
60 CommandKind::Count | CommandKind::Is => 3 == args.len(),
61 CommandKind::Set => 4 == args.len(),
65 print_err(&format!("Incorrect number of arguments to `@{}`", self), lineno);
69 if args[0] == "-" && command_num == 0 {
70 print_err(&format!("Tried to use the previous path in the first command"), lineno);
74 if let CommandKind::Count = self {
75 if args[2].parse::<usize>().is_err() {
76 print_err(&format!("Third argument to @count must be a valid usize"), lineno);
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::Count => "count",
90 CommandKind::Is => "is",
91 CommandKind::Set => "set",
98 static ref LINE_PATTERN: Regex = RegexBuilder::new(
100 \s(?P<invalid>!?)@(?P<negated>!?)
101 (?P<cmd>[A-Za-z]+(?:-[A-Za-z]+)*)
105 .ignore_whitespace(true)
111 fn print_err(msg: &str, lineno: usize) {
112 eprintln!("Invalid command: {} on line {}", msg, lineno)
115 /// Get a list of commands from a file. Does the work of ensuring the commands
116 /// are syntactically valid.
117 fn get_commands(template: &str) -> Result<Vec<Command>, ()> {
118 let mut commands = Vec::new();
119 let mut errors = false;
120 let file = fs::read_to_string(template).unwrap();
122 for (lineno, line) in file.split('\n').enumerate() {
123 let lineno = lineno + 1;
125 let cap = match LINE_PATTERN.captures(line) {
130 let negated = cap.name("negated").unwrap().as_str() == "!";
131 let cmd = cap.name("cmd").unwrap().as_str();
133 let cmd = match cmd {
134 "has" => CommandKind::Has,
135 "count" => CommandKind::Count,
136 "is" => CommandKind::Is,
137 "set" => CommandKind::Set,
139 print_err(&format!("Unrecognized command name `@{}`", cmd), lineno);
145 if let Some(m) = cap.name("invalid") {
146 if m.as_str() == "!" {
149 "`!@{0}{1}`, (help: try with `@!{1}`)",
150 if negated { "!" } else { "" },
160 let args = cap.name("args").map_or(Some(vec![]), |m| shlex::split(m.as_str()));
162 let args = match args {
167 "Invalid arguments to shlex::split: `{}`",
168 cap.name("args").unwrap().as_str()
177 if !cmd.validate(&args, commands.len(), lineno) {
182 commands.push(Command { negated, kind: cmd, args, lineno })
185 if !errors { Ok(commands) } else { Err(()) }
188 /// Performs the actual work of ensuring a command passes. Generally assumes the command
189 /// is syntactically valid.
190 fn check_command(command: Command, cache: &mut Cache) -> Result<(), CkError> {
191 // FIXME: Be more granular about why, (e.g. syntax error, count not equal)
192 let result = match command.kind {
193 CommandKind::Has => {
194 match command.args.len() {
195 // @has <path> = file existence
196 1 => cache.get_file(&command.args[0]).is_ok(),
197 // @has <path> <jsonpath> = check path exists
199 let val = cache.get_value(&command.args[0])?;
200 let results = select(&val, &command.args[1]).unwrap();
203 // @has <path> <jsonpath> <value> = check *any* item matched by path equals value
205 let val = cache.get_value(&command.args[0])?;
206 let results = select(&val, &command.args[1]).unwrap();
207 let pat = string_to_value(&command.args[2], 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::Count => {
228 // @count <path> <jsonpath> <count> = Check that the jsonpath matches exactly [count] times
229 assert_eq!(command.args.len(), 3);
230 let expected: usize = command.args[2].parse().unwrap();
232 let val = cache.get_value(&command.args[0])?;
233 let results = select(&val, &command.args[1]).unwrap();
234 results.len() == expected
237 // @has <path> <jsonpath> <value> = check *exactly one* item matched by path, and it equals value
238 assert_eq!(command.args.len(), 3);
239 let val = cache.get_value(&command.args[0])?;
240 let results = select(&val, &command.args[1]).unwrap();
241 let pat = string_to_value(&command.args[2], cache);
242 let is = results.len() == 1 && results[0] == pat.as_ref();
243 if !command.negated && !is {
244 return Err(CkError::FailedCheck(
246 "{} matched to {:?}, but expected {:?}",
257 CommandKind::Set => {
258 // @set <name> = <path> <jsonpath>
259 assert_eq!(command.args.len(), 4);
260 assert_eq!(command.args[1], "=", "Expected an `=`");
261 let val = cache.get_value(&command.args[2])?;
262 let results = select(&val, &command.args[3]).unwrap();
266 "Didn't get 1 result for `{}`: got {:?}",
270 match results.len() {
273 let r = cache.variables.insert(command.args[0].clone(), results[0].clone());
274 assert!(r.is_none(), "Name collision: {} is duplicated", command.args[0]);
279 "Got multiple results in `@set` for `{}`: {:?}",
280 &command.args[3], results
287 if result == command.negated {
289 Err(CkError::FailedCheck(
291 "`@!{} {}` matched when it shouldn't",
293 command.args.join(" ")
298 // FIXME: In the future, try 'peeling back' each step, and see at what level the match failed
299 Err(CkError::FailedCheck(
301 "`@{} {}` didn't match when it should",
303 command.args.join(" ")
313 fn string_to_value<'a>(s: &str, cache: &'a Cache) -> Cow<'a, Value> {
314 if s.starts_with("$") {
315 Cow::Borrowed(&cache.variables.get(&s[1..]).unwrap_or_else(|| {
316 // FIXME(adotinthevoid): Show line number
317 panic!("No variable: `{}`. Current state: `{:?}`", &s[1..], cache.variables)
320 Cow::Owned(serde_json::from_str(s).unwrap())