From 9d7038274905a6932630cf9a56c0ca19d63f8408 Mon Sep 17 00:00:00 2001 From: Yorhel Date: Sun, 5 Mar 2017 09:06:48 +0100 Subject: Refactor message handling and IO into separate crates --- .gitignore | 2 + Cargo.toml | 4 +- src/lib.rs | 7 +- src/msg.rs | 130 ----------------------- src/parser.rs | 252 --------------------------------------------- vndbapi-msg/Cargo.toml | 7 ++ vndbapi-msg/src/lib.rs | 6 ++ vndbapi-msg/src/msg.rs | 130 +++++++++++++++++++++++ vndbapi-msg/src/parser.rs | 255 ++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 404 insertions(+), 389 deletions(-) delete mode 100644 src/msg.rs delete mode 100644 src/parser.rs create mode 100644 vndbapi-msg/Cargo.toml create mode 100644 vndbapi-msg/src/lib.rs create mode 100644 vndbapi-msg/src/msg.rs create mode 100644 vndbapi-msg/src/parser.rs diff --git a/.gitignore b/.gitignore index a9d37c5..2745baf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ target Cargo.lock +vndbapi-msg/target +vndbapi-msg/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index a7390a2..1a0e2c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" authors = ["Yorhel "] [dependencies] +vndbapi-msg = { path="vndbapi-msg" } rustls = "0.5.3" lazy_static = "0.2.2" -webpki = "0.8.0" # Must be the same as used by rustls +webpki = "0.10.0" # Must be the same as used by rustls netbuf = "0.3.8" -serde_json = "0.8.4" diff --git a/src/lib.rs b/src/lib.rs index c09ab0b..195158f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,14 +2,11 @@ extern crate rustls; extern crate webpki; extern crate netbuf; -extern crate serde_json; +extern crate vndbapi_msg as msg; mod conn; -mod parser; - -pub mod msg; -pub use conn::Connection; +pub use conn::*; static LE_ROOT: [webpki::TrustAnchor<'static>; 2] = [ // ISRG Root X1 - The Let's Encrypt root cert diff --git a/src/msg.rs b/src/msg.rs deleted file mode 100644 index ead8ac0..0000000 --- a/src/msg.rs +++ /dev/null @@ -1,130 +0,0 @@ -use std::fmt; -use serde_json::Value; - -use parser::{parse_filter,parse_arg,parse_message}; - - -#[derive(Debug,Clone)] -pub enum Arg { - BareString(String), - Json(Value), - Filter(Filter), -} - -#[derive(Debug,Clone)] -pub struct Message { - name: String, - args: Vec, -} - - -#[derive(Debug,Clone)] -pub enum Filter { - And(Box, Box), - Or(Box, Box), - Expr(String, Op, Value), -} - - -#[derive(Debug,Clone,Copy,PartialEq,Eq)] -pub enum Op { - Eq, - NEq, - Le, - LEq, - Gt, - GEq, - Fuzzy, -} - - -impl Op { - pub fn as_str(self) -> &'static str { - match self { - Op::Eq => "=", - Op::NEq => "!=", - Op::Le => "<", - Op::LEq => "<=", - Op::Gt => ">", - Op::GEq => ">=", - Op::Fuzzy => "~", - } - } -} - - -impl fmt::Display for Op { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(self.as_str()) - } -} - - -impl fmt::Display for Filter { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - &Filter::And(ref x, ref y) => write!(f, "({} and {})", x, y), - &Filter::Or(ref x, ref y) => write!(f, "({} or {})", x, y), - &Filter::Expr(ref n, o, ref v) => write!(f, "({} {} {})", n, o, v), - } - } -} - - -impl fmt::Display for Arg { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - &Arg::BareString(ref x) => f.write_str(&x), - &Arg::Json(ref x) => write!(f, "{}", x), - &Arg::Filter(ref x) => write!(f, "{}", x), - } - } -} - - -impl fmt::Display for Message { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(&self.name)?; - for a in self.args.iter() { - f.write_str(" ")?; - write!(f, "{}", a)?; - } - Ok(()) - } -} - - -impl Filter { - pub fn parse(s: &str) -> Result<(Filter, &str), &'static str> { - parse_filter(s) - } -} - - -impl Arg { - pub fn parse(s: &str) -> Result<(Arg, &str), &'static str> { - parse_arg(s) - } -} - - -impl Message { - pub fn parse(s: &str) -> Result { - parse_message(s) - } - - pub fn new(name: &str) -> Result { - if name.contains(|c| !(c >= 'a' && c <= 'z')) { - return Err("Invalid message name") - } - Ok(Message{name: name.to_string(), args: Vec::new()}) - } - - pub fn push_arg(mut self, arg: Arg) -> Message { self.args.push(arg); self } - pub fn push_bare (self, arg: &str ) -> Message { self.push_arg(Arg::BareString(arg.to_string())) } - pub fn push_json (self, arg: Value ) -> Message { self.push_arg(Arg::Json(arg)) } - pub fn push_filter(self, arg: Filter) -> Message { self.push_arg(Arg::Filter(arg)) } - - pub fn name(&self) -> &str { &self.name } - pub fn args(&self) -> &[Arg] { &self.args } -} diff --git a/src/parser.rs b/src/parser.rs deleted file mode 100644 index 0644e45..0000000 --- a/src/parser.rs +++ /dev/null @@ -1,252 +0,0 @@ -use serde_json::Value; - -use msg::{Filter,Op,Arg,Message}; - -type Result = ::std::result::Result; - - - -/* Parse a JSON value with trailing data. This is a workaround until a proper solution has - * been implemented: https://github.com/serde-rs/json/issues/183 */ -fn parse_json(s: &str) -> Result<(Value, &str)> { - let mut bytes = 0; - let val: Value = ::serde_json::de::StreamDeserializer::new(s.bytes().map(|b| { bytes += 1; Ok(b)} )) - .next().ok_or("Expected JSON value")?.map_err(|_| "Invalid JSON value")?; - - // The JSON deserializer consumes one extra byte for numeric types, subtract that. - match val { Value::I64(_) | Value::U64(_) | Value::F64(_) => bytes -= 1, _ => () }; - - Ok((val, &s[bytes..])) -} - - -fn is_ws(c: char) -> bool { c == ' ' || c == '\t' || c == '\r' || c == '\n' } -fn is_filtername(c: char) -> bool { (c >= 'a' && c <= 'z') || c == '_' } -fn is_barestr(c: char) -> bool { (c >= 'a' && c <= 'z') || c == '_' || c == ',' } -fn trim_ws(s: &str) -> &str { s.trim_left_matches(is_ws) } - - - - -#[derive(Debug,PartialEq,Clone,Copy)] -enum Token { - Open, - Close, - And, - Or, - Expr, -} - -pub struct FilterParser<'a> { - buf: &'a str, - hasexpr: bool, -} - - -impl<'a> FilterParser<'a> { - // Consume any whitespace - fn conws(&mut self) { - self.buf = trim_ws(self.buf); - } - - // Consume the given number of bytes - fn con(&mut self, bytes: usize) { - self.buf = &self.buf[bytes..]; - } - - fn token_expr(&mut self) -> Result { - let name: String = self.buf.chars().take_while(|&c| is_filtername(c)).collect(); - if name.len() == 0 { - return Err("Invalid token"); - } - self.con(name.len()); - self.conws(); - - let op = parse_op(self.buf).ok_or("Expected comparison operator")?; - self.con(op.as_str().len()); - self.conws(); - - let val = parse_json(self.buf)?; - self.buf = val.1; - - Ok(Filter::Expr(name, op, val.0)) - } - - // This tokenizer has two states: - // hasexpr (allows And, Or, Close) - // !hasexpr (allows Open, Expr) - // These states are necessary to handle ambiguity between Expr and the And/Or tokens, and are - // also used to enforce the following properties (which simplifies the parsing step): - // - Expr and And/Or tokens cannot be chained - // - And/Or/Close tokens always follow a Close/Expr token. - // - Expr/Open tokens always follow a Open/And/Or token - // - // An Expr token doesn't consume anything, the caller is expected to run token_expr() to get - // the expression and advance the parsing state. - fn token(&mut self) -> Result { - self.conws(); - - let ret = match (self.hasexpr, self.buf.chars().next()) { - (_, None) => Err("Unexpected end of input"), - (false,Some('(')) => { self.con(1); Ok(Token::Open) }, - (true, Some(')')) => { self.con(1); Ok(Token::Close) }, - (true, Some('a')) => if self.buf.starts_with("and") { self.con(3); Ok(Token::And) } else { Err("Invalid token") }, - (true, Some('o')) => if self.buf.starts_with("or") { self.con(2); Ok(Token::Or ) } else { Err("Invalid token") }, - (false,_) => Ok(Token::Expr), - _ => Err("Invalid token"), - }; - - self.hasexpr = match ret { Ok(Token::Close) | Ok(Token::Expr) => true, _ => false }; - ret - } - - fn parse(&mut self) -> Result { - // This is a simple shunting-yard implementation - let mut exp = Vec::new(); - let mut ops = vec![Token::Open]; // Only And, Or and Open - - if self.token()? != Token::Open { - return Err("Filter must start with an open parentheses"); - } - - while ops.len() > 0 { - match self.token()? { - Token::Expr => exp.push(self.token_expr()?), - - op@Token::Open => ops.push(op), - - Token::Close => { - while let Some(op) = ops.pop() { - if op == Token::Open { - break; - } else { - apply(&mut exp, op); - } - } - }, - - o1@Token::And | o1@Token::Or => { - while let Some(&o2) = ops.last() { - if o2 != Token::Open && (o1 != o2 && o1 == Token::Or) { - ops.pop(); - apply(&mut exp, o2); - } else { - break; - } - } - ops.push(o1); - }, - } - } - Ok(exp.pop().unwrap()) - } -} - - -fn apply(exp: &mut Vec, op: Token) { - let right = Box::new(exp.pop().unwrap()); - let left = Box::new(exp.pop().unwrap()); - exp.push(if op == Token::And { Filter::And(left, right) } else { Filter::Or(left, right) }); -} - - -fn parse_op(s: &str) -> Option { - if s.starts_with("=" ) { Some(Op::Eq ) } - else if s.starts_with("!=") { Some(Op::NEq) } - else if s.starts_with("<=") { Some(Op::LEq) } - else if s.starts_with("<" ) { Some(Op::Le ) } - else if s.starts_with(">=") { Some(Op::GEq) } - else if s.starts_with(">" ) { Some(Op::Gt ) } - else if s.starts_with("~" ) { Some(Op::Fuzzy) } - else { None } -} - - -pub fn parse_filter(s: &str) -> Result<(Filter, &str)> { - let mut p = FilterParser{buf: s, hasexpr: false}; - p.parse().map(|r| (r, p.buf)) -} - - -pub fn parse_arg(s: &str) -> Result<(Arg, &str)> { - let s = trim_ws(s); - - // This match on the first character can be replaced by simply trying parse_filter and - // parse_json in sequence; but that results in less than ideal error messages on badly - // formatted input. - match s.chars().next() { - None => return Err("Empty argument"), - - Some('(') => { - return parse_filter(s).map(|(v,r)| (Arg::Filter(v), r)); - }, - - Some('[') | Some('{') | Some('"') => { - return parse_json(s).map(|(v,r)| (Arg::Json(v), r)); - }, - - Some(_) => { - if let Ok((v,r)) = parse_json(s) { - return Ok((Arg::Json(v), r)); - - } else { - let mut splt = s.splitn(2, is_ws); - let v = splt.next().unwrap(); - let rem = splt.next().unwrap_or(""); - - if !v.contains(|c| !is_barestr(c)) { - return Ok((Arg::BareString(v.to_string()), rem)); - } else { - return Err("Invalid argument") - } - } - }, - } -} - - -pub fn parse_message(s: &str) -> Result { - let mut buf = trim_ws(s); - - let mut splt = buf.splitn(2, is_ws); - let name = splt.next().ok_or("Empty message")?; - let mut msg = Message::new(name)?; - - buf = trim_ws(splt.next().unwrap_or("")); - while buf.len() > 0 { - let v = parse_arg(buf)?; - msg = msg.push_arg(v.0); - buf = trim_ws(v.1); - } - - Ok(msg) -} - - -#[test] -fn test_parse_filter() { - let ok = |i, o| { - let s = format!("{}garbage", i); - let f = parse_filter(&s).unwrap(); - assert_eq!(&format!("{}", f.0), o); - assert_eq!(f.1, "garbage"); - }; - ok("(n=1)", "(n = 1)"); - ok("(something_else>=[1,\"str\"])", "(something_else >= [1,\"str\"])"); - ok("(((n=1) and blah=[]))", "((n = 1) and (blah = []))"); - ok("(((n=1) and blah=[] or x=\"hi\"))", "(((n = 1) and (blah = [])) or (x = \"hi\"))"); - ok("(a=1andb=2andc=3)", "((a = 1) and ((b = 2) and (c = 3)))"); - ok("(a=1orb=2andc=3)", "((a = 1) or ((b = 2) and (c = 3)))"); - ok("(a=1orb=2andc=3and(d=4ande=5orf=6)andg=7)", "((a = 1) or ((b = 2) and ((c = 3) and ((((d = 4) and (e = 5)) or (f = 6)) and (g = 7)))))"); - ok("(and=nulloror!=false)", "((and = null) or (or != false))"); - - let nok = |i| { assert!(parse_filter(i).is_err()) }; - nok("()"); - nok("(n=1 n=1)"); - nok("n=1"); - nok("(and)"); - nok("(n=1 and"); - nok("(n=1 and )"); - nok("(n=1 and and n=2)"); - nok(") and n=1"); -} diff --git a/vndbapi-msg/Cargo.toml b/vndbapi-msg/Cargo.toml new file mode 100644 index 0000000..05655f1 --- /dev/null +++ b/vndbapi-msg/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "vndbapi-msg" +version = "0.1.0" +authors = ["Yorhel "] + +[dependencies] +serde_json = "0.9.8" diff --git a/vndbapi-msg/src/lib.rs b/vndbapi-msg/src/lib.rs new file mode 100644 index 0000000..d36199c --- /dev/null +++ b/vndbapi-msg/src/lib.rs @@ -0,0 +1,6 @@ +extern crate serde_json; + +mod parser; +mod msg; + +pub use msg::*; diff --git a/vndbapi-msg/src/msg.rs b/vndbapi-msg/src/msg.rs new file mode 100644 index 0000000..ead8ac0 --- /dev/null +++ b/vndbapi-msg/src/msg.rs @@ -0,0 +1,130 @@ +use std::fmt; +use serde_json::Value; + +use parser::{parse_filter,parse_arg,parse_message}; + + +#[derive(Debug,Clone)] +pub enum Arg { + BareString(String), + Json(Value), + Filter(Filter), +} + +#[derive(Debug,Clone)] +pub struct Message { + name: String, + args: Vec, +} + + +#[derive(Debug,Clone)] +pub enum Filter { + And(Box, Box), + Or(Box, Box), + Expr(String, Op, Value), +} + + +#[derive(Debug,Clone,Copy,PartialEq,Eq)] +pub enum Op { + Eq, + NEq, + Le, + LEq, + Gt, + GEq, + Fuzzy, +} + + +impl Op { + pub fn as_str(self) -> &'static str { + match self { + Op::Eq => "=", + Op::NEq => "!=", + Op::Le => "<", + Op::LEq => "<=", + Op::Gt => ">", + Op::GEq => ">=", + Op::Fuzzy => "~", + } + } +} + + +impl fmt::Display for Op { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.as_str()) + } +} + + +impl fmt::Display for Filter { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + &Filter::And(ref x, ref y) => write!(f, "({} and {})", x, y), + &Filter::Or(ref x, ref y) => write!(f, "({} or {})", x, y), + &Filter::Expr(ref n, o, ref v) => write!(f, "({} {} {})", n, o, v), + } + } +} + + +impl fmt::Display for Arg { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + &Arg::BareString(ref x) => f.write_str(&x), + &Arg::Json(ref x) => write!(f, "{}", x), + &Arg::Filter(ref x) => write!(f, "{}", x), + } + } +} + + +impl fmt::Display for Message { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&self.name)?; + for a in self.args.iter() { + f.write_str(" ")?; + write!(f, "{}", a)?; + } + Ok(()) + } +} + + +impl Filter { + pub fn parse(s: &str) -> Result<(Filter, &str), &'static str> { + parse_filter(s) + } +} + + +impl Arg { + pub fn parse(s: &str) -> Result<(Arg, &str), &'static str> { + parse_arg(s) + } +} + + +impl Message { + pub fn parse(s: &str) -> Result { + parse_message(s) + } + + pub fn new(name: &str) -> Result { + if name.contains(|c| !(c >= 'a' && c <= 'z')) { + return Err("Invalid message name") + } + Ok(Message{name: name.to_string(), args: Vec::new()}) + } + + pub fn push_arg(mut self, arg: Arg) -> Message { self.args.push(arg); self } + pub fn push_bare (self, arg: &str ) -> Message { self.push_arg(Arg::BareString(arg.to_string())) } + pub fn push_json (self, arg: Value ) -> Message { self.push_arg(Arg::Json(arg)) } + pub fn push_filter(self, arg: Filter) -> Message { self.push_arg(Arg::Filter(arg)) } + + pub fn name(&self) -> &str { &self.name } + pub fn args(&self) -> &[Arg] { &self.args } +} diff --git a/vndbapi-msg/src/parser.rs b/vndbapi-msg/src/parser.rs new file mode 100644 index 0000000..135c71d --- /dev/null +++ b/vndbapi-msg/src/parser.rs @@ -0,0 +1,255 @@ +use serde_json::Value; + +use msg::{Filter,Op,Arg,Message}; + +type Result = ::std::result::Result; + + + +/* Parse a JSON value with trailing data. This is a workaround until a proper solution has + * been implemented: https://github.com/serde-rs/json/issues/183 */ +fn parse_json(s: &str) -> Result<(Value, &str)> { + println!("Parsing JSON: {}", s); + let mut bytes = 0; + let val: Value = ::serde_json::Deserializer::from_iter(s.bytes().map(|b| { bytes += 1; Ok(b)} )) + .into_iter().next() + .ok_or("Expected JSON value")? + .map_err(|_| "Invalid JSON value")?; + + // The JSON deserializer consumes one extra byte for numeric types, subtract that. + match val { Value::Number(_) => bytes -= 1, _ => () }; + + Ok((val, &s[bytes..])) +} + + +fn is_ws(c: char) -> bool { c == ' ' || c == '\t' || c == '\r' || c == '\n' } +fn is_filtername(c: char) -> bool { (c >= 'a' && c <= 'z') || c == '_' } +fn is_barestr(c: char) -> bool { (c >= 'a' && c <= 'z') || c == '_' || c == ',' } +fn trim_ws(s: &str) -> &str { s.trim_left_matches(is_ws) } + + + + +#[derive(Debug,PartialEq,Clone,Copy)] +enum Token { + Open, + Close, + And, + Or, + Expr, +} + +pub struct FilterParser<'a> { + buf: &'a str, + hasexpr: bool, +} + + +impl<'a> FilterParser<'a> { + // Consume any whitespace + fn conws(&mut self) { + self.buf = trim_ws(self.buf); + } + + // Consume the given number of bytes + fn con(&mut self, bytes: usize) { + self.buf = &self.buf[bytes..]; + } + + fn token_expr(&mut self) -> Result { + let name: String = self.buf.chars().take_while(|&c| is_filtername(c)).collect(); + if name.len() == 0 { + return Err("Invalid token"); + } + self.con(name.len()); + self.conws(); + + let op = parse_op(self.buf).ok_or("Expected comparison operator")?; + self.con(op.as_str().len()); + self.conws(); + + let val = parse_json(self.buf)?; + self.buf = val.1; + + Ok(Filter::Expr(name, op, val.0)) + } + + // This tokenizer has two states: + // hasexpr (allows And, Or, Close) + // !hasexpr (allows Open, Expr) + // These states are necessary to handle ambiguity between Expr and the And/Or tokens, and are + // also used to enforce the following properties (which simplifies the parsing step): + // - Expr and And/Or tokens cannot be chained + // - And/Or/Close tokens always follow a Close/Expr token. + // - Expr/Open tokens always follow a Open/And/Or token + // + // An Expr token doesn't consume anything, the caller is expected to run token_expr() to get + // the expression and advance the parsing state. + fn token(&mut self) -> Result { + self.conws(); + + let ret = match (self.hasexpr, self.buf.chars().next()) { + (_, None) => Err("Unexpected end of input"), + (false,Some('(')) => { self.con(1); Ok(Token::Open) }, + (true, Some(')')) => { self.con(1); Ok(Token::Close) }, + (true, Some('a')) => if self.buf.starts_with("and") { self.con(3); Ok(Token::And) } else { Err("Invalid token") }, + (true, Some('o')) => if self.buf.starts_with("or") { self.con(2); Ok(Token::Or ) } else { Err("Invalid token") }, + (false,_) => Ok(Token::Expr), + _ => Err("Invalid token"), + }; + + self.hasexpr = match ret { Ok(Token::Close) | Ok(Token::Expr) => true, _ => false }; + ret + } + + fn parse(&mut self) -> Result { + // This is a simple shunting-yard implementation + let mut exp = Vec::new(); + let mut ops = vec![Token::Open]; // Only And, Or and Open + + if self.token()? != Token::Open { + return Err("Filter must start with an open parentheses"); + } + + while ops.len() > 0 { + match self.token()? { + Token::Expr => exp.push(self.token_expr()?), + + op@Token::Open => ops.push(op), + + Token::Close => { + while let Some(op) = ops.pop() { + if op == Token::Open { + break; + } else { + apply(&mut exp, op); + } + } + }, + + o1@Token::And | o1@Token::Or => { + while let Some(&o2) = ops.last() { + if o2 != Token::Open && (o1 != o2 && o1 == Token::Or) { + ops.pop(); + apply(&mut exp, o2); + } else { + break; + } + } + ops.push(o1); + }, + } + } + Ok(exp.pop().unwrap()) + } +} + + +fn apply(exp: &mut Vec, op: Token) { + let right = Box::new(exp.pop().unwrap()); + let left = Box::new(exp.pop().unwrap()); + exp.push(if op == Token::And { Filter::And(left, right) } else { Filter::Or(left, right) }); +} + + +fn parse_op(s: &str) -> Option { + if s.starts_with("=" ) { Some(Op::Eq ) } + else if s.starts_with("!=") { Some(Op::NEq) } + else if s.starts_with("<=") { Some(Op::LEq) } + else if s.starts_with("<" ) { Some(Op::Le ) } + else if s.starts_with(">=") { Some(Op::GEq) } + else if s.starts_with(">" ) { Some(Op::Gt ) } + else if s.starts_with("~" ) { Some(Op::Fuzzy) } + else { None } +} + + +pub fn parse_filter(s: &str) -> Result<(Filter, &str)> { + let mut p = FilterParser{buf: s, hasexpr: false}; + p.parse().map(|r| (r, p.buf)) +} + + +pub fn parse_arg(s: &str) -> Result<(Arg, &str)> { + let s = trim_ws(s); + + // This match on the first character can be replaced by simply trying parse_filter and + // parse_json in sequence; but that results in less than ideal error messages on badly + // formatted input. + match s.chars().next() { + None => return Err("Empty argument"), + + Some('(') => { + return parse_filter(s).map(|(v,r)| (Arg::Filter(v), r)); + }, + + Some('[') | Some('{') | Some('"') => { + return parse_json(s).map(|(v,r)| (Arg::Json(v), r)); + }, + + Some(_) => { + if let Ok((v,r)) = parse_json(s) { + return Ok((Arg::Json(v), r)); + + } else { + let mut splt = s.splitn(2, is_ws); + let v = splt.next().unwrap(); + let rem = splt.next().unwrap_or(""); + + if !v.contains(|c| !is_barestr(c)) { + return Ok((Arg::BareString(v.to_string()), rem)); + } else { + return Err("Invalid argument") + } + } + }, + } +} + + +pub fn parse_message(s: &str) -> Result { + let mut buf = trim_ws(s); + + let mut splt = buf.splitn(2, is_ws); + let name = splt.next().ok_or("Empty message")?; + let mut msg = Message::new(name)?; + + buf = trim_ws(splt.next().unwrap_or("")); + while buf.len() > 0 { + let v = parse_arg(buf)?; + msg = msg.push_arg(v.0); + buf = trim_ws(v.1); + } + + Ok(msg) +} + + +#[test] +fn test_parse_filter() { + let ok = |i, o| { + let s = format!("{}garbage", i); + let f = parse_filter(&s).unwrap(); + assert_eq!(&format!("{}", f.0), o); + assert_eq!(f.1, "garbage"); + }; + ok("(n=1)", "(n = 1)"); + ok("(something_else>=[1,\"str\"])", "(something_else >= [1,\"str\"])"); + ok("(((n=1) and blah=[]))", "((n = 1) and (blah = []))"); + ok("(((n=1) and blah=[] or x=\"hi\"))", "(((n = 1) and (blah = [])) or (x = \"hi\"))"); + ok("(a=1andb=2andc=3)", "((a = 1) and ((b = 2) and (c = 3)))"); + ok("(a=1orb=2andc=3)", "((a = 1) or ((b = 2) and (c = 3)))"); + ok("(a=1orb=2andc=3and(d=4ande=5orf=6)andg=7)", "((a = 1) or ((b = 2) and ((c = 3) and ((((d = 4) and (e = 5)) or (f = 6)) and (g = 7)))))"); + ok("(and=nulloror!=false)", "((and = null) or (or != false))"); + + let nok = |i| { assert!(parse_filter(i).is_err()) }; + nok("()"); + nok("(n=1 n=1)"); + nok("n=1"); + nok("(and)"); + nok("(n=1 and"); + nok("(n=1 and )"); + nok("(n=1 and and n=2)"); + nok(") and n=1"); +} -- cgit v1.2.3