From 3fdb2a3a19bc0248763d2bf15a152b8661534a82 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Tue, 13 Jun 2017 17:36:41 -0700 Subject: Add YAML --- src/file/format/mod.rs | 18 ++++----- src/file/format/yaml.rs | 93 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 ++ tests/Settings-invalid.yaml | 2 + tests/Settings.yaml | 12 ++++++ tests/file_yaml.rs | 71 ++++++++++++++++++++++++++++++++++ 6 files changed, 190 insertions(+), 9 deletions(-) create mode 100644 src/file/format/yaml.rs create mode 100644 tests/Settings-invalid.yaml create mode 100644 tests/Settings.yaml create mode 100644 tests/file_yaml.rs diff --git a/src/file/format/mod.rs b/src/file/format/mod.rs index c9b1012..02cafe7 100644 --- a/src/file/format/mod.rs +++ b/src/file/format/mod.rs @@ -9,8 +9,8 @@ mod toml; #[cfg(feature = "json")] mod json; -// #[cfg(feature = "yaml")] -// mod yaml; +#[cfg(feature = "yaml")] +mod yaml; #[derive(Debug, Clone, Copy)] pub enum FileFormat { @@ -22,9 +22,9 @@ pub enum FileFormat { #[cfg(feature = "json")] Json, - // /// YAML (parsed with yaml_rust) - // #[cfg(feature = "yaml")] - // Yaml, + /// YAML (parsed with yaml_rust) + #[cfg(feature = "yaml")] + Yaml, } impl FileFormat { @@ -38,8 +38,8 @@ impl FileFormat { #[cfg(feature = "json")] FileFormat::Json => vec!["json"], - // #[cfg(feature = "yaml")] - // FileFormat::Yaml => vec!["yaml", "yml"], + #[cfg(feature = "yaml")] + FileFormat::Yaml => vec!["yaml", "yml"], } } @@ -54,8 +54,8 @@ impl FileFormat { #[cfg(feature = "json")] FileFormat::Json => json::parse(uri, text, namespace), - // #[cfg(feature = "yaml")] - // FileFormat::Yaml => yaml::Content::parse(text, namespace), + #[cfg(feature = "yaml")] + FileFormat::Yaml => yaml::parse(uri, text, namespace), } } } diff --git a/src/file/format/yaml.rs b/src/file/format/yaml.rs new file mode 100644 index 0000000..2840438 --- /dev/null +++ b/src/file/format/yaml.rs @@ -0,0 +1,93 @@ +use yaml_rust as yaml; +use source::Source; +use std::error::Error; +use std::fmt; +use std::collections::{BTreeMap, HashMap}; +use std::mem; +use value::{Value, ValueKind}; + +pub fn parse(uri: Option<&String>, text: &str, namespace: Option<&String>) -> Result, Box> { + let mut docs = yaml::YamlLoader::load_from_str(text)?; + + // Designate root + let mut root = match docs.len() { + 0 => yaml::Yaml::Hash(BTreeMap::new()), + 1 => mem::replace(&mut docs[0], yaml::Yaml::Null), + n => { + return Err(Box::new(MultipleDocumentsError(n))); + } + }; + + // Limit to namespace + if let Some(namespace) = namespace { + root = yaml::Yaml::Hash(match root { + yaml::Yaml::Hash(ref mut table) => { + if let Some(yaml::Yaml::Hash(table)) = table.remove(&yaml::Yaml::String(namespace.clone())) { + table + } else { + BTreeMap::new() + } + } + + _ => { + BTreeMap::new() + } + }); + }; + + // TODO: Have a proper error fire if the root of a file is ever not a Table + let value = from_yaml_value(uri, &root); + match value.kind { + ValueKind::Table(map) => Ok(map), + + _ => Ok(HashMap::new()), + } +} + +fn from_yaml_value(uri: Option<&String>, value: &yaml::Yaml) -> Value { + match *value { + yaml::Yaml::String(ref value) => Value::new(uri, ValueKind::String(value.clone())), + yaml::Yaml::Real(ref value) => Value::new(uri, ValueKind::Float(value.parse::().unwrap())), + yaml::Yaml::Integer(value) => Value::new(uri, ValueKind::Integer(value)), + yaml::Yaml::Boolean(value) => Value::new(uri, ValueKind::Boolean(value)), + yaml::Yaml::Hash(ref table) => { + let mut m = HashMap::new(); + for (key, value) in table { + if let Some(k) = key.as_str() { + m.insert(k.to_owned(), from_yaml_value(uri, value)); + } + // TODO: should we do anything for non-string keys? + } + Value::new(uri, ValueKind::Table(m)) + } + yaml::Yaml::Array(ref array) => { + let mut l = Vec::new(); + + for value in array { + l.push(from_yaml_value(uri, value)); + } + + Value::new(uri, ValueKind::Array(l)) + } + // TODO: how should we handle Null and BadValue? + _ => { + unimplemented!(); + } + + } +} + +#[derive(Debug, Copy, Clone)] +struct MultipleDocumentsError(usize); + +impl fmt::Display for MultipleDocumentsError { + fn fmt(&self, format: &mut fmt::Formatter) -> fmt::Result { + write!(format, "Got {} YAML documents, expected 1", self.0) + } +} + +impl Error for MultipleDocumentsError { + fn description(&self) -> &str { + "More than one YAML document provided" + } +} diff --git a/src/lib.rs b/src/lib.rs index 3ea87d6..534533d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,6 +33,9 @@ extern crate toml; #[cfg(feature = "json")] extern crate serde_json; +#[cfg(feature = "yaml")] +extern crate yaml_rust; + mod error; mod value; mod de; diff --git a/tests/Settings-invalid.yaml b/tests/Settings-invalid.yaml new file mode 100644 index 0000000..070ff1b --- /dev/null +++ b/tests/Settings-invalid.yaml @@ -0,0 +1,2 @@ +ok: true +error false diff --git a/tests/Settings.yaml b/tests/Settings.yaml new file mode 100644 index 0000000..c92fcb0 --- /dev/null +++ b/tests/Settings.yaml @@ -0,0 +1,12 @@ +debug: true +production: false +arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +place: + name: Torre di Pisa + longitude: 43.7224985 + latitude: 10.3970522 + favorite: false + reviews: 3866 + rating: 4.5 + creator: + name: John Smith diff --git a/tests/file_yaml.rs b/tests/file_yaml.rs new file mode 100644 index 0000000..4ca5557 --- /dev/null +++ b/tests/file_yaml.rs @@ -0,0 +1,71 @@ +extern crate config; +extern crate serde; +extern crate float_cmp; + +#[macro_use] +extern crate serde_derive; + +use std::collections::HashMap; +use float_cmp::ApproxEqUlps; +use config::*; + +#[derive(Debug, Deserialize)] +struct Place { + name: String, + longitude: f64, + latitude: f64, + favorite: bool, + telephone: Option, + reviews: u64, + creator: HashMap, + rating: Option, +} + +#[derive(Debug, Deserialize)] +struct Settings { + debug: f64, + production: Option, + place: Place, + #[serde(rename = "arr")] + elements: Vec, +} + +fn make() -> Config { + let mut c = Config::default(); + c.merge(File::new("tests/Settings", FileFormat::Yaml)) + .unwrap(); + + c +} + +#[test] +fn test_file() { + let c = make(); + + // Deserialize the entire file as single struct + let s: Settings = c.deserialize().unwrap(); + + assert!(s.debug.approx_eq_ulps(&1.0, 2)); + assert_eq!(s.production, Some("false".to_string())); + assert_eq!(s.place.name, "Torre di Pisa"); + assert!(s.place.longitude.approx_eq_ulps(&43.7224985, 2)); + assert!(s.place.latitude.approx_eq_ulps(&10.3970522, 2)); + assert_eq!(s.place.favorite, false); + assert_eq!(s.place.reviews, 3866); + assert_eq!(s.place.rating, Some(4.5)); + assert_eq!(s.place.telephone, None); + assert_eq!(s.elements.len(), 10); + assert_eq!(s.elements[3], "4".to_string()); + assert_eq!(s.place.creator["name"].clone().into_str().unwrap(), "John Smith".to_string()); +} + +#[test] +fn test_error_parse() { + let mut c = Config::default(); + let res = c.merge(File::new("tests/Settings-invalid", FileFormat::Yaml)); + + assert!(res.is_err()); + assert_eq!(res.unwrap_err().to_string(), + "while parsing a block mapping, did not find expected key at line 2 column 1 in tests/Settings-invalid.yaml" + .to_string()); +} -- cgit v1.2.3