From 3c3d1e860eded0027f1efe3bb25e0fa6fa8b8940 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Tue, 13 Jun 2017 15:57:21 -0700 Subject: Add JSON --- src/file/format/json.rs | 77 ++++++++++++++++++++ src/file/format/mod.rs | 18 ++--- src/lib.rs | 3 + tests/Settings-invalid.json | 4 ++ tests/Settings.json | 16 +++++ tests/file_json.rs | 71 ++++++++++++++++++ tests/get.rs | 170 ++++++++++++++++++++++++++++++++++++++++++++ tests/get_array.rs | 39 ---------- tests/get_scalar.rs | 52 -------------- tests/get_struct.rs | 70 ------------------ tests/get_table.rs | 50 ------------- 11 files changed, 350 insertions(+), 220 deletions(-) create mode 100644 src/file/format/json.rs create mode 100644 tests/Settings-invalid.json create mode 100644 tests/Settings.json create mode 100644 tests/file_json.rs create mode 100644 tests/get.rs delete mode 100644 tests/get_array.rs delete mode 100644 tests/get_scalar.rs delete mode 100644 tests/get_struct.rs delete mode 100644 tests/get_table.rs diff --git a/src/file/format/json.rs b/src/file/format/json.rs new file mode 100644 index 0000000..a3d80a7 --- /dev/null +++ b/src/file/format/json.rs @@ -0,0 +1,77 @@ +use serde_json; +use source::Source; +use std::collections::HashMap; +use std::error::Error; +use value::{Value, ValueKind}; + +pub fn parse(uri: Option<&String>, text: &str, namespace: Option<&String>) -> Result, Box> { + // Parse a JSON object value from the text + let mut root: serde_json::Value = serde_json::from_str(text)?; + + // Limit to namespace + if let Some(namespace) = namespace { + root = serde_json::Value::Object(match root { + serde_json::Value::Object(ref mut table) => { + if let Some(serde_json::Value::Object(table)) = table.remove(namespace) { + table + } else { + serde_json::Map::new() + } + } + + _ => { + serde_json::Map::new() + } + }); + }; + + // TODO: Have a proper error fire if the root of a file is ever not a Table + let value = from_json_value(uri, &root); + match value.kind { + ValueKind::Table(map) => Ok(map), + + _ => Ok(HashMap::new()), + } +} + +fn from_json_value(uri: Option<&String>, value: &serde_json::Value) -> Value { + match *value { + serde_json::Value::String(ref value) => Value::new(uri, ValueKind::String(value.clone())), + + serde_json::Value::Number(ref value) => { + if let Some(value) = value.as_i64() { + Value::new(uri, ValueKind::Integer(value)) + } else if let Some(value) = value.as_f64() { + Value::new(uri, ValueKind::Float(value)) + } else { + unreachable!(); + } + } + + serde_json::Value::Bool(value) => Value::new(uri, ValueKind::Boolean(value)), + + serde_json::Value::Object(ref table) => { + let mut m = HashMap::new(); + + for (key, value) in table { + m.insert(key.clone(), from_json_value(uri, value)); + } + + Value::new(uri, ValueKind::Table(m)) + } + + serde_json::Value::Array(ref array) => { + let mut l = Vec::new(); + + for value in array { + l.push(from_json_value(uri, value)); + } + + Value::new(uri, ValueKind::Array(l)) + } + + serde_json::Value::Null => { + Value::new(uri, ValueKind::Nil) + } + } +} diff --git a/src/file/format/mod.rs b/src/file/format/mod.rs index 5c36815..c9b1012 100644 --- a/src/file/format/mod.rs +++ b/src/file/format/mod.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; #[cfg(feature = "toml")] mod toml; -// #[cfg(feature = "json")] -// mod json; +#[cfg(feature = "json")] +mod json; // #[cfg(feature = "yaml")] // mod yaml; @@ -18,9 +18,9 @@ pub enum FileFormat { #[cfg(feature = "toml")] Toml, - // /// JSON (parsed with serde_json) - // #[cfg(feature = "json")] - // Json, + /// JSON (parsed with serde_json) + #[cfg(feature = "json")] + Json, // /// YAML (parsed with yaml_rust) // #[cfg(feature = "yaml")] @@ -35,8 +35,8 @@ impl FileFormat { #[cfg(feature = "toml")] FileFormat::Toml => vec!["toml"], - // #[cfg(feature = "json")] - // FileFormat::Json => vec!["json"], + #[cfg(feature = "json")] + FileFormat::Json => vec!["json"], // #[cfg(feature = "yaml")] // FileFormat::Yaml => vec!["yaml", "yml"], @@ -51,8 +51,8 @@ impl FileFormat { #[cfg(feature = "toml")] FileFormat::Toml => toml::parse(uri, text, namespace), - // #[cfg(feature = "json")] - // FileFormat::Json => json::Content::parse(text, namespace), + #[cfg(feature = "json")] + FileFormat::Json => json::parse(uri, text, namespace), // #[cfg(feature = "yaml")] // FileFormat::Yaml => yaml::Content::parse(text, namespace), diff --git a/src/lib.rs b/src/lib.rs index 5d06364..3ea87d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,6 +30,9 @@ extern crate nom; #[cfg(feature = "toml")] extern crate toml; +#[cfg(feature = "json")] +extern crate serde_json; + mod error; mod value; mod de; diff --git a/tests/Settings-invalid.json b/tests/Settings-invalid.json new file mode 100644 index 0000000..ba2d7cb --- /dev/null +++ b/tests/Settings-invalid.json @@ -0,0 +1,4 @@ +{ + "ok": true, + "error" +} diff --git a/tests/Settings.json b/tests/Settings.json new file mode 100644 index 0000000..c8b72c5 --- /dev/null +++ b/tests/Settings.json @@ -0,0 +1,16 @@ +{ + "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_json.rs b/tests/file_json.rs new file mode 100644 index 0000000..983be86 --- /dev/null +++ b/tests/file_json.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::Json)) + .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::Json)); + + assert!(res.is_err()); + assert_eq!(res.unwrap_err().to_string(), + "expected `:` at line 4 column 1 in tests/Settings-invalid.json" + .to_string()); +} diff --git a/tests/get.rs b/tests/get.rs new file mode 100644 index 0000000..1c98643 --- /dev/null +++ b/tests/get.rs @@ -0,0 +1,170 @@ +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, + rating: Option, +} + +#[derive(Debug, Deserialize)] +struct Settings { + debug: f64, + production: Option, + place: Place, +} + +fn make() -> Config { + let mut c = Config::default(); + c.merge(File::new("tests/Settings", FileFormat::Toml)) + .unwrap(); + + c +} + +#[test] +fn test_scalar() { + let c = make(); + + assert_eq!(c.get("debug").ok(), Some(true)); + assert_eq!(c.get("production").ok(), Some(false)); +} + +#[test] +fn test_scalar_type_loose() { + let c = make(); + + assert_eq!(c.get("debug").ok(), Some(true)); + assert_eq!(c.get("debug").ok(), Some("true".to_string())); + assert_eq!(c.get("debug").ok(), Some(1)); + assert_eq!(c.get("debug").ok(), Some(1.0)); + + assert_eq!(c.get("debug_s").ok(), Some(true)); + assert_eq!(c.get("debug_s").ok(), Some("true".to_string())); + assert_eq!(c.get("debug_s").ok(), Some(1)); + assert_eq!(c.get("debug_s").ok(), Some(1.0)); + + assert_eq!(c.get("production").ok(), Some(false)); + assert_eq!(c.get("production").ok(), Some("false".to_string())); + assert_eq!(c.get("production").ok(), Some(0)); + assert_eq!(c.get("production").ok(), Some(0.0)); + + assert_eq!(c.get("production_s").ok(), Some(false)); + assert_eq!(c.get("production_s").ok(), Some("false".to_string())); + assert_eq!(c.get("production_s").ok(), Some(0)); + assert_eq!(c.get("production_s").ok(), Some(0.0)); +} + +#[test] +fn test_get_scalar_path() { + let c = make(); + + assert_eq!(c.get("place.favorite").ok(), Some(false)); + assert_eq!(c.get("place.creator.name").ok(), Some("John Smith".to_string())); +} + +#[test] +fn test_map() { + let c = make(); + let m: HashMap = c.get("place").unwrap(); + + assert_eq!(m.len(), 7); + assert_eq!(m["name"].clone().into_str().unwrap(), "Torre di Pisa".to_string()); + assert_eq!(m["reviews"].clone().into_int().unwrap(), 3866); +} + +#[test] +fn test_map_str() { + let c = make(); + let m: HashMap = c.get("place.creator").unwrap(); + + assert_eq!(m.len(), 1); + assert_eq!(m["name"], "John Smith".to_string()); +} + +#[test] +fn test_map_struct() { + #[derive(Debug, Deserialize)] + struct Settings { + place: HashMap, + } + + let c = make(); + let s: Settings = c.deserialize().unwrap(); + + assert_eq!(s.place.len(), 7); + assert_eq!(s.place["name"].clone().into_str().unwrap(), "Torre di Pisa".to_string()); + assert_eq!(s.place["reviews"].clone().into_int().unwrap(), 3866); +} + +#[test] +fn test_file_struct() { + 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); +} + +#[test] +fn test_scalar_struct() { + let c = make(); + + // Deserialize a scalar struct that has lots of different + // data types + let p: Place = c.get("place").unwrap(); + + assert_eq!(p.name, "Torre di Pisa"); + assert!(p.longitude.approx_eq_ulps(&43.7224985, 2)); + assert!(p.latitude.approx_eq_ulps(&10.3970522, 2)); + assert_eq!(p.favorite, false); + assert_eq!(p.reviews, 3866); + assert_eq!(p.rating, Some(4.5)); + assert_eq!(p.telephone, None); +} + +#[test] +fn test_array_scalar() { + let c = make(); + let arr: Vec = c.get("arr").unwrap(); + + assert_eq!(arr.len(), 10); + assert_eq!(arr[3], 4); +} + +#[test] +fn test_struct_array() { + #[derive(Debug, Deserialize)] + struct Settings { + #[serde(rename = "arr")] + elements: Vec, + } + + let c = make(); + let s: Settings = c.deserialize().unwrap(); + + assert_eq!(s.elements.len(), 10); + assert_eq!(s.elements[3], "4".to_string()); +} diff --git a/tests/get_array.rs b/tests/get_array.rs deleted file mode 100644 index 579597c..0000000 --- a/tests/get_array.rs +++ /dev/null @@ -1,39 +0,0 @@ -extern crate config; -extern crate serde; - -#[macro_use] -extern crate serde_derive; - -use config::*; - -#[derive(Debug, Deserialize)] -struct Settings { - #[serde(rename = "arr")] - elements: Vec, -} - -fn make() -> Config { - let mut c = Config::default(); - c.merge(File::new("tests/Settings", FileFormat::Toml)) - .unwrap(); - - c -} - -#[test] -fn test_array_scalar() { - let c = make(); - let arr: Vec = c.get("arr").unwrap(); - - assert_eq!(arr.len(), 10); - assert_eq!(arr[3], 4); -} - -#[test] -fn test_struct_array() { - let c = make(); - let s: Settings = c.deserialize().unwrap(); - - assert_eq!(s.elements.len(), 10); - assert_eq!(s.elements[3], "4".to_string()); -} diff --git a/tests/get_scalar.rs b/tests/get_scalar.rs deleted file mode 100644 index 5d54e18..0000000 --- a/tests/get_scalar.rs +++ /dev/null @@ -1,52 +0,0 @@ -extern crate config; - -use config::*; - -fn make() -> Config { - let mut c = Config::default(); - c.merge(File::new("tests/Settings", FileFormat::Toml)) - .unwrap(); - - c -} - -#[test] -fn test_scalar() { - let c = make(); - - assert_eq!(c.get("debug").ok(), Some(true)); - assert_eq!(c.get("production").ok(), Some(false)); -} - -#[test] -fn test_scalar_type_loose() { - let c = make(); - - assert_eq!(c.get("debug").ok(), Some(true)); - assert_eq!(c.get("debug").ok(), Some("true".to_string())); - assert_eq!(c.get("debug").ok(), Some(1)); - assert_eq!(c.get("debug").ok(), Some(1.0)); - - assert_eq!(c.get("debug_s").ok(), Some(true)); - assert_eq!(c.get("debug_s").ok(), Some("true".to_string())); - assert_eq!(c.get("debug_s").ok(), Some(1)); - assert_eq!(c.get("debug_s").ok(), Some(1.0)); - - assert_eq!(c.get("production").ok(), Some(false)); - assert_eq!(c.get("production").ok(), Some("false".to_string())); - assert_eq!(c.get("production").ok(), Some(0)); - assert_eq!(c.get("production").ok(), Some(0.0)); - - assert_eq!(c.get("production_s").ok(), Some(false)); - assert_eq!(c.get("production_s").ok(), Some("false".to_string())); - assert_eq!(c.get("production_s").ok(), Some(0)); - assert_eq!(c.get("production_s").ok(), Some(0.0)); -} - -#[test] -fn test_get_scalar_path() { - let c = make(); - - assert_eq!(c.get("place.favorite").ok(), Some(false)); - assert_eq!(c.get("place.creator.name").ok(), Some("John Smith".to_string())); -} diff --git a/tests/get_struct.rs b/tests/get_struct.rs deleted file mode 100644 index 57e4bb2..0000000 --- a/tests/get_struct.rs +++ /dev/null @@ -1,70 +0,0 @@ -extern crate config; -extern crate serde; -extern crate float_cmp; - -#[macro_use] -extern crate serde_derive; - -use config::*; -use float_cmp::ApproxEqUlps; - -#[derive(Debug, Deserialize)] -struct Place { - name: String, - longitude: f64, - latitude: f64, - favorite: bool, - telephone: Option, - reviews: u64, - rating: Option, -} - -#[derive(Debug, Deserialize)] -struct Settings { - debug: f64, - production: Option, - place: Place, -} - -fn make() -> Config { - let mut c = Config::default(); - c.merge(File::new("tests/Settings", FileFormat::Toml)) - .unwrap(); - - c -} - -#[test] -fn test_file_struct() { - 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); -} - -#[test] -fn test_scalar_struct() { - let c = make(); - - // Deserialize a scalar struct that has lots of different - // data types - let p: Place = c.get("place").unwrap(); - - assert_eq!(p.name, "Torre di Pisa"); - assert!(p.longitude.approx_eq_ulps(&43.7224985, 2)); - assert!(p.latitude.approx_eq_ulps(&10.3970522, 2)); - assert_eq!(p.favorite, false); - assert_eq!(p.reviews, 3866); - assert_eq!(p.rating, Some(4.5)); - assert_eq!(p.telephone, None); -} diff --git a/tests/get_table.rs b/tests/get_table.rs deleted file mode 100644 index 8fba334..0000000 --- a/tests/get_table.rs +++ /dev/null @@ -1,50 +0,0 @@ -extern crate config; -extern crate serde; - -#[macro_use] -extern crate serde_derive; - -use std::collections::HashMap; -use config::*; - -#[derive(Debug, Deserialize)] -struct Settings { - place: HashMap, -} - -fn make() -> Config { - let mut c = Config::default(); - c.merge(File::new("tests/Settings", FileFormat::Toml)) - .unwrap(); - - c -} - -#[test] -fn test_map() { - let c = make(); - let m: HashMap = c.get("place").unwrap(); - - assert_eq!(m.len(), 7); - assert_eq!(m["name"].clone().into_str().unwrap(), "Torre di Pisa".to_string()); - assert_eq!(m["reviews"].clone().into_int().unwrap(), 3866); -} - -#[test] -fn test_map_str() { - let c = make(); - let m: HashMap = c.get("place.creator").unwrap(); - - assert_eq!(m.len(), 1); - assert_eq!(m["name"], "John Smith".to_string()); -} - -#[test] -fn test_map_struct() { - let c = make(); - let s: Settings = c.deserialize().unwrap(); - - assert_eq!(s.place.len(), 7); - assert_eq!(s.place["name"].clone().into_str().unwrap(), "Torre di Pisa".to_string()); - assert_eq!(s.place["reviews"].clone().into_int().unwrap(), 3866); -} -- cgit v1.2.3