From 9674d6006f2ca35588826c26105aba3460ae3a2c Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 30 May 2018 23:00:50 -0500 Subject: [PATCH] FEATURE: Parsing support for assert statements. --- src/ast/mod.rs | 3 +++ src/build/mod.rs | 16 ++++++++++++++++ src/lib.rs | 17 ++++++++++++++++- src/parse/mod.rs | 15 +++++++++++++++ src/parse/test.rs | 4 ++-- src/tokenizer/mod.rs | 15 ++++++++++----- src/tokenizer/test.rs | 18 ++++++++++++++---- 7 files changed, 76 insertions(+), 12 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 351c985..e8e5585 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -792,6 +792,9 @@ pub enum Statement { // Import a file. Import(ImportDef), + + // Assert statement + Assert(Expression), } #[cfg(test)] diff --git a/src/build/mod.rs b/src/build/mod.rs index 0705ad2..c258bbc 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -505,6 +505,7 @@ impl Builder { fn build_stmt(&mut self, stmt: &Statement) -> Result, Box> { match stmt { + &Statement::Assert(ref expr) => self.build_assert(expr), &Statement::Let(ref def) => self.build_let(def), &Statement::Import(ref def) => self.build_import(def), &Statement::Expression(ref expr) => self.eval_expr(expr), @@ -1110,6 +1111,21 @@ impl Builder { ))); } + fn build_assert(&self, expr: &Expression) -> Result, Box> { + let ok = try!(self.eval_expr(expr)); + if let &Val::Boolean(b) = ok.as_ref() { + // record the assertion result. + if b { + // success! + } else { + // failure! + } + } else { + // record an assertion type-failure result. + } + Ok(ok) + } + // Evals a single Expression in the context of a running Builder. // It does not mutate the builders collected state at all. pub fn eval_expr(&self, expr: &Expression) -> Result, Box> { diff --git a/src/lib.rs b/src/lib.rs index cbd1ad9..c4dc70f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,7 @@ //! //! The following words are reserved in ucg and can't be used as named bindings. //! +//! * assert //! * true //! * false //! * let @@ -371,7 +372,7 @@ //! //! * Let statements //! -//! The let expression binds the result of any valid expression to a name. It starts with the `let` keyword and is followed by +//! The let statement binds the result of any valid expression to a name. It starts with the `let` keyword and is followed by //! the name of the binding, an `=`, and a valid ucg expression. //! //! ```ucg @@ -389,6 +390,20 @@ //! //! let mysqlconf = dbconfigs.mysql; //! ``` +//! +//! * Assert statement +//! +//! The assert statement defines an expression that must evaluate to either true or false. Assert statements are noops except +//! during a validation compile. They give you a way to assert certains properties about your data and can be used as a form +//! of unit testting for your configurations. It starts with the assert keyword followed by a valid boolean ucg expression. +//! +//! ```ucg +//! assert host == "www.example.com"; +//! assert select qa, 443, { +//! qa = 80, +//! prod = 443, +//! } == 443; +//! ``` // The following is necessary to allow the macros in tokenizer and parse modules // to succeed. diff --git a/src/parse/mod.rs b/src/parse/mod.rs index f6428e8..be31a43 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -896,9 +896,24 @@ named!(import_statement, ) ); +named!(assert_statement, + do_parse!( + word!("assert") >> + pos: pos >> + expr: add_return_error!( + nom::ErrorKind::Custom( + error::Error::new( + "Invalid syntax for assert", + error::ErrorType::ParseError, pos)), + expression) >> + (Statement::Assert(expr)) + ) +); + //trace_macros!(true); fn statement(i: TokenIter) -> nom::IResult { return alt_peek!(i, + word!("assert") => assert_statement | word!("import") => import_statement | word!("let") => let_statement | expression_statement); diff --git a/src/parse/test.rs b/src/parse/test.rs index a24a986..2a67564 100644 --- a/src/parse/test.rs +++ b/src/parse/test.rs @@ -42,7 +42,7 @@ macro_rules! assert_error { #[test] fn test_null_parsing() { - assert_parse!(empty_value("NULL"), Value::Empty(Position::new(1, 1))); + assert_parse!(empty_value("NULL "), Value::Empty(Position::new(1, 1))); } #[test] @@ -337,7 +337,7 @@ fn test_expression_statement_parse() { #[test] fn test_expression_parse() { assert_parse!( - expression("NULL"), + expression("NULL "), Expression::Simple(Value::Empty(Position::new(1, 1))) ); assert_parse!( diff --git a/src/tokenizer/mod.rs b/src/tokenizer/mod.rs index f3cdc43..664783c 100644 --- a/src/tokenizer/mod.rs +++ b/src/tokenizer/mod.rs @@ -142,7 +142,7 @@ macro_rules! do_tag_tok { } named!(emptytok( Span ) -> Token, - do_tag_tok!(TokenType::EMPTY, "NULL") + do_tag_tok!(TokenType::EMPTY, "NULL", WS) ); named!(commatok( Span ) -> Token, @@ -233,10 +233,6 @@ named!(fatcommatok( Span ) -> Token, do_tag_tok!(TokenType::PUNCT, "=>") ); -named!(lettok( Span ) -> Token, - do_tag_tok!(TokenType::BAREWORD, "let", WS) -); - named!(selecttok( Span ) -> Token, do_tag_tok!(TokenType::BAREWORD, "select", WS) ); @@ -245,10 +241,18 @@ named!(macrotok( Span ) -> Token, do_tag_tok!(TokenType::BAREWORD, "macro", WS) ); +named!(lettok( Span ) -> Token, + do_tag_tok!(TokenType::BAREWORD, "let", WS) +); + named!(importtok( Span ) -> Token, do_tag_tok!(TokenType::BAREWORD, "import", WS) ); +named!(asserttok( Span ) -> Token, + do_tag_tok!(TokenType::BAREWORD, "assert", WS) +); + named!(astok( Span ) -> Token, do_tag_tok!(TokenType::BAREWORD, "as", WS) ); @@ -356,6 +360,7 @@ named!(token( Span ) -> Token, booleantok | lettok | selecttok | + asserttok | macrotok | importtok | astok | diff --git a/src/tokenizer/test.rs b/src/tokenizer/test.rs index f3a007e..540fdfa 100644 --- a/src/tokenizer/test.rs +++ b/src/tokenizer/test.rs @@ -4,7 +4,7 @@ use nom_locate::LocatedSpan; #[test] fn test_empty_token() { - let result = emptytok(LocatedSpan::new("NULL")); + let result = emptytok(LocatedSpan::new("NULL ")); assert!(result.is_done(), format!("result {:?} is not done", result)); if let nom::IResult::Done(_, tok) = result { assert_eq!(tok.fragment, "NULL"); @@ -12,6 +12,16 @@ fn test_empty_token() { } } +#[test] +fn test_assert_token() { + let result = asserttok(LocatedSpan::new("assert ")); + assert!(result.is_done(), format!("result {:?} is not done", result)); + if let nom::IResult::Done(_, tok) = result { + assert_eq!(tok.fragment, "assert"); + assert_eq!(tok.typ, TokenType::BAREWORD); + } +} + #[test] fn test_escape_quoted() { let result = escapequoted(LocatedSpan::new("foo \\\"bar\"")); @@ -93,7 +103,7 @@ fn test_lteqtok() { #[test] fn test_tokenize_one_of_each() { let result = tokenize(LocatedSpan::new( - "let import macro select as => [ ] { } ; = % / * \ + "map filter assert let import macro select as => [ ] { } ; = % / * \ + - . ( ) , 1 . foo \"bar\" // comment\n ; true false == < > <= >= !=", )); assert!(result.is_ok(), format!("result {:?} is not ok", result)); @@ -101,8 +111,8 @@ fn test_tokenize_one_of_each() { for (i, t) in v.iter().enumerate() { println!("{}: {:?}", i, t); } - assert_eq!(v.len(), 35); - assert_eq!(v[34].typ, TokenType::END); + assert_eq!(v.len(), 38); + assert_eq!(v[37].typ, TokenType::END); } #[test]