use jsonpath_rust::JsonPathQuery; use regex::Regex; use serde_json::Value; use std::str::FromStr; use std::string::ToString; use std::{error::Error, fmt}; use std::cmp::{Eq, PartialEq, PartialOrd, Ord, Ordering}; use std::fs; use std::path::Path; #[cfg(feature = "jq")] use jq_rs; #[derive(Debug)] pub struct BarbParseError {} impl Error for BarbParseError {} trait PreambleLine { fn is_match(s: String) -> bool; } impl fmt::Display for BarbParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Error parsing barb file") } } pub enum Method { GET, PUT, POST, PATCH, DELETE, } impl FromStr for Method { type Err = BarbParseError; fn from_str(s: &str) -> Result { match s { "GET" => Ok(Self::GET), "PUT" => Ok(Self::PUT), "POST" => Ok(Self::POST), "PATCH" => Ok(Self::PATCH), "DELETE" => Ok(Self::DELETE), _ => Err(BarbParseError {}), } } } impl ToString for Method { fn to_string(&self) -> String { match self { Self::GET => String::from("GET"), Self::PUT => String::from("PUT"), Self::POST => String::from("POST"), Self::PATCH => String::from("PATCH"), Self::DELETE => String::from("DELETE"), } } } impl Method { pub fn takes_body(&self) -> bool { match self { Method::GET => false, Method::DELETE => false, _ => true, } } } #[derive(Debug)] pub struct Header { name: String, value: String, } impl fmt::Display for Header { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}: {}", self.name, self.value) } } impl Header { pub fn name(&self) -> &String { &self.name } pub fn value(&self) -> &String { &self.value } } enum FilterType { JQ, PATH, } pub struct BarbFilter { name: Option, filter: String, filter_type: FilterType, } impl FromStr for BarbFilter { type Err = BarbParseError; fn from_str(s: &str) -> Result { let re = Regex::new("^#(?P[A-Za-z0-9_]*)(?P[|$])(?P.+)$").unwrap(); let groups = re.captures(s).ok_or(BarbParseError {})?; Ok(BarbFilter::new( match &groups["name"] { "" => None, any => Some(String::from(any)), }, String::from(&groups["filter"]), match &groups["type"] { "|" => FilterType::JQ, _ => FilterType::PATH, }, )) } } impl PreambleLine for BarbFilter { fn is_match(s: String) -> bool { let re = Regex::new("^#(?P[A-Za-z0-9_]*)[|$](?P.+)$").unwrap(); re.is_match(s.as_str()) } } impl BarbFilter { fn new(name: Option, filter: String, filter_type: FilterType) -> BarbFilter { BarbFilter { name, filter, filter_type, } } pub fn from_path(filter: String) -> BarbFilter { BarbFilter { name: None, filter_type: FilterType::PATH, filter, } } pub fn name(&self) -> &Option { &self.name } #[cfg(test)] pub fn filter(&self) -> &String { &self.filter } #[cfg(feature = "jq")] fn apply_jq(&self, body: &String) -> Result { if let FilterType::JQ = self.filter_type { return Err(String::from("Incorrect filter type")); } jq_rs::run(self.filter.as_str(), body.as_str()) .map_err(|x| x.to_string()) .map(|x| String::from(x.trim().trim_matches('"'))) } fn apply_path(&self, body: &String) -> Result { if let FilterType::JQ = self.filter_type { return Err(String::from("Incorrect filter type")); } let json: Value = serde_json::from_str(body.as_str()) .map_err(|_| String::from("Failed to decode body"))?; let path = &json.path(self.filter.as_str())?; Ok(match path { Value::Array(val) => match val.len() { 1 => val.first().unwrap(), _ => path, }, _ => path, } .to_string() .trim() .trim_matches('"') .to_string()) } pub fn apply(&self, body: &String) -> Result { match self.filter_type { #[cfg(feature = "jq")] FilterType::JQ => self.apply_jq(body), FilterType::PATH => self.apply_path(body), _ => Ok(body.to_string()), } } } struct BarbPreamble { pub method: Method, pub url: String, pub headers: Vec
, pub filters: Vec, pub dependency: Option, } impl BarbPreamble { fn new(method: Method, url: String, headers: Vec
, filters: Vec, dependency: Option) -> Self { BarbPreamble { method, url, headers, filters, dependency, } } } pub struct BarbFile { file_name: String, preamble: BarbPreamble, body: Option, } impl BarbFile { pub fn headers(&self) -> &Vec
{ &self.preamble.headers } pub fn method(&self) -> &Method { &self.preamble.method } pub fn method_as_string(&self) -> String { self.preamble.method.to_string() } pub fn url(&self) -> &String { &self.preamble.url } pub fn filters(&self) -> &Vec { &self.preamble.filters } pub fn body(&self) -> &Option { &self.body } pub fn dependency(&self) -> Option { let dep = self.preamble.dependency.as_ref()?; let dep_path = Path::new(dep); if !dep_path.is_absolute() { let my_path = Path::new(&self.file_name).parent().or(Some(Path::new("")))?; return Some(String::from(my_path.join(dep_path).to_str()?)); } Some(String::from(dep_path.to_str()?)) } } impl PartialEq for BarbFile { fn eq(&self, other: &Self) -> bool { self.file_name == other.file_name } } impl Eq for BarbFile {} impl PartialOrd for BarbFile { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for BarbFile { fn cmp(&self, other: &Self) -> Ordering { self.file_name.cmp(&other.file_name) } } fn decode_url_line(line: &str) -> Result<(Method, String), BarbParseError> { let mut components = line[1..].split('^'); let meth = components.next().ok_or(BarbParseError {})?; let url = components.next().ok_or(BarbParseError {})?; return Ok((Method::from_str(meth)?, String::from(url))); } fn decode_header(line: &str) -> Result { let mut components = line[1..].split(':'); let header_name = components.next().ok_or(BarbParseError {})?; let header_val = components.next().ok_or(BarbParseError {})?.trim(); Ok(Header { name: String::from(header_name), value: String::from(header_val), }) } impl BarbFile { pub fn from_file(file_name: String) -> Result { let mut bfile = Self::from_str( fs::read_to_string(file_name.as_str()) .map_err(|_| BarbParseError {})? .as_str() ).map_err(|_| BarbParseError {})?; bfile.file_name = file_name; Ok(bfile) } } impl FromStr for BarbFile { type Err = BarbParseError; fn from_str(s: &str) -> Result { let mut lines = s.split('\n'); let (method, url) = decode_url_line(lines.next().ok_or(BarbParseError {})?)?; let mut headers: Vec
= vec![]; let mut filters: Vec = vec![]; let mut dependency: Option = None; for line in &mut lines { if line == "" { // End of header. break; } if let Some(_) = line.find(':') { headers.push(decode_header(line).map_err(|_| BarbParseError {})?); } if let Some('>') = line.chars().nth(1) { dependency = line.get(2..).map(|x| String::from(x)); } if BarbFilter::is_match(String::from(line)) { match BarbFilter::from_str(line) { Ok(filter) => filters.push(filter), Err(_) => (), }; } } let body = lines.fold(String::from(""), |acc, x| acc + x); Ok(BarbFile { file_name: String::from(""), preamble: BarbPreamble::new(method, url, headers, filters, dependency), body: if body == "" { None } else { Some(body) }, }) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_method_from_str() { assert!(matches!(Method::from_str("GET").unwrap(), Method::GET)); assert!(matches!(Method::from_str("PUT").unwrap(), Method::PUT)); assert!(matches!(Method::from_str("POST").unwrap(), Method::POST)); assert!(matches!(Method::from_str("PATCH").unwrap(), Method::PATCH)); assert!(matches!( Method::from_str("DELETE").unwrap(), Method::DELETE )); } #[test] fn test_method_takes_body() { assert!(!Method::GET.takes_body()); assert!(Method::PUT.takes_body()); assert!(Method::POST.takes_body()); assert!(Method::PATCH.takes_body()); assert!(!Method::DELETE.takes_body()); } #[test] fn test_decode_url_line() { let (method, url) = decode_url_line("#GET^http://blahblah").unwrap(); assert!(matches!(method, Method::GET)); assert_eq!(url, "http://blahblah"); } #[test] fn test_decode_header() { let hdr = decode_header("#Authorization: TOKEN 12345").unwrap(); assert_eq!(hdr.name, "Authorization"); assert_eq!(hdr.value, "TOKEN 12345"); } #[test] fn test_parse_barbfile_no_body() { let barbfile = BarbFile::from_str("#GET^https://blah.com/api/blah\n#Authorization: BLAH\n#|filtr\n") .unwrap(); assert!(matches!(barbfile.preamble.method, Method::GET)); assert_eq!(barbfile.preamble.url, "https://blah.com/api/blah"); assert_eq!( barbfile.preamble.filters[0].filter(), &String::from("filtr") ); assert_eq!(barbfile.preamble.headers.len(), 1); assert_eq!(barbfile.preamble.headers[0].name, "Authorization"); assert_eq!(barbfile.preamble.headers[0].value, "BLAH"); assert_eq!(barbfile.body, None); } #[test] fn test_parse_barbfile_body() { let barbfile = BarbFile::from_str("#POST^https://blah.com/api/blah\n#Authorization: BLAH\n#|filtr\n\n{\"key\":\"value\"}\n") .unwrap(); assert!(matches!(barbfile.preamble.method, Method::POST)); assert_eq!(barbfile.preamble.url, "https://blah.com/api/blah"); assert_eq!( barbfile.preamble.filters[0].filter(), &String::from("filtr") ); assert_eq!(barbfile.preamble.headers.len(), 1); assert_eq!(barbfile.preamble.headers[0].name, "Authorization"); assert_eq!(barbfile.preamble.headers[0].value, "BLAH"); assert_eq!(barbfile.body, Some(String::from("{\"key\":\"value\"}"))) } #[test] fn test_jq_parse_named_filter() { let filter = BarbFilter::from_str("#FOO|.bar.foo").unwrap(); assert_eq!(filter.name, Some(String::from("FOO"))); assert_eq!(filter.filter, String::from(".bar.foo")); assert!(matches!(filter.filter_type, FilterType::JQ)); } #[test] fn test_jq_parse_named_filter_no_name() { let filter = BarbFilter::from_str("#|.bar.foo").unwrap(); assert_eq!(filter.name, None); assert_eq!(filter.filter, String::from(".bar.foo")); assert!(matches!(filter.filter_type, FilterType::JQ)); } #[test] fn test_path_parse_named_filter() { let filter = BarbFilter::from_str("#FOO$$.bar.foo").unwrap(); assert_eq!(filter.name, Some(String::from("FOO"))); assert_eq!(filter.filter, String::from("$.bar.foo")); assert!(matches!(filter.filter_type, FilterType::PATH)); } #[test] fn test_path_parse_named_filter_no_name() { let filter = BarbFilter::from_str("#$$.bar.foo").unwrap(); assert_eq!(filter.name, None); assert_eq!(filter.filter, String::from("$.bar.foo")); assert!(matches!(filter.filter_type, FilterType::PATH)); } #[cfg(feature = "jq")] #[test] fn test_apply_filter_jq() { let jq_f = BarbFilter::from_str("#|.status").unwrap(); let subject = String::from(r#"{"status": "OK"}"#); assert_eq!(jq_f.apply(&subject).unwrap(), String::from("OK")); } #[test] fn test_apply_filter_path() { let path_f = BarbFilter::from_str("#$$.status").unwrap(); let subject = String::from(r#"{"status": "OK"}"#); assert_eq!(path_f.apply(&subject).unwrap(), String::from("OK")); } #[test] fn test_parse_dependency() { let bfile = BarbFile::from_str("#GET^http://test.com\n#>blah.barb").unwrap(); assert_eq!(bfile.dependency().as_ref().unwrap(), &String::from("blah.barb")); } #[test] fn test_parse_mult_dependency_keeps_last() { let bfile = BarbFile::from_str("#GET^http://test.com\n#>blah.barb\n#>foo.barb\n#>bar.barb").unwrap(); assert_eq!(bfile.dependency().as_ref().unwrap(), &String::from("bar.barb")); } }