From 9df7d57f69d8ead6c989846524f38227c9f07c65 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Tue, 8 Aug 2017 21:02:54 -0500 Subject: [PATCH] Add format string support. --- README.md | 9 ++++++ src/build.rs | 29 ++++++++++++++++-- src/format.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 ++ src/parse.rs | 49 ++++++++++++++++++++++++++++-- 5 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 src/format.rs diff --git a/README.md b/README.md index 94aecac..e9832bc 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,15 @@ concatenation using `+`. The expressions enforce the same type between operands. "foo" + "bar"; +### String formatting + +UCG supports some string interpolation using format strings. The syntax is +shamelessly ripped off from python. + + "foo @ @ \@" % (1, "bar") + +This gets turned into "foo 1 bar {" + ### Bindings and Tuples. Let statements introduce a new name in a UCG file. Most configurations diff --git a/src/build.rs b/src/build.rs index 66f2608..a41be50 100644 --- a/src/build.rs +++ b/src/build.rs @@ -16,7 +16,7 @@ use parse::{parse,Statement,Expression,Value,FieldList,SelectorList}; use std::fs::File; use std::io::Read; use std::error::Error; -use std::collections::{HashSet,HashMap, VecDeque}; +use std::collections::{HashSet,HashMap,VecDeque}; use std::collections::hash_map::Entry; use std::fmt; use std::fmt::{Display,Formatter}; @@ -25,6 +25,8 @@ use std::rc::Rc; use nom; +use format; + quick_error! { #[derive(Debug,PartialEq)] pub enum BuildError { @@ -52,6 +54,10 @@ quick_error! { description("Eval Error") display("Bad Argument Length {}", msg) } + FormatError(msg: String) { + description("String Format Error") + display("String format Error {}", msg) + } TODO(msg: String) { description("TODO Error") display("TODO Error {}", msg) @@ -112,7 +118,6 @@ pub enum Val { } impl Val { - pub fn type_name(&self) -> String { match self { &Val::Int(_) => "Integer".to_string(), @@ -177,6 +182,17 @@ impl Display for Val { } } +impl From for String { + fn from(v: Val) -> String { + match v { + Val::Int(ref i) => format!("{}", i), + Val::Float(ref f) => format!("{}", f), + Val::String(ref s) => s.to_string(), + val => format!("<{}>", val), + } + } +} + /// ValueMap defines a set of values in a parsed file. type ValueMap = HashMap>; @@ -480,6 +496,15 @@ impl Builder { Expression::Grouped(expr) => { return self.eval_expr(*expr); }, + Expression::Format(tmpl, mut args) => { + let mut vals = Vec::new(); + for v in args.drain(0..) { + let rcv = try!(self.eval_expr(v)); + vals.push(rcv.deref().clone()); + } + let formatter = format::Formatter::new(tmpl, vals); + Ok(Rc::new(Val::String(try!(formatter.render())))) + }, Expression::Call{macroref: sel, arglist: mut args} => { let v = try!(self.lookup_selector(sel)); if let &Val::Macro(ref m) = v.deref() { diff --git a/src/format.rs b/src/format.rs new file mode 100644 index 0000000..893b1ff --- /dev/null +++ b/src/format.rs @@ -0,0 +1,84 @@ +// Copyright 2017 Jeremy Wall +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::clone::Clone; +use std::error::Error; + +use build::BuildError; + +pub struct Formatter + Clone> { + tmpl: String, + args: Vec, +} + +impl + Clone> Formatter { + pub fn new>(tmpl: S, args: Vec) -> Self { + Formatter{ + tmpl: tmpl.into(), + args: args, + } + } + + pub fn render(&self) -> Result> { + let mut buf = String::new(); + let mut should_escape = false; + let mut count = 0; + for c in self.tmpl.chars() { + if c == '@' && !should_escape { + if count == self.args.len() { + return Err(Box::new( + BuildError::FormatError( + "Too few arguments to string formatter.".to_string()))) + } + let arg = self.args[count].clone(); + let strval = arg.into(); + buf.push_str(&strval); + count += 1; + } else if c == '\\' && !should_escape { + should_escape = true; + } else { + buf.push(c); + } + } + if self.args.len() != count { + return Err(Box::new( + BuildError::FormatError( + "Too many arguments to string formatter.".to_string()))) + } + return Ok(buf); + } +} + +#[cfg(test)] +mod test { + use super::Formatter; + + #[test] + fn test_format_happy_path() { + let formatter = Formatter::new("foo @ @ \\@", vec!["bar", "quux"]); + assert_eq!(formatter.render().unwrap(), "foo bar quux @"); + } + + #[test] + fn test_format_happy_wrong_too_few_args() { + let formatter = Formatter::new("foo @ @ \\@", vec!["bar"]); + assert!(formatter.render().is_err()); + } + + #[test] + fn test_format_happy_wrong_too_many_args() { + let formatter = Formatter::new("foo @ @ \\@", vec!["bar", "quux", "baz"]); + assert!(formatter.render().is_err()); + } +} diff --git a/src/lib.rs b/src/lib.rs index b44bd79..679cc99 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,8 @@ extern crate quick_error; pub mod parse; pub mod build; +mod format; + pub use parse::Value; pub use parse::Expression; pub use parse::Statement; diff --git a/src/parse.rs b/src/parse.rs index 96d3bf3..e2742e4 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -21,6 +21,9 @@ quick_error! { } } +// TODO(jwall): Convert to tokenizer steps followed by parser steps. +// TODO(jwall): Error Reporting with Line and Column information. + use std::str::FromStr; use std::str::from_utf8; use std::error::Error; @@ -97,14 +100,18 @@ pub enum Expression { Copy(SelectorList, FieldList), Grouped(Box), + Format(String, Vec), + Call { macroref: SelectorList, arglist: Vec, }, + Macro { arglist: Vec, tuple: FieldList, }, + Select { val: Box, default: Box, @@ -449,6 +456,24 @@ named!(select_expression, ) ); +fn tuple_to_format(t: (String, Vec)) -> ParseResult { + Ok(Expression::Format(t.0, t.1)) +} + +named!(format_expression, + map_res!( + do_parse!( + tmpl: ws!(quoted) >> + ws!(tag!("%")) >> + lparen >> + args: ws!(separated_list!(ws!(comma), expression)) >> + rparen >> + (tmpl, args) + ), + tuple_to_format + ) +); + fn tuple_to_call(t: (Value, Vec)) -> ParseResult { if let Value::Selector(sl) = t.0 { Ok(Expression::Call { @@ -502,6 +527,7 @@ named!(expression, complete!(div_expression) | complete!(grouped_expression) | complete!(macro_expression) | + complete!(format_expression) | complete!(select_expression) | complete!(call_expression) | complete!(copy_expression) | @@ -576,8 +602,9 @@ named!(pub parse >, many1!(ws!(statement))); mod test { use std::str::from_utf8; use super::{Statement, Expression, Value}; - use super::{number, symbol, parse, field_value, tuple, grouped_expression, copy_expression}; - use super::{arglist, macro_expression, select_expression, call_expression, expression}; + use super::{number, symbol, parse, field_value, tuple, grouped_expression}; + use super::{arglist, copy_expression, macro_expression, select_expression}; + use super::{format_expression, call_expression, expression}; use super::{expression_statement, let_statement, import_statement, statement}; use nom::IResult; @@ -774,6 +801,22 @@ mod test { ); } + #[test] + fn test_format_parse() { + assert!(format_expression(&b"\"foo"[..]).is_err() ); + assert!(format_expression(&b"\"foo\""[..]).is_incomplete() ); + assert!(format_expression(&b"\"foo\" %"[..]).is_incomplete() ); + assert!(format_expression(&b"\"foo\" % (1, 2"[..]).is_incomplete() ); + + assert_eq!(format_expression(&b"\"foo @ @\" % (1, 2)"[..]), + IResult::Done(&b""[..], + Expression::Format("foo @ @".to_string(), + vec![Expression::Simple(Value::Int(1)), + Expression::Simple(Value::Int(2))]) + ) + ); + } + #[test] fn test_call_parse() { assert!(call_expression(&b"foo"[..]).is_incomplete() ); @@ -792,7 +835,7 @@ mod test { ], } ) - ); + ); assert_eq!(call_expression(&b"foo.bar (1, \"foo\")"[..]), IResult::Done(&b""[..],