diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d247577 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,24 @@ +language: rust +branches: + only: + - master +matrix: + include: + - name: "stable rust" + rust: stable + before_script: + - rustup component add rustfmt + script: + - cargo fmt -- --check + - cargo build + - cargo test + - name: "nightly rust + rustfmt" + rust: nightly + before_script: + - rustup component add rustfmt --toolchain nightly-x86_64-unknown-linux-gnu + script: + - cargo fmt -- --check + - cargo build + - cargo test + allow_failures: + - name: "stable rust" diff --git a/Cargo.toml b/Cargo.toml index 4979f62..892b763 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,20 @@ name = "molysite" version = "0.1.0" edition = "2018" -[dependencies] -nom = "^3.2" +[dependencies.nom] +version = "^4.2" + +[dependencies.serde_json] +optional = true +version = "1.0.9" + +[dev-dependencies] +serde_derive = "1.0.27" + +[dev-dependencies.serde] +version = "1.0.27" + +[features] +arraynested = [] +withserde = ["serde_json"] +default = ["arraynested"] diff --git a/examples/hcl2json.rs b/examples/hcl2json.rs index a54d459..1961701 100644 --- a/examples/hcl2json.rs +++ b/examples/hcl2json.rs @@ -18,16 +18,14 @@ fn main() { let mut file = match File::open(&path) { // The `description` method of `io::Error` returns a string that // describes the error - Err(why) => panic!("couldn't open {}: {}", display, - why.description()), + Err(why) => panic!("couldn't open {}: {}", display, why.description()), Ok(file) => file, }; // Read the file contents into a string, returns `io::Result` let mut s = String::new(); match file.read_to_string(&mut s) { - Err(why) => panic!("couldn't read {}: {}", display, - why.description()), + Err(why) => panic!("couldn't read {}: {}", display, why.description()), Ok(_) => { let parsed = parse_hcl(&s); print!("{}\n", parsed.unwrap()); diff --git a/src/common.rs b/src/common.rs index cbcb5d2..94d23f6 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,17 +1,40 @@ use std::num::ParseIntError; -use std::str::{self, FromStr}; use std::u32; +use nom; +use nom::types::CompleteStr; use nom::{digit, hex_digit}; -named!(pub boolean, - map!( - alt_complete!(tag!("true") | tag!("false")), - |value: &[u8]| value == b"true" - ) +#[macro_export] +macro_rules! complete_named ( + ($name:ident, $submac:ident!( $($args:tt)* )) => ( + fn $name<'a>( i: CompleteStr<'a> ) -> nom::IResult, CompleteStr<'a>, u32> { + $submac!(i, $($args)*) + } + ); + ($name:ident<$o:ty>, $submac:ident!( $($args:tt)* )) => ( + fn $name<'a>( i: CompleteStr<'a> ) -> nom::IResult, $o, u32> { + $submac!(i, $($args)*) + } + ); + (pub $name:ident, $submac:ident!( $($args:tt)* )) => ( + pub fn $name<'a>( i: CompleteStr<'a> ) -> nom::IResult, CompleteStr<'a>, u32> { + $submac!(i, $($args)*) + } + ); + (pub $name:ident<$o:ty>, $submac:ident!( $($args:tt)* )) => ( + pub fn $name<'a>( i: CompleteStr<'a> ) -> nom::IResult, $o, u32> { + $submac!(i, $($args)*) + } + ); ); -named!( +complete_named!(pub boolean,map!( + alt!(tag!("true") | tag!("false")), + |value: CompleteStr| value == CompleteStr("true") +)); + +complete_named!( unsigned_float, recognize!(alt_complete!( delimited!(digit, tag!("."), opt!(complete!(digit))) @@ -20,35 +43,29 @@ named!( )) ); -named!(pub float, map_res!( - map_res!( - recognize!(alt_complete!( - delimited!( - pair!(opt!(alt!(tag!("+") | tag!("-"))), unsigned_float), - tag!("e"), - pair!(opt!(alt!(tag!("+") | tag!("-"))), unsigned_float) - ) | - unsigned_float - )), - str::from_utf8 - ), - FromStr::from_str +complete_named!(pub float, flat_map!( + recognize!(alt_complete!( + delimited!( + pair!(opt!(alt!(tag!("+") | tag!("-"))), unsigned_float), + tag!("e"), + pair!(opt!(alt!(tag!("+") | tag!("-"))), unsigned_float) + ) | + unsigned_float + )), + parse_to!(f32) )); -fn to_i(i: &str) -> Result { - u32::from_str_radix(i, 16) +fn complete_to_i(i: CompleteStr) -> Result { + u32::from_str_radix(i.0, 16) } -named!(pub int, map_res!( - map_res!( - preceded!(tag!("0x"), hex_digit), - str::from_utf8 - ), - to_i -)); +complete_named!( + int, + map_res!(preceded!(tag!("0x"), hex_digit), complete_to_i) +); // TODO: add support for octal -named!(pub number, alt_complete!( +complete_named!(pub number, alt_complete!( map!(int, |i| { i as f32 }) | float )); diff --git a/src/hcl.rs b/src/hcl.rs index ef286cd..4e9c7f2 100644 --- a/src/hcl.rs +++ b/src/hcl.rs @@ -1,32 +1,29 @@ -use std::collections::HashMap; use std::str; use std::string::String; -use nom::IResult::Done; -use nom::{alphanumeric, eol, multispace, not_line_ending}; +use nom; +use nom::types::CompleteStr; +use nom::{alphanumeric, eol, multispace, not_line_ending, ExtendInto}; use crate::common::{boolean, number}; -use crate::types::{JsonValue, ParseError}; +use crate::types::{Map, Number, ParseError, Value}; -pub fn parse_hcl(config: &str) -> Result { - match hcl(&config.as_bytes()[..]) { - Done(_, c) => Ok(c), +pub fn parse_hcl(config: &str) -> Result { + match hcl(CompleteStr(config)) { + Ok((_, c)) => Ok(c), _ => Err(0), } } -named!(hcl, map!(hcl_top, |h| JsonValue::Object(h))); +complete_named!(hcl, map!(hcl_top, |h| Value::Object(h))); -named!(end_of_line, alt!(eof!() | eol)); +complete_named!(end_of_line, alt!(eof!() | eol)); -fn to_s(i: Vec) -> String { - String::from_utf8_lossy(&i).into_owned() -} fn slen(i: String) -> usize { i.len() } -fn ulen(i: &[u8]) -> usize { - i.len() +fn cslen(i: CompleteStr) -> usize { + i.0.len() } fn take_limited(min: usize, max: usize) -> usize { if max < min { @@ -35,42 +32,39 @@ fn take_limited(min: usize, max: usize) -> usize { return min; } -named!( +complete_named!( hcl_escaped_string, - map!( - escaped_transform!( - is_not!("\\\"\n"), - '\\', - alt!( - tag!("\\") => { |_| &b"\\"[..] } | - tag!("\"") => { |_| &b"\""[..] } | - tag!("n") => { |_| &b"\n"[..] } - ) - ), - to_s + escaped_transform!( + is_not!("\\\"\n"), + '\\', + alt!( + tag!("\\") => { |_| "\\" } | + tag!("\"") => { |_| "\"" } | + tag!("n") => { |_| "\n" } + ) ) ); -named!( +complete_named!( hcl_template_string, map!( do_parse!(tag!("${") >> s: take_until_and_consume!("}") >> (s)), - |s| format!("${{{}}}", String::from_utf8_lossy(s)) + |s: CompleteStr| { format!("${{{}}}", s.0) } ) ); -named!( +complete_named!( hcl_quoted_escaped_string, delimited!( tag!("\""), map!( fold_many0!( - alt_complete!( + alt!( hcl_template_string | flat_map!( do_parse!( max: map!(peek!(hcl_escaped_string), slen) - >> min: map!(peek!(take_until!("${")), ulen) + >> min: map!(peek!(take_until!("${")), cslen) >> buf: take!(take_limited(min, max)) >> (buf) ), @@ -84,28 +78,26 @@ named!( acc } ), - |s| s.join("") + |s| { s.join("") } ), tag!("\"") ) ); -named!( +complete_named!( hcl_multiline_string, map!( do_parse!( - delimiter: tag!("<<") + tag!("<<") >> indent: opt!(tag!("-")) >> delimiter: terminated!(alphanumeric, eol) - >> delimiter_str: expr_res!(str::from_utf8(delimiter)) - >> s: take_until!(delimiter_str) - >> tag!(delimiter_str) + >> s: take_until!(delimiter.0) + >> tag!(delimiter.0) >> end_of_line >> (indent, s) ), |(indent, s)| { - let body = String::from_utf8_lossy(s); - let lines: Vec<&str> = body.split("\n").collect(); + let lines: Vec<&str> = s.0.split("\n").collect(); let mut out: Vec<&str> = Vec::new(); let count = lines.len(); @@ -139,40 +131,44 @@ named!( ); // close enough... -named!( +complete_named!( identifier_char, alt!(tag!("_") | tag!("-") | tag!(".") | alphanumeric) ); -named!( +complete_named!( hcl_unquoted_key, - map!( - fold_many0!(identifier_char, Vec::new(), |mut acc: Vec<_>, item| { - acc.extend(item); + fold_many0!( + identifier_char, + String::new(), + |mut acc: String, item: CompleteStr| { + //acc.extend(item); + item.extend_into(&mut acc); acc - }), - to_s + } ) ); -named!( +complete_named!( hcl_quoted_escaped_key, map!( do_parse!(tag!("\"") >> out: opt!(hcl_escaped_string) >> tag!("\"") >> (out)), - |out| if let Some(val) = out { - val - } else { - "".to_string() + |out| { + if let Some(val) = out { + val + } else { + "".to_string() + } } ) ); -named!( +complete_named!( hcl_key, alt!(hcl_quoted_escaped_key | hcl_unquoted_key) ); -named!(space, eat_separator!(&b" \t"[..])); +complete_named!(space, eat_separator!(&b" \t"[..])); macro_rules! sp ( ($i:expr, $($args:tt)*) => ( @@ -182,58 +178,90 @@ macro_rules! sp ( ) ); -named!( - hcl_key_value<(String, JsonValue)>, - sp!(alt_complete!( +complete_named!( + hcl_key_value<(String, Value)>, + sp!(alt!( separated_pair!(hcl_key, tag!("="), hcl_value_nested_hash) | separated_pair!(hcl_key, tag!("="), hcl_value) | pair!(hcl_key, hcl_value_nested_hash) )) ); -named!( +complete_named!( comment_one_line, - do_parse!(alt!(tag!("//") | tag!("#")) >> opt!(not_line_ending) >> end_of_line >> (&b""[..])) + do_parse!( + alt!(tag!("//") | tag!("#")) >> opt!(not_line_ending) >> end_of_line >> (CompleteStr("")) + ) ); -named!( +complete_named!( comment_block, - do_parse!(tag!("/*") >> take_until_and_consume!("*/") >> (&b""[..])) + do_parse!(tag!("/*") >> take_until_and_consume!("*/") >> (CompleteStr(""))) ); -named!( +complete_named!( blanks, do_parse!( many0!(alt!( tag!(",") | multispace | comment_one_line | comment_block - )) >> (&b""[..]) + )) >> (CompleteStr("")) ) ); -named!( - hcl_key_values>, - many0!(complete!(do_parse!( +complete_named!( + hcl_key_values>, + many0!(do_parse!( opt!(blanks) >> out: hcl_key_value >> opt!(blanks) >> (out) - ))) + )) ); -named!( - hcl_hash>, +complete_named!( + hcl_hash>, do_parse!(opt!(blanks) >> tag!("{") >> out: hcl_top >> tag!("}") >> opt!(blanks) >> (out)) ); -named!( - hcl_top>, +#[cfg(not(feature = "arraynested"))] +complete_named!( + hcl_top>, map!(hcl_key_values, |tuple_vec| { - let mut top: HashMap = HashMap::new(); + let mut top: Map = Map::new(); for (k, v) in tuple_vec.into_iter().rev() { + // FIXME use deep merge if top.contains_key(&k) { - if let JsonValue::Array(ref v_a) = v { + if let Value::Object(ref val_dict) = v { + if let Some(mut current) = top.remove(&k) { + if let Value::Object(ref mut top_dict) = current { + let mut copy = top_dict.clone(); + let val_copy = val_dict.clone(); + copy.extend(val_copy); + top.insert(k, Value::Object(copy)); + continue; + } else { + top.insert(k, current); + continue; + } + } + } + } + top.insert(k, v); + } + top + }) +); + +#[cfg(feature = "arraynested")] +complete_named!( + hcl_top>, + map!(hcl_key_values, |tuple_vec| { + let mut top: Map = Map::new(); + for (k, v) in tuple_vec.into_iter().rev() { + if top.contains_key(&k) { + if let Value::Array(ref v_a) = v { if let Some(current) = top.remove(&k) { - if let JsonValue::Array(ref a) = current { + if let Value::Array(ref a) = current { let mut copy = v_a.to_vec(); copy.extend(a.to_vec()); - top.insert(k, JsonValue::Array(copy)); + top.insert(k, Value::Array(copy)); continue; } } @@ -245,35 +273,51 @@ named!( }) ); +#[cfg(not(feature = "arraynested"))] +complete_named!( + hcl_value_nested_hash, + map!( + // NOTE hcl allows arbitrarily deep nesting + pair!(many0!(sp!(hcl_quoted_escaped_key)), hcl_value_hash), + |(tuple_vec, value)| { + let mut cur = value; + for parent in tuple_vec.into_iter().rev() { + let mut h: Map = Map::new(); + h.insert(parent.to_string(), cur); + cur = Value::Object(h); + } + cur + } + ) +); + // a bit odd if you ask me -named!( - hcl_value_nested_hash, +#[cfg(feature = "arraynested")] +complete_named!( + hcl_value_nested_hash, map!( // NOTE hcl allows arbitrarily deep nesting pair!(many0!(sp!(hcl_quoted_escaped_key)), hcl_value_hash), |(tuple_vec, value)| { let mut cur = value; for parent in tuple_vec.into_iter().rev() { - let mut inner: Vec = Vec::new(); + let mut inner: Vec = Vec::new(); inner.push(cur); - let mut h: HashMap = HashMap::new(); - h.insert(parent.to_string(), JsonValue::Array(inner)); - cur = JsonValue::Object(h); + let mut h: Map = Map::new(); + h.insert(parent.to_string(), Value::Array(inner)); + cur = Value::Object(h); } - let mut outer: Vec = Vec::new(); + let mut outer: Vec = Vec::new(); outer.push(cur); - JsonValue::Array(outer) + Value::Array(outer) } ) ); -named!( - hcl_value_hash, - map!(hcl_hash, |h| JsonValue::Object(h)) -); +complete_named!(hcl_value_hash, map!(hcl_hash, |h| Value::Object(h))); -named!( - hcl_array>, +complete_named!( + hcl_array>, delimited!( tag!("["), do_parse!( @@ -304,24 +348,24 @@ named!( ) ); -named!( - hcl_value, +complete_named!( + hcl_value, alt!( - hcl_hash => { |h| JsonValue::Object(h) } | - hcl_array => { |v| JsonValue::Array(v) } | - hcl_quoted_escaped_string => { |s| JsonValue::Str(s) } | - hcl_multiline_string => { |s| JsonValue::Str(s) } | - number => { |num| JsonValue::Num(num) } | - boolean => { |b| JsonValue::Boolean(b) } + hcl_hash => { |h| Value::Object(h) } | + hcl_array => { |v| Value::Array(v) } | + hcl_quoted_escaped_string => { |s| Value::String(s) } | + hcl_multiline_string => { |s| Value::String(s) } | + number => { |num| Value::Number(Number::from_f64(num as f64).unwrap()) } | + boolean => { |b| Value::Bool(b) } ) ); #[test] fn hcl_hex_num() { let test = "foo = 0x42"; - if let Ok(JsonValue::Object(dict)) = parse_hcl(test) { - if let Some(&JsonValue::Num(ref resp)) = dict.get("foo") { - return assert_eq!(66., *resp); + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::Number(ref resp)) = dict.get("foo") { + return assert_eq!(Number::from_f64(66.).unwrap(), *resp); } } panic!("object did not parse"); @@ -330,8 +374,8 @@ fn hcl_hex_num() { #[test] fn hcl_string_empty() { let test = "foo = \"\""; - if let Ok(JsonValue::Object(dict)) = parse_hcl(test) { - if let Some(&JsonValue::Str(ref resp)) = dict.get("foo") { + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::String(ref resp)) = dict.get("foo") { return assert_eq!("", resp); } } @@ -341,8 +385,8 @@ fn hcl_string_empty() { #[test] fn hcl_string_with_escaped_quote_test() { let test = "foo = \"bar\\\"foo\""; - if let Ok(JsonValue::Object(dict)) = parse_hcl(test) { - if let Some(&JsonValue::Str(ref resp)) = dict.get("foo") { + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::String(ref resp)) = dict.get("foo") { return assert_eq!("bar\"foo", resp); } } @@ -352,8 +396,8 @@ fn hcl_string_with_escaped_quote_test() { #[test] fn hcl_string_with_escaped_newline_test() { let test = "foo = \"bar\\nfoo\""; - if let Ok(JsonValue::Object(dict)) = parse_hcl(test) { - if let Some(&JsonValue::Str(ref resp)) = dict.get("foo") { + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::String(ref resp)) = dict.get("foo") { return assert_eq!("bar\nfoo", resp); } } @@ -363,8 +407,8 @@ fn hcl_string_with_escaped_newline_test() { #[test] fn hcl_string_with_space_test() { let test = "foo = \"bar foo\""; - if let Ok(JsonValue::Object(dict)) = parse_hcl(test) { - if let Some(&JsonValue::Str(ref resp)) = dict.get("foo") { + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::String(ref resp)) = dict.get("foo") { return assert_eq!("bar foo", resp); } } @@ -374,8 +418,8 @@ fn hcl_string_with_space_test() { #[test] fn hcl_string_with_template_test() { let test = "foo = \"${bar\"foo}\""; - if let Ok(JsonValue::Object(dict)) = parse_hcl(test) { - if let Some(&JsonValue::Str(ref resp)) = dict.get("foo") { + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::String(ref resp)) = dict.get("foo") { return assert_eq!("${bar\"foo}", resp); } } @@ -385,8 +429,8 @@ fn hcl_string_with_template_test() { #[test] fn hcl_string_with_escapes_and_template_test() { let test = "foo = \"wow\\\"wow${bar\"foo}\""; - if let Ok(JsonValue::Object(dict)) = parse_hcl(test) { - if let Some(&JsonValue::Str(ref resp)) = dict.get("foo") { + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::String(ref resp)) = dict.get("foo") { return assert_eq!("wow\"wow${bar\"foo}", resp); } } @@ -396,24 +440,41 @@ fn hcl_string_with_escapes_and_template_test() { #[test] fn hcl_string_multi_with_template() { let test = "foo = \"wow\"\nbar= \"${bar\"foo}\""; - if let Ok(JsonValue::Object(dict)) = parse_hcl(test) { - if let Some(&JsonValue::Str(ref resp)) = dict.get("foo") { + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::String(ref resp)) = dict.get("foo") { return assert_eq!("wow", resp); } } panic!("object did not parse"); } +#[cfg(not(feature = "arraynested"))] +#[test] +fn hcl_block_empty_key() { + let test = "foo \"\" {\nbar = 1\n}"; + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::Object(ref dict)) = dict.get("foo") { + if let Some(&Value::Object(ref dict)) = dict.get("") { + if let Some(&Value::Number(ref resp)) = dict.get("bar") { + return assert_eq!(Number::from_f64(1.).unwrap(), *resp); + } + } + } + } + panic!("object did not parse"); +} + +#[cfg(feature = "arraynested")] #[test] fn hcl_block_empty_key() { let test = "foo \"\" {\nbar = 1\n}"; - if let Ok(JsonValue::Object(dict)) = parse_hcl(test) { - if let Some(&JsonValue::Array(ref array)) = dict.get("foo") { - if let Some(&JsonValue::Object(ref dict)) = array.get(0) { - if let Some(&JsonValue::Array(ref array)) = dict.get("") { - if let Some(&JsonValue::Object(ref dict)) = array.get(0) { - if let Some(&JsonValue::Num(ref resp)) = dict.get("bar") { - return assert_eq!(1., *resp); + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::Array(ref array)) = dict.get("foo") { + if let Some(&Value::Object(ref dict)) = array.get(0) { + if let Some(&Value::Array(ref array)) = dict.get("") { + if let Some(&Value::Object(ref dict)) = array.get(0) { + if let Some(&Value::Number(ref resp)) = dict.get("bar") { + return assert_eq!(Number::from_f64(1.).unwrap(), *resp); } } } @@ -423,15 +484,32 @@ fn hcl_block_empty_key() { panic!("object did not parse"); } +#[cfg(not(feature = "arraynested"))] #[test] fn hcl_block_key() { let test = "potato \"salad\\\"is\" {\nnot = \"real\"\n}"; - if let Ok(JsonValue::Object(dict)) = parse_hcl(test) { - if let Some(&JsonValue::Array(ref array)) = dict.get("potato") { - if let Some(&JsonValue::Object(ref dict)) = array.get(0) { - if let Some(&JsonValue::Array(ref array)) = dict.get("salad\"is") { - if let Some(&JsonValue::Object(ref dict)) = array.get(0) { - if let Some(&JsonValue::Str(ref resp)) = dict.get("not") { + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::Object(ref dict)) = dict.get("potato") { + if let Some(&Value::Object(ref dict)) = dict.get("salad\"is") { + if let Some(&Value::String(ref resp)) = dict.get("not") { + return assert_eq!("real", resp); + } + } + } + } + panic!("object did not parse"); +} + +#[cfg(feature = "arraynested")] +#[test] +fn hcl_block_key() { + let test = "potato \"salad\\\"is\" {\nnot = \"real\"\n}"; + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::Array(ref array)) = dict.get("potato") { + if let Some(&Value::Object(ref dict)) = array.get(0) { + if let Some(&Value::Array(ref array)) = dict.get("salad\"is") { + if let Some(&Value::Object(ref dict)) = array.get(0) { + if let Some(&Value::String(ref resp)) = dict.get("not") { return assert_eq!("real", resp); } } @@ -442,18 +520,36 @@ fn hcl_block_key() { panic!("object did not parse"); } +#[cfg(not(feature = "arraynested"))] #[test] fn hcl_block_nested_key() { let test = "potato \"salad\" \"is\" {\nnot = \"real\"\n}"; - if let Ok(JsonValue::Object(dict)) = parse_hcl(test) { - println!("{:?}", dict); - if let Some(&JsonValue::Array(ref array)) = dict.get("potato") { - if let Some(&JsonValue::Object(ref dict)) = array.get(0) { - if let Some(&JsonValue::Array(ref array)) = dict.get("salad") { - if let Some(&JsonValue::Object(ref dict)) = array.get(0) { - if let Some(&JsonValue::Array(ref array)) = dict.get("is") { - if let Some(&JsonValue::Object(ref dict)) = array.get(0) { - if let Some(&JsonValue::Str(ref resp)) = dict.get("not") { + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::Object(ref dict)) = dict.get("potato") { + if let Some(&Value::Object(ref dict)) = dict.get("salad") { + if let Some(&Value::Object(ref dict)) = dict.get("is") { + if let Some(&Value::String(ref resp)) = dict.get("not") { + return assert_eq!("real", resp); + } + } + } + } + } + panic!("object did not parse"); +} + +#[cfg(feature = "arraynested")] +#[test] +fn hcl_block_nested_key() { + let test = "potato \"salad\" \"is\" {\nnot = \"real\"\n}"; + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::Array(ref array)) = dict.get("potato") { + if let Some(&Value::Object(ref dict)) = array.get(0) { + if let Some(&Value::Array(ref array)) = dict.get("salad") { + if let Some(&Value::Object(ref dict)) = array.get(0) { + if let Some(&Value::Array(ref array)) = dict.get("is") { + if let Some(&Value::Object(ref dict)) = array.get(0) { + if let Some(&Value::String(ref resp)) = dict.get("not") { return assert_eq!("real", resp); } } @@ -469,15 +565,15 @@ fn hcl_block_nested_key() { #[test] fn hcl_key_chars() { let test = "foo_bar = \"bar\""; - if let Ok(JsonValue::Object(dict)) = parse_hcl(test) { - if let Some(&JsonValue::Str(ref resp)) = dict.get("foo_bar") { + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::String(ref resp)) = dict.get("foo_bar") { return assert_eq!("bar", resp); } } let test = "foo_bar = \"bar\""; - if let Ok(JsonValue::Object(dict)) = parse_hcl(test) { - if let Some(&JsonValue::Str(ref resp)) = dict.get("foo_bar") { + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::String(ref resp)) = dict.get("foo_bar") { return assert_eq!("bar", resp); } } @@ -485,6 +581,39 @@ fn hcl_key_chars() { panic!("object did not parse"); } +#[cfg(not(feature = "arraynested"))] +#[test] +fn hcl_slice_expand() { + let test = "service \"foo\" { + key = \"value\" +} + +service \"bar\" { + key = \"value\" +}"; + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::Object(ref dict)) = dict.get("service") { + let mut pass = false; + if let Some(&Value::Object(_)) = dict.get("foo") { + pass = true; + } + if !pass { + panic!("missing nested object") + } + pass = false; + if let Some(&Value::Object(_)) = dict.get("bar") { + pass = true; + } + if !pass { + panic!("missing nested object") + } + return; + } + } + panic!("object did not parse") +} + +#[cfg(feature = "arraynested")] #[test] fn hcl_slice_expand() { let test = "service \"foo\" { @@ -494,22 +623,24 @@ fn hcl_slice_expand() { service \"bar\" { key = \"value\" }"; - if let Ok(JsonValue::Object(dict)) = parse_hcl(test) { - if let Some(&JsonValue::Array(ref array)) = dict.get("service") { + if let Ok(Value::Object(dict)) = parse_hcl(test) { + if let Some(&Value::Array(ref array)) = dict.get("service") { let mut pass = false; - if let Some(&JsonValue::Object(_)) = array.get(0) { + if let Some(&Value::Object(_)) = array.get(0) { pass = true; } if !pass { panic!("missing nested object") } pass = false; - if let Some(&JsonValue::Object(_)) = array.get(1) { + if let Some(&Value::Object(_)) = array.get(1) { pass = true; } if !pass { panic!("missing nested object") } + return; } } + panic!("object did not parse") } diff --git a/src/json.rs b/src/json.rs index b3a236f..fa64093 100644 --- a/src/json.rs +++ b/src/json.rs @@ -1,64 +1,47 @@ //! This was modified from https://github.com/Geal/nom/blob/master/tests/json.rs //! Copyright (c) 2015-2016 Geoffroy Couprie - MIT License -use std::collections::HashMap; use std::str; -use nom::IResult::Done; +use nom; +use nom::types::CompleteStr; use crate::common::{boolean, float}; -use crate::types::{JsonValue, ParseError}; +use crate::types::{Map, Number, ParseError, Value}; // NOTE this json parser is only included for internal verification purposes // the standard hcl parser by hashicorp includes a nonstandrd json parser // this is not intended to mirror that -pub fn parse_json(config: &str) -> Result { - match json(&config.as_bytes()[..]) { - Done(_, c) => Ok(c), +pub fn parse_json(config: &str) -> Result { + match json(CompleteStr(config)) { + Ok((_, c)) => Ok(c), _ => Err(0), } } -named!(json, map!(json_hash, |h| JsonValue::Object(h))); +complete_named!(json, map!(json_hash, |h| Value::Object(h))); -fn to_s(i: Vec) -> String { - String::from_utf8_lossy(&i).into_owned() -} - -named!( +complete_named!( json_escaped_string, - map!( - escaped_transform!( - is_not!("\\\"\n"), - '\\', - alt!( - tag!("\\") => { |_| &b"\\"[..] } | - tag!("\"") => { |_| &b"\""[..] } | - tag!("n") => { |_| &b"\n"[..] } - ) - ), - to_s + escaped_transform!( + is_not!("\\\"\n"), + '\\', + alt!( + tag!("\\") => { |_| "\\" } | + tag!("\"") => { |_| "\"" } | + tag!("n") => { |_| "\n" } + ) ) ); -named!( +complete_named!( json_string, - delimited!( - tag!("\""), - map!( - fold_many0!(json_escaped_string, Vec::new(), |mut acc: Vec<_>, item| { - acc.push(item); - acc - }), - |s| s.join("") - ), - tag!("\"") - ) + delimited!(tag!("\""), json_escaped_string, tag!("\"")) ); -named!( - json_array>, +complete_named!( + json_array>, ws!(delimited!( tag!("["), separated_list!(tag!(","), json_value), @@ -66,13 +49,13 @@ named!( )) ); -named!( - json_key_value<(String, JsonValue)>, +complete_named!( + json_key_value<(String, Value)>, ws!(separated_pair!(json_string, tag!(":"), json_value)) ); -named!( - json_hash>, +complete_named!( + json_hash>, ws!(map!( delimited!( tag!("{"), @@ -80,7 +63,7 @@ named!( tag!("}") ), |tuple_vec| { - let mut h: HashMap = HashMap::new(); + let mut h: Map = Map::new(); for (k, v) in tuple_vec { h.insert(String::from(k), v); } @@ -89,14 +72,14 @@ named!( )) ); -named!( - json_value, +complete_named!( + json_value, ws!(alt!( - json_hash => { |h| JsonValue::Object(h) } | - json_array => { |v| JsonValue::Array(v) } | - json_string => { |s| JsonValue::Str(String::from(s)) } | - float => { |num| JsonValue::Num(num) } | - boolean => { |b| JsonValue::Boolean(b) } + json_hash => { |h| Value::Object(h) } | + json_array => { |v| Value::Array(v) } | + json_string => { |s| Value::String(String::from(s)) } | + float => { |num| Value::Number(Number::from_f64(num as f64).unwrap()) } | + boolean => { |b| Value::Bool(b) } )) ); @@ -106,11 +89,11 @@ fn json_bool_test() { \"b\": \"false\" }"; - if let Ok(JsonValue::Object(dict)) = parse_json(test) { - if let Some(&JsonValue::Boolean(ref resp)) = dict.get("a") { + if let Ok(Value::Object(dict)) = parse_json(test) { + if let Some(&Value::Bool(ref resp)) = dict.get("a") { assert_eq!(true, *resp); } - if let Some(&JsonValue::Boolean(ref resp)) = dict.get("b") { + if let Some(&Value::Bool(ref resp)) = dict.get("b") { assert_eq!(false, *resp); } return; @@ -124,11 +107,11 @@ fn json_hash_test() { \"b\": \"x\" }"; - if let Ok(JsonValue::Object(dict)) = parse_json(test) { - if let Some(&JsonValue::Num(ref resp)) = dict.get("a") { - assert_eq!(42., *resp); + if let Ok(Value::Object(dict)) = parse_json(test) { + if let Some(&Value::Number(ref resp)) = dict.get("a") { + assert_eq!(Number::from_f64(42.).unwrap(), *resp); } - if let Some(&JsonValue::Str(ref resp)) = dict.get("b") { + if let Some(&Value::String(ref resp)) = dict.get("b") { assert_eq!("x", *resp); } return; @@ -144,23 +127,23 @@ fn json_parse_example_test() { } }"; - if let Ok(JsonValue::Object(dict)) = parse_json(test) { - if let Some(&JsonValue::Num(ref resp)) = dict.get("a") { - assert_eq!(42., *resp); + if let Ok(Value::Object(dict)) = parse_json(test) { + if let Some(&Value::Number(ref resp)) = dict.get("a") { + assert_eq!(Number::from_f64(42.).unwrap(), *resp); } - if let Some(&JsonValue::Array(ref arr)) = dict.get("b") { - if let Some(&JsonValue::Str(ref resp)) = arr.get(0) { + if let Some(&Value::Array(ref arr)) = dict.get("b") { + if let Some(&Value::String(ref resp)) = arr.get(0) { assert_eq!("x", *resp); } - if let Some(&JsonValue::Str(ref resp)) = arr.get(1) { + if let Some(&Value::String(ref resp)) = arr.get(1) { assert_eq!("y", *resp); } - if let Some(&JsonValue::Num(ref resp)) = arr.get(2) { - assert_eq!(12., *resp); + if let Some(&Value::Number(ref resp)) = arr.get(2) { + assert_eq!(Number::from_f64(12.).unwrap(), *resp); } } - if let Some(&JsonValue::Object(ref dict)) = dict.get("c") { - if let Some(&JsonValue::Str(ref resp)) = dict.get("hello") { + if let Some(&Value::Object(ref dict)) = dict.get("c") { + if let Some(&Value::String(ref resp)) = dict.get("hello") { assert_eq!("world", *resp); } } diff --git a/src/lib.rs b/src/lib.rs index 8d22dc8..1242271 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,9 @@ #[macro_use] extern crate nom; +#[cfg(feature = "withserde")] +extern crate serde_json; + pub mod types; #[macro_use] diff --git a/src/types.rs b/src/types.rs index 454227f..a577daa 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,20 +1,50 @@ -use std::collections::HashMap; use std::fmt; +#[cfg(feature = "withserde")] +pub use serde_json::{Map, Number, Value}; + +//#[cfg(not(feature="serde"))] +use std::collections::HashMap; + +#[cfg(not(feature = "withserde"))] +pub type Map = HashMap; + +#[cfg(not(feature = "withserde"))] +#[derive(Clone, Debug, PartialEq)] +pub struct Number { + n: N, +} + +#[cfg(not(feature = "withserde"))] #[derive(Clone, Debug, PartialEq)] -pub enum JsonValue { - Str(String), - Num(f32), - Array(Vec), - Object(HashMap), - Boolean(bool), +pub enum N { + Float(f64), } +#[cfg(not(feature = "withserde"))] +impl Number { + pub fn from_f64(f: f64) -> Option { + Some(Number { n: N::Float(f) }) + } +} + +#[cfg(not(feature = "withserde"))] +#[derive(Clone, Debug, PartialEq)] +pub enum Value { + Null, + Bool(bool), + Number(Number), + String(String), + Array(Vec), + Object(Map), +} + +#[cfg(not(feature = "withserde"))] #[allow(unused_must_use)] -impl fmt::Display for JsonValue { +impl fmt::Display for Value { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { - JsonValue::Object(ref obj) => { + Value::Object(ref obj) => { "{".fmt(f); for (n, prop) in obj.iter().enumerate() { if n != 0 { @@ -28,7 +58,7 @@ impl fmt::Display for JsonValue { "}".fmt(f); Result::Ok(()) } - JsonValue::Array(ref arr) => { + Value::Array(ref arr) => { "[".fmt(f); for (n, item) in arr.iter().enumerate() { if n != 0 { @@ -39,10 +69,14 @@ impl fmt::Display for JsonValue { "]".fmt(f); Result::Ok(()) } - JsonValue::Str(ref string) => write!(f, "\"{}\"", string.escape_default()), - JsonValue::Num(number) => write!(f, "{}", number.to_string()), - JsonValue::Boolean(boolean) => write!(f, "{}", boolean.to_string()), + Value::String(ref string) => write!(f, "\"{}\"", string.escape_default()), + Value::Number(ref number) => match number.n { + N::Float(number) => write!(f, "{}", number.to_string()), + }, + Value::Bool(boolean) => write!(f, "{}", boolean.to_string()), + Value::Null => write!(f, "null"), } } } + pub type ParseError = u32; diff --git a/tests/integration_tests.rs b/tests/fixture_tests.rs similarity index 97% rename from tests/integration_tests.rs rename to tests/fixture_tests.rs index 170213e..1ab67ad 100644 --- a/tests/integration_tests.rs +++ b/tests/fixture_tests.rs @@ -5,6 +5,7 @@ use std::path::Path; use molysite::hcl::parse_hcl; use molysite::json::parse_json; +#[cfg(feature = "arraynested")] macro_rules! fixture_tests { ($($name:ident: $value:expr,)*) => { $( @@ -17,6 +18,7 @@ macro_rules! fixture_tests { } } +#[cfg(feature = "arraynested")] fixture_tests! { test_fixture_assign_deep: ("assign_deep", true), test_fixture_basic: ("basic", true), @@ -57,6 +59,7 @@ fixture_tests! { //test_fixture_unterminated_brace: ("unterminated_brace", false), } +#[allow(dead_code)] fn test_fixture(case: &str, expect_pass: bool) { let mut hcl = String::new(); let mut json = String::new(); diff --git a/tests/fixtures.rs b/tests/fixtures.rs new file mode 100644 index 0000000..b7b078d --- /dev/null +++ b/tests/fixtures.rs @@ -0,0 +1,96 @@ +extern crate molysite; + +#[cfg(feature = "arraynested")] +mod fixtures { + use std::fs::File; + use std::io::prelude::*; + use std::path::Path; + + use molysite::hcl::parse_hcl; + use molysite::json::parse_json; + + macro_rules! fixture_tests { + ($($name:ident: $value:expr,)*) => { + $( + #[test] + fn $name() { + let (case, expect_pass) = $value; + test_fixture(case, expect_pass); + } + )* + } + } + + fixture_tests! { + test_fixture_assign_deep: ("assign_deep", true), + test_fixture_basic: ("basic", true), + test_fixture_basic_int_string: ("basic_int_string", true), + test_fixture_basic_squish: ("basic_squish", true), + //test_fixture_block_assign: ("block_assign", false), + test_fixture_decode_policy: ("decode_policy", true), + test_fixture_decode_tf_variable: ("decode_tf_variable", true), + test_fixture_empty: ("empty", true), + test_fixture_escape: ("escape", true), + test_fixture_escape_backslash: ("escape_backslash", true), + test_fixture_flat: ("flat", true), + test_fixture_float: ("float", true), + //test_fixture_git_crypt: ("git_crypt", false), + test_fixture_list_of_lists: ("list_of_lists", true), + test_fixture_list_of_maps: ("list_of_maps", true), + test_fixture_multiline: ("multiline", true), + //test_fixture_multiline_bad: ("multiline_bad", false), + test_fixture_multiline_indented: ("multiline_indented", true), + //test_fixture_multiline_literal: ("multiline_literal", false), + test_fixture_multiline_literal_with_hil: ("multiline_literal_with_hil", true), + test_fixture_multiline_no_eof: ("multiline_no_eof", true), + test_fixture_multiline_no_hanging_indent: ("multiline_no_hanging_indent", true), + //test_fixture_multiline_no_marker: ("multiline_no_marker", false), + test_fixture_nested_block_comment: ("nested_block_comment", true), + //test_fixture_nested_provider_bad: ("nested_provider_bad", false), + test_fixture_object_with_bool: ("object_with_bool", true), + test_fixture_scientific: ("scientific", true), + test_fixture_slice_expand: ("slice_expand", true), + //test_fixture_structure2: ("structure2", false), + test_fixture_structure: ("structure", true), + test_fixture_structure_flatmap: ("structure_flatmap", true), + test_fixture_structure_list: ("structure_list", true), + test_fixture_structure_multi: ("structure_multi", true), + test_fixture_terraform_heroku: ("terraform_heroku", true), + test_fixture_tfvars: ("tfvars", true), + //test_fixture_unterminated_block_comment: ("unterminated_block_comment", false), + //test_fixture_unterminated_brace: ("unterminated_brace", false), + } + + #[allow(dead_code)] + fn test_fixture(case: &str, expect_pass: bool) { + let mut hcl = String::new(); + let mut json = String::new(); + + let hcl_path = format!("tests/test-fixtures/{}.hcl", case); + let json_path = format!("tests/test-fixtures/{}.hcl.json", case); + + let path = Path::new(&hcl_path); + let mut file = File::open(&path).unwrap(); + file.read_to_string(&mut hcl).unwrap(); + + if expect_pass { + let path = Path::new(&json_path); + let mut file = File::open(&path).unwrap(); + file.read_to_string(&mut json).unwrap(); + } + + let parsed_hcl = parse_hcl(&hcl); + if let Ok(parsed_hcl) = parsed_hcl { + if expect_pass { + let parsed_json = parse_json(&json).unwrap(); + assert_eq!(parsed_hcl, parsed_json); + } else { + panic!("Expected failure") + } + } else { + if expect_pass { + panic!("Expected success") + } + } + } +} diff --git a/tests/serde.rs b/tests/serde.rs new file mode 100644 index 0000000..739316d --- /dev/null +++ b/tests/serde.rs @@ -0,0 +1,93 @@ +extern crate serde; + +#[cfg(feature = "withserde")] +extern crate serde_json; + +#[allow(unused_imports)] +#[macro_use] +extern crate serde_derive; + +extern crate molysite; + +#[allow(unused_imports)] +use std::collections::HashMap; + +#[allow(unused_imports)] +use molysite::hcl::parse_hcl; + +#[cfg(feature = "withserde")] +#[test] +fn hcl_serde_basic() { + #[derive(Deserialize, Debug)] + struct User { + fingerprint: String, + location: String, + } + + let test = "fingerprint = \"foo\" +location = \"bar\""; + + if let Ok(j) = parse_hcl(test) { + let u: User = serde_json::from_value(j).unwrap(); + assert_eq!("foo", u.fingerprint); + assert_eq!("bar", u.location); + } +} + +#[cfg(not(feature = "arraynested"))] +#[cfg(feature = "withserde")] +#[test] +fn hcl_serde_policy() { + #[derive(Deserialize, Debug)] + struct PolicyResp { + key: HashMap, + } + + #[derive(Deserialize, Debug)] + struct Policy { + policy: String, + options: Option>, + } + + let test = "key \"\" { + policy = \"read\" +} + +key \"foo/\" { + policy = \"write\" + options = [\"list\", \"edit\"] +} + +key \"foo/bar/\" { + policy = \"read\" +} + +key \"foo/bar/baz\" { + policy = \"deny\" +}"; + + if let Ok(j) = parse_hcl(test) { + let u: PolicyResp = serde_json::from_value(j).unwrap(); + println!("{:?}", u); + if let Some(policy) = u.key.get("") { + assert_eq!(policy.policy, "read"); + } else { + panic!("missing key"); + } + if let Some(policy) = u.key.get("foo/") { + assert_eq!(policy.policy, "write"); + } else { + panic!("missing key"); + } + if let Some(policy) = u.key.get("foo/bar/") { + assert_eq!(policy.policy, "read"); + } else { + panic!("missing key"); + } + if let Some(policy) = u.key.get("foo/bar/baz") { + assert_eq!(policy.policy, "deny"); + } else { + panic!("missing key"); + } + } +}