Add format string support.

This commit is contained in:
Jeremy Wall 2017-08-08 21:02:54 -05:00
parent 200db01b13
commit 9df7d57f69
5 changed files with 168 additions and 5 deletions

View File

@ -47,6 +47,15 @@ concatenation using `+`. The expressions enforce the same type between operands.
"foo" + "bar"; "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. ### Bindings and Tuples.
Let statements introduce a new name in a UCG file. Most configurations Let statements introduce a new name in a UCG file. Most configurations

View File

@ -16,7 +16,7 @@ use parse::{parse,Statement,Expression,Value,FieldList,SelectorList};
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::error::Error; use std::error::Error;
use std::collections::{HashSet,HashMap, VecDeque}; use std::collections::{HashSet,HashMap,VecDeque};
use std::collections::hash_map::Entry; use std::collections::hash_map::Entry;
use std::fmt; use std::fmt;
use std::fmt::{Display,Formatter}; use std::fmt::{Display,Formatter};
@ -25,6 +25,8 @@ use std::rc::Rc;
use nom; use nom;
use format;
quick_error! { quick_error! {
#[derive(Debug,PartialEq)] #[derive(Debug,PartialEq)]
pub enum BuildError { pub enum BuildError {
@ -52,6 +54,10 @@ quick_error! {
description("Eval Error") description("Eval Error")
display("Bad Argument Length {}", msg) display("Bad Argument Length {}", msg)
} }
FormatError(msg: String) {
description("String Format Error")
display("String format Error {}", msg)
}
TODO(msg: String) { TODO(msg: String) {
description("TODO Error") description("TODO Error")
display("TODO Error {}", msg) display("TODO Error {}", msg)
@ -112,7 +118,6 @@ pub enum Val {
} }
impl Val { impl Val {
pub fn type_name(&self) -> String { pub fn type_name(&self) -> String {
match self { match self {
&Val::Int(_) => "Integer".to_string(), &Val::Int(_) => "Integer".to_string(),
@ -177,6 +182,17 @@ impl Display for Val {
} }
} }
impl From<Val> 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. /// ValueMap defines a set of values in a parsed file.
type ValueMap = HashMap<String, Rc<Val>>; type ValueMap = HashMap<String, Rc<Val>>;
@ -480,6 +496,15 @@ impl Builder {
Expression::Grouped(expr) => { Expression::Grouped(expr) => {
return self.eval_expr(*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} => { Expression::Call{macroref: sel, arglist: mut args} => {
let v = try!(self.lookup_selector(sel)); let v = try!(self.lookup_selector(sel));
if let &Val::Macro(ref m) = v.deref() { if let &Val::Macro(ref m) = v.deref() {

84
src/format.rs Normal file
View File

@ -0,0 +1,84 @@
// Copyright 2017 Jeremy Wall <jeremy@marzhillstudios.com>
//
// 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<V: Into<String> + Clone> {
tmpl: String,
args: Vec<V>,
}
impl<V: Into<String> + Clone> Formatter<V> {
pub fn new<S: Into<String>>(tmpl: S, args: Vec<V>) -> Self {
Formatter{
tmpl: tmpl.into(),
args: args,
}
}
pub fn render(&self) -> Result<String, Box<Error>> {
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());
}
}

View File

@ -20,6 +20,8 @@ extern crate quick_error;
pub mod parse; pub mod parse;
pub mod build; pub mod build;
mod format;
pub use parse::Value; pub use parse::Value;
pub use parse::Expression; pub use parse::Expression;
pub use parse::Statement; pub use parse::Statement;

View File

@ -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::FromStr;
use std::str::from_utf8; use std::str::from_utf8;
use std::error::Error; use std::error::Error;
@ -97,14 +100,18 @@ pub enum Expression {
Copy(SelectorList, FieldList), Copy(SelectorList, FieldList),
Grouped(Box<Expression>), Grouped(Box<Expression>),
Format(String, Vec<Expression>),
Call { Call {
macroref: SelectorList, macroref: SelectorList,
arglist: Vec<Expression>, arglist: Vec<Expression>,
}, },
Macro { Macro {
arglist: Vec<String>, arglist: Vec<String>,
tuple: FieldList, tuple: FieldList,
}, },
Select { Select {
val: Box<Expression>, val: Box<Expression>,
default: Box<Expression>, default: Box<Expression>,
@ -449,6 +456,24 @@ named!(select_expression<Expression>,
) )
); );
fn tuple_to_format(t: (String, Vec<Expression>)) -> ParseResult<Expression> {
Ok(Expression::Format(t.0, t.1))
}
named!(format_expression<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<Expression>)) -> ParseResult<Expression> { fn tuple_to_call(t: (Value, Vec<Expression>)) -> ParseResult<Expression> {
if let Value::Selector(sl) = t.0 { if let Value::Selector(sl) = t.0 {
Ok(Expression::Call { Ok(Expression::Call {
@ -502,6 +527,7 @@ named!(expression<Expression>,
complete!(div_expression) | complete!(div_expression) |
complete!(grouped_expression) | complete!(grouped_expression) |
complete!(macro_expression) | complete!(macro_expression) |
complete!(format_expression) |
complete!(select_expression) | complete!(select_expression) |
complete!(call_expression) | complete!(call_expression) |
complete!(copy_expression) | complete!(copy_expression) |
@ -576,8 +602,9 @@ named!(pub parse<Vec<Statement> >, many1!(ws!(statement)));
mod test { mod test {
use std::str::from_utf8; use std::str::from_utf8;
use super::{Statement, Expression, Value}; use super::{Statement, Expression, Value};
use super::{number, symbol, parse, field_value, tuple, grouped_expression, copy_expression}; use super::{number, symbol, parse, field_value, tuple, grouped_expression};
use super::{arglist, macro_expression, select_expression, call_expression, 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 super::{expression_statement, let_statement, import_statement, statement};
use nom::IResult; 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] #[test]
fn test_call_parse() { fn test_call_parse() {
assert!(call_expression(&b"foo"[..]).is_incomplete() ); assert!(call_expression(&b"foo"[..]).is_incomplete() );
@ -792,7 +835,7 @@ mod test {
], ],
} }
) )
); );
assert_eq!(call_expression(&b"foo.bar (1, \"foo\")"[..]), assert_eq!(call_expression(&b"foo.bar (1, \"foo\")"[..]),
IResult::Done(&b""[..], IResult::Done(&b""[..],