Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ edition = "2018"
maintenance = { status = "actively-developed" }

[features]
default = ["toml", "json", "yaml", "ini", "ron", "json5", "convert-case", "async"]
default = ["toml", "json", "yaml", "ini", "ron", "json5", "gura", "convert-case", "async"]
json = ["serde_json"]
yaml = ["yaml-rust"]
ini = ["rust-ini"]
json5 = ["json5_rs"]
ura = ["gura"]
convert-case = ["convert_case"]
preserve_order = ["indexmap", "toml/preserve_order", "serde_json/preserve_order", "ron/indexmap"]
async = ["async-trait"]
Expand All @@ -36,6 +37,7 @@ yaml-rust = { version = "0.4", optional = true }
rust-ini = { version = "0.19", optional = true }
ron = { version = "0.8", optional = true }
json5_rs = { version = "0.4", optional = true, package = "json5" }
gura = { version = "0.3.0", optional = true, package = "gura" }
indexmap = { version = "2.0.0", features = ["serde"], optional = true }
convert_case = { version = "0.6", optional = true }
pathdiff = "0.2"
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

- Set defaults
- Set explicit values (to programmatically override)
- Read from [JSON], [TOML], [YAML], [INI], [RON], [JSON5] files
- Read from [JSON], [TOML], [YAML], [INI], [RON], [JSON5], [GURA] files
- Read from environment
- Loosely typed — Configuration values may be read in any supported type, as long as there exists a reasonable conversion
- Access nested fields using a formatted path — Uses a subset of JSONPath; currently supports the child ( `redis.port` ) and subscript operators ( `databases[0].name` )
Expand All @@ -22,6 +22,7 @@
[INI]: https://github.com/zonyitoo/rust-ini
[RON]: https://github.com/ron-rs/ron
[JSON5]: https://github.com/callum-oakley/json5-rs
[GURA]: https://github.com/gura-conf/gura-rs-parser

Please note this library

Expand All @@ -43,6 +44,7 @@ config = "0.13.1"
- `toml` - Adds support for reading TOML files
- `ron` - Adds support for reading RON files
- `json5` - Adds support for reading JSON5 files
- `gura` - Adds support for reading GURA files

### Support for custom formats

Expand Down
57 changes: 57 additions & 0 deletions src/file/format/gura.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use std::error::Error;

use gura::GuraType;

use crate::map::Map;
use crate::value::{Value, ValueKind};

pub fn parse(
uri: Option<&String>,
text: &str,
) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> {
let val = from_gura_value(uri, &gura::parse(text).unwrap());

match val.kind {
ValueKind::Table(map) => Ok(map),

_ => Ok(Map::new())
}
}

fn from_gura_value(uri: Option<&String>, val: &GuraType) -> Value {
match val {
GuraType::Null => Value::new(uri, ValueKind::Nil),

GuraType::Object(ref table) => {
let mut m = Map::new();

for (key, val) in table {
m.insert(key.clone(), from_gura_value(uri, val));
}

Value::new(uri, ValueKind::Table(m))
}

GuraType::Bool(val) => Value::new(uri, ValueKind::Boolean(*val)),

GuraType::String(ref val) => Value::new(uri, ValueKind::String(val.clone())),

GuraType::Integer(val) => Value::new(uri, ValueKind::I64(*val as i64)),

GuraType::BigInteger(val) => Value::new(uri, ValueKind::I128(*val)),

GuraType::Float(val) => Value::new(uri, ValueKind::Float(*val)),

GuraType::Array(ref arr) => {
let mut l = Vec::new();

for val in arr {
l.push(from_gura_value(uri, val));
}

Value::new(uri, ValueKind::Array(l))
}

_ => Value::new(uri, ValueKind::Nil),
}
}
13 changes: 13 additions & 0 deletions src/file/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ mod ron;
#[cfg(feature = "json5")]
mod json5;

#[cfg(feature = "ura")]
mod gura;

/// File formats provided by the library.
///
/// Although it is possible to define custom formats using [`Format`] trait it is recommended to use FileFormat if possible.
Expand Down Expand Up @@ -55,6 +58,10 @@ pub enum FileFormat {
/// JSON5 (parsed with json5)
#[cfg(feature = "json5")]
Json5,

/// GURA (parsed with ura)
#[cfg(feature = "ura")]
Gura,
}

lazy_static! {
Expand All @@ -81,6 +88,9 @@ lazy_static! {
#[cfg(feature = "json5")]
formats.insert(FileFormat::Json5, vec!["json5"]);

#[cfg(feature = "ura")]
formats.insert(FileFormat::Gura, vec!["ura"]);

formats
};
}
Expand Down Expand Up @@ -117,6 +127,9 @@ impl FileFormat {
#[cfg(feature = "json5")]
FileFormat::Json5 => json5::parse(uri, text),

#[cfg(feature = "ura")]
FileFormat::Gura => gura::parse(uri, text),

#[cfg(all(
not(feature = "toml"),
not(feature = "json"),
Expand Down
3 changes: 3 additions & 0 deletions tests/Settings-enum-test.ura
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# comment

bar: "bar is a lowercase param"
20 changes: 20 additions & 0 deletions tests/Settings.ura
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#comment

debug: true
debug_gura: 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"
username: "jsmith"
email: "jsmith@localhost"

FOO: "FOO should be overridden"
bar: "I am bar"
199 changes: 199 additions & 0 deletions tests/file_gura.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#![cfg(feature = "json")]

use serde_derive::Deserialize;

use config::{Config, File, FileFormat, Map, Value};
use float_cmp::ApproxEqUlps;

#[derive(Debug, Deserialize)]
struct Place {
name: String,
longitude: f64,
latitude: f64,
favorite: bool,
telephone: Option<String>,
reviews: u64,
creator: Map<String, Value>,
rating: Option<f32>,
}

#[derive(Debug, Deserialize)]
struct Settings {
debug: f64,
production: Option<String>,
place: Place,
#[serde(rename = "arr")]
elements: Vec<String>,
}

fn make() -> Config {
Config::builder()
.add_source(File::new("tests/Settings", FileFormat::Gura))
.build()
.unwrap()
}

#[test]
fn test_file() {
let c = make();

// Deserialize the entire file as single struct
let s: Settings = c.try_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.722_498_5, 2));
assert!(s.place.latitude.approx_eq_ulps(&10.397_052_2, 2));
assert!(!s.place.favorite);
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());
if cfg!(feature = "preserve_order") {
assert_eq!(
s.place
.creator
.into_iter()
.collect::<Vec<(String, config::Value)>>(),
vec![
("name".to_string(), "John Smith".into()),
("username".into(), "jsmith".into()),
("email".into(), "jsmith@localhost".into()),
]
);
} else {
assert_eq!(
s.place.creator["name"].clone().into_string().unwrap(),
"John Smith".to_string()
);
}
}

#[test]
fn test_gura_vec() {
let c = Config::builder()
.add_source(File::from_str(
r#"
hosts: [
"alpha",
"omega"
]
"#,
FileFormat::Gura,
))
.build()
.unwrap();

let v = c.get_array("hosts").unwrap();
let mut vi = v.into_iter();
assert_eq!(vi.next().unwrap().into_string().unwrap(), "alpha");
assert_eq!(vi.next().unwrap().into_string().unwrap(), "omega");
assert!(vi.next().is_none());
}

#[derive(Debug, Deserialize, PartialEq)]
enum EnumSettings {
Bar(String),
}

#[derive(Debug, Deserialize, PartialEq)]
struct StructSettings {
foo: String,
bar: String,
}
#[derive(Debug, Deserialize, PartialEq)]
#[allow(non_snake_case)]
struct CapSettings {
FOO: String,
}

#[test]
fn test_override_uppercase_value_for_struct() {
std::env::set_var("APP_FOO", "I HAVE BEEN OVERRIDDEN_WITH_UPPER_CASE");

let cfg = Config::builder()
.add_source(File::new("tests/Settings", FileFormat::Gura))
.add_source(config::Environment::with_prefix("APP").separator("_"))
.build()
.unwrap();

let cap_settings = cfg.clone().try_deserialize::<CapSettings>();
let lower_settings = cfg.try_deserialize::<StructSettings>().unwrap();

match cap_settings {
Ok(v) => {
// this assertion will ensure that the map has only lowercase keys
assert_ne!(v.FOO, "FOO should be overridden");
assert_eq!(
lower_settings.foo,
"I HAVE BEEN OVERRIDDEN_WITH_UPPER_CASE".to_string()
);
}
Err(e) => {
if e.to_string().contains("missing field `FOO`") {
println!("triggered error {:?}", e);
assert_eq!(
lower_settings.foo,
"I HAVE BEEN OVERRIDDEN_WITH_UPPER_CASE".to_string()
);
} else {
panic!("{}", e);
}
}
}
}

#[test]
fn test_override_lowercase_value_for_struct() {
std::env::set_var("config_foo", "I have been overridden_with_lower_case");

let cfg = Config::builder()
.add_source(File::new("tests/Settings", FileFormat::Gura))
.add_source(config::Environment::with_prefix("config").separator("_"))
.build()
.unwrap();

let values: StructSettings = cfg.try_deserialize().unwrap();
assert_eq!(
values.foo,
"I have been overridden_with_lower_case".to_string()
);
assert_ne!(values.foo, "I am bar".to_string());
}

#[test]
fn test_override_uppercase_value_for_enums() {
std::env::set_var("APPS_BAR", "I HAVE BEEN OVERRIDDEN_WITH_UPPER_CASE");

let cfg = Config::builder()
.add_source(File::new("tests/Settings-enum-test", FileFormat::Gura))
.add_source(config::Environment::with_prefix("APPS").separator("_"))
.build()
.unwrap();
let val: EnumSettings = cfg.try_deserialize().unwrap();

assert_eq!(
val,
EnumSettings::Bar("I HAVE BEEN OVERRIDDEN_WITH_UPPER_CASE".to_string())
);
}

#[test]
fn test_override_lowercase_value_for_enums() {
std::env::set_var("test_bar", "I have been overridden_with_lower_case");

let cfg = Config::builder()
.add_source(File::new("tests/Settings-enum-test", FileFormat::Json))
.add_source(config::Environment::with_prefix("test").separator("_"))
.build()
.unwrap();

let param: EnumSettings = cfg.try_deserialize().unwrap();

assert_eq!(
param,
EnumSettings::Bar("I have been overridden_with_lower_case".to_string())
);
}