FEATURE: NULL or empty values in the type system.

* Flags with empty values are considered not having a value they are set to.
* Json outputs empty values as null.
* Env ignores variable with empty values.
This commit is contained in:
Jeremy Wall 2018-03-12 20:29:31 -05:00
parent 5a03cc33ef
commit 8a6935c3da
7 changed files with 90 additions and 16 deletions

View File

@ -86,6 +86,7 @@ impl Position {
/// Defines the types of tokens in UCG syntax. /// Defines the types of tokens in UCG syntax.
#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord, Hash)] #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord, Hash)]
pub enum TokenType { pub enum TokenType {
EMPTY,
END, END,
WS, WS,
COMMENT, COMMENT,
@ -328,6 +329,7 @@ impl SelectorDef {
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub enum Value { pub enum Value {
// Constant Values // Constant Values
Empty(Position),
Int(Positioned<i64>), Int(Positioned<i64>),
Float(Positioned<f64>), Float(Positioned<f64>),
String(Positioned<String>), String(Positioned<String>),
@ -342,6 +344,7 @@ impl Value {
/// Returns the type name of the Value it is called on as a string. /// Returns the type name of the Value it is called on as a string.
pub fn type_name(&self) -> String { pub fn type_name(&self) -> String {
match self { match self {
&Value::Empty(_) => "EmptyValue".to_string(),
&Value::Int(_) => "Integer".to_string(), &Value::Int(_) => "Integer".to_string(),
&Value::Float(_) => "Float".to_string(), &Value::Float(_) => "Float".to_string(),
&Value::String(_) => "String".to_string(), &Value::String(_) => "String".to_string(),
@ -371,6 +374,7 @@ impl Value {
/// Returns a stringified version of the Value. /// Returns a stringified version of the Value.
pub fn to_string(&self) -> String { pub fn to_string(&self) -> String {
match self { match self {
&Value::Empty(_) => "EmptyValue".to_string(),
&Value::Int(ref i) => format!("{}", i.val), &Value::Int(ref i) => format!("{}", i.val),
&Value::Float(ref f) => format!("{}", f.val), &Value::Float(ref f) => format!("{}", f.val),
&Value::String(ref s) => format!("{}", s.val), &Value::String(ref s) => format!("{}", s.val),
@ -384,6 +388,7 @@ impl Value {
/// Returns the position for a Value. /// Returns the position for a Value.
pub fn pos(&self) -> &Position { pub fn pos(&self) -> &Position {
match self { match self {
&Value::Empty(ref pos) => pos,
&Value::Int(ref i) => &i.pos, &Value::Int(ref i) => &i.pos,
&Value::Float(ref f) => &f.pos, &Value::Float(ref f) => &f.pos,
&Value::String(ref s) => &s.pos, &Value::String(ref s) => &s.pos,
@ -399,6 +404,7 @@ impl Value {
enum_type_equality!( enum_type_equality!(
self, self,
target, target,
&Value::Empty(_),
&Value::Int(_), &Value::Int(_),
&Value::Float(_), &Value::Float(_),
&Value::String(_), &Value::String(_),

View File

@ -72,6 +72,7 @@ type BuildResult = Result<(), Box<Error>>;
/// The Intermediate representation of a compiled UCG AST. /// The Intermediate representation of a compiled UCG AST.
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
pub enum Val { pub enum Val {
Empty,
Int(i64), Int(i64),
Float(f64), Float(f64),
String(String), String(String),
@ -84,6 +85,7 @@ impl Val {
/// Returns the Type of a Val as a string. /// Returns the Type of a Val as a string.
pub fn type_name(&self) -> String { pub fn type_name(&self) -> String {
match self { match self {
&Val::Empty => "EmptyValue".to_string(),
&Val::Int(_) => "Integer".to_string(), &Val::Int(_) => "Integer".to_string(),
&Val::Float(_) => "Float".to_string(), &Val::Float(_) => "Float".to_string(),
&Val::String(_) => "String".to_string(), &Val::String(_) => "String".to_string(),
@ -98,6 +100,7 @@ impl Val {
enum_type_equality!( enum_type_equality!(
self, self,
target, target,
&Val::Empty,
&Val::Int(_), &Val::Int(_),
&Val::Float(_), &Val::Float(_),
&Val::String(_), &Val::String(_),
@ -155,6 +158,7 @@ impl Val {
impl Display for Val { impl Display for Val {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self { match self {
&Val::Empty => write!(f, "EmptyValue"),
&Val::Float(ref ff) => write!(f, "Float({})", ff), &Val::Float(ref ff) => write!(f, "Float({})", ff),
&Val::Int(ref i) => write!(f, "Int({})", i), &Val::Int(ref i) => write!(f, "Int({})", i),
&Val::String(ref s) => write!(f, "String({})", s), &Val::String(ref s) => write!(f, "String({})", s),
@ -248,6 +252,7 @@ impl Builder {
fn value_to_val(&self, v: &Value) -> Result<Rc<Val>, Box<Error>> { fn value_to_val(&self, v: &Value) -> Result<Rc<Val>, Box<Error>> {
match v { match v {
&Value::Empty(_) => Ok(Rc::new(Val::Empty)),
&Value::Int(ref i) => Ok(Rc::new(Val::Int(i.val))), &Value::Int(ref i) => Ok(Rc::new(Val::Int(i.val))),
&Value::Float(ref f) => Ok(Rc::new(Val::Float(f.val))), &Value::Float(ref f) => Ok(Rc::new(Val::Float(f.val))),
&Value::String(ref s) => Ok(Rc::new(Val::String(s.val.to_string()))), &Value::String(ref s) => Ok(Rc::new(Val::String(s.val.to_string()))),

View File

@ -40,6 +40,10 @@ impl EnvConverter {
eprintln!("Skipping embedded tuple..."); eprintln!("Skipping embedded tuple...");
return Ok(()); return Ok(());
} }
if let &Val::Empty = val.as_ref() {
eprintln!("Skipping empty variable: {}", name);
return Ok(());
}
try!(write!(w, "{}=", name.val)); try!(write!(w, "{}=", name.val));
try!(self.write(&val, w)); try!(self.write(&val, w));
} }
@ -55,6 +59,10 @@ impl EnvConverter {
fn write(&self, v: &Val, w: &mut Write) -> Result<()> { fn write(&self, v: &Val, w: &mut Write) -> Result<()> {
match v { match v {
&Val::Empty => {
// Empty is a noop.
return Ok(());
}
&Val::Float(ref f) => { &Val::Float(ref f) => {
try!(write!(w, "{} ", f)); try!(write!(w, "{} ", f));
} }

View File

@ -28,8 +28,21 @@ impl FlagConverter {
FlagConverter {} FlagConverter {}
} }
fn write_flag_name(&self, pfx: &str, name: &str, w: &mut Write) -> Result<()> {
if name.chars().count() > 1 || pfx.chars().count() > 0 {
try!(write!(w, "--{}{} ", pfx, name));
} else {
try!(write!(w, "-{} ", name));
}
return Ok(());
}
fn write(&self, pfx: &str, v: &Val, w: &mut Write) -> Result<()> { fn write(&self, pfx: &str, v: &Val, w: &mut Write) -> Result<()> {
match v { match v {
&Val::Empty => {
// Empty is a noop.
return Ok(());
}
&Val::Float(ref f) => { &Val::Float(ref f) => {
try!(write!(w, "{} ", f)); try!(write!(w, "{} ", f));
} }
@ -45,15 +58,15 @@ impl FlagConverter {
} }
&Val::Tuple(ref flds) => { &Val::Tuple(ref flds) => {
for &(ref name, ref val) in flds.iter() { for &(ref name, ref val) in flds.iter() {
if let &Val::Empty = val.as_ref() {
try!(self.write_flag_name(pfx, &name.val, w));
continue;
}
if val.is_tuple() { if val.is_tuple() {
let new_pfx = format!("{}{}.", pfx, name); let new_pfx = format!("{}{}.", pfx, name);
try!(self.write(&new_pfx, val, w)); try!(self.write(&new_pfx, val, w));
} else { } else {
if name.val.chars().count() > 1 || pfx.chars().count() > 0 { try!(self.write_flag_name(pfx, &name.val, w));
try!(write!(w, "--{}{} ", pfx, name.val));
} else {
try!(write!(w, "-{} ", name.val));
}
// TODO(jwall): What if the value is a tuple? // TODO(jwall): What if the value is a tuple?
try!(self.write(pfx, &val, w)); try!(self.write(pfx, &val, w));
} }

View File

@ -48,6 +48,7 @@ impl JsonConverter {
fn convert_value(&self, v: &Val) -> Result<serde_json::Value> { fn convert_value(&self, v: &Val) -> Result<serde_json::Value> {
let jsn_val = match v { let jsn_val = match v {
&Val::Empty => serde_json::Value::Null,
&Val::Float(f) => { &Val::Float(f) => {
let n = match serde_json::Number::from_f64(f) { let n = match serde_json::Number::from_f64(f) {
Some(n) => n, Some(n) => n,

View File

@ -195,8 +195,17 @@ named!(list_value<TokenIter, Value, ParseError>,
) )
); );
named!(empty_value<TokenIter, Value, ParseError>,
do_parse!(
pos: pos >>
match_type!(EMPTY) >>
(Value::Empty(pos))
)
);
named!(value<TokenIter, Value, ParseError>, named!(value<TokenIter, Value, ParseError>,
alt!( alt!(
empty_value |
number | number |
quoted_value | quoted_value |
list_value | list_value |
@ -355,17 +364,17 @@ fn tuple_to_copy(t: (SelectorDef, FieldList)) -> ParseResult<Expression> {
} }
named!(copy_expression<TokenIter, Expression, ParseError>, named!(copy_expression<TokenIter, Expression, ParseError>,
map_res!( map_res!(
do_parse!( do_parse!(
pos: pos >> pos: pos >>
selector: selector_list >> selector: selector_list >>
punct!("{") >> punct!("{") >>
fields: field_list >> fields: field_list >>
punct!("}") >> punct!("}") >>
(SelectorDef::new(selector, pos.line, pos.column as usize), fields) (SelectorDef::new(selector, pos.line, pos.column as usize), fields)
), ),
tuple_to_copy tuple_to_copy
) )
); );
fn tuple_to_macro(mut t: (Position, Vec<Value>, Value)) -> ParseResult<Expression> { fn tuple_to_macro(mut t: (Position, Vec<Value>, Value)) -> ParseResult<Expression> {
@ -702,6 +711,11 @@ mod test {
} }
} }
#[test]
fn test_null_parsing() {
assert_parse!(empty_value("NULL"), Value::Empty(Position::new(1, 1)));
}
#[test] #[test]
fn test_symbol_parsing() { fn test_symbol_parsing() {
assert_parse!( assert_parse!(
@ -924,6 +938,10 @@ mod test {
#[test] #[test]
fn test_expression_parse() { fn test_expression_parse() {
assert_parse!(
expression("NULL"),
Expression::Simple(Value::Empty(Position::new(1, 1)))
);
assert_parse!( assert_parse!(
expression("\"foo\""), expression("\"foo\""),
Expression::Simple(Value::String(value_node!("foo".to_string(), 1, 1))) Expression::Simple(Value::String(value_node!("foo".to_string(), 1, 1)))

View File

@ -117,6 +117,10 @@ macro_rules! do_tag_tok {
} }
} }
named!(emptytok( Span ) -> Token,
do_tag_tok!(TokenType::EMPTY, "NULL")
);
named!(commatok( Span ) -> Token, named!(commatok( Span ) -> Token,
do_tag_tok!(TokenType::PUNCT, ",") do_tag_tok!(TokenType::PUNCT, ",")
); );
@ -268,6 +272,7 @@ named!(whitespace( Span ) -> Token,
named!(token( Span ) -> Token, named!(token( Span ) -> Token,
alt!( alt!(
strtok | strtok |
emptytok | // This must come before the barewordtok
barewordtok | barewordtok |
digittok | digittok |
commatok | commatok |
@ -358,6 +363,14 @@ macro_rules! match_type {
match_type!($i, BAREWORD => token_clone) match_type!($i, BAREWORD => token_clone)
}; };
($i:expr, EMPTY => $h:expr) => {
match_type!($i, TokenType::EMPTY, "Not NULL", $h)
};
($i:expr, EMPTY) => {
match_type!($i, EMPTY => token_clone)
};
($i:expr, STR => $h:expr) => { ($i:expr, STR => $h:expr) => {
match_type!($i, TokenType::QUOTED, "Not a String", $h) match_type!($i, TokenType::QUOTED, "Not a String", $h)
}; };
@ -571,6 +584,16 @@ mod tokenizer_test {
use nom; use nom;
use nom_locate::LocatedSpan; use nom_locate::LocatedSpan;
#[test]
fn test_empty_token() {
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");
assert_eq!(tok.typ, TokenType::EMPTY);
}
}
#[test] #[test]
fn test_escape_quoted() { fn test_escape_quoted() {
let result = escapequoted(LocatedSpan::new("foo \\\"bar\"")); let result = escapequoted(LocatedSpan::new("foo \\\"bar\""));