Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
90 changes: 83 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,17 @@ let path_to_config: PathBuf = {
};
```

---

* List of get methods available on Vec<Field>:
* **NOTE**: Any numbers outside of `i64` range will
error on TOML files as TOML spec does not support them
```rust
let config = Config::<JSON>::open("/path/to/config.json").unwrap();
let field = config.get(MyFields::SomeField).unwrap();

let f: Option<String> = field.get_string();
let f: Option<char> = field.get_char();
let f: Option<bool> = field.get_bool();
let f: Option<u8> = field.get_u8();
// If you need the underlying Value for custom deserialization
let f: Option<&serde_json::Value> = field.get_generic_inner();

let f: Option<String> = field.get_string();
let f: Option<char> = field.get_char();
Expand All @@ -132,10 +132,86 @@ let path_to_config: PathBuf = {
let f: Option<f64> = field.get_f64();
```

* Sometimes you may not know the exact path to a user's config file, but
you instead inform your user that it must in a list of possible locations.
---

* Sometimes a config's field isn't a basic type like String or u8.

In these cases, instead of using `field.get_u8()` etc., you can use
`field.get_generic_inner()` to access the field value directly.

If the key requested is present, Quickfig will get you a reference
to its field (as `&Value`) which you can then deserialize as needed.

Ex: You expect a config to have "colors" & "fonts" keys, and you
open a `config.json` with this content:
```json
{
"colors": {
"primary": "blue",
"accents": ["purple", "cyan"],
"filter": {
"brightness": 7,
"inverted": false
}
},
"fonts": [
{ "size": 1, "name": "roboto" },
{ "size": 2, "name": "verdana" }
]
}
```

In your application:
```rust
// Fields you expect to be in the config
#[derive(ConfigFields)]
enum AppConfig {
#[keys("colors")]
Colors,
#[keys("fonts")]
Fonts
}

// Types for your expected config structure
#[derive(serde::Deserialize)]
struct Colors {
primary: String,
accents: Vec<String>,
filter: Filter
}
#[derive(serde::Deserialize)]
struct Filter {
brightness: u8,
inverted: bool
}
#[derive(serde::Deserialize)]
struct Fonts(Vec<Font>);
#[derive(serde::Deserialize)]
struct Font {
size: u8,
name: String
}

// Opening the config.json file
let config = Config::<JSON>::open("/path/to/config.json").unwrap();
// Access "colors" key & verify only 1 match
let colors_field = config.get(AppConfig::Colors).unwrap();
colors_field.only_one_key().unwrap();

// Get the underlying value without trying to parse it
let colors_inner: &serde_json::Value = colors_field
.get_generic_inner()
.unwrap();

// Deserialize it yourself
let colors: Colors = Colors::deserialize(colors_inner).unwrap();
```

---

* Sometimes you want to allow multiple possible paths for a user's config.

For example, your docs may state:
For example, your docs might say:
```txt
MyApp will first check for your config at "~/.config/MyApp/config.json",
then "~/.MyApp/config.json", then "~/.local/share/MyApp/config.json"...
Expand Down
163 changes: 163 additions & 0 deletions bin_test/src/usage_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,176 @@ use super::utils::*;
use super::utils::TestFileType as TFT;

// MODS
// generics : testing generic types
// with custom deserialization
//
// json_main : testing JSON configs
// toml_main : testing TOML configs
//
// misc_tests_json : overlapping keys,
// misc_tests_toml : overlapping keys,


#[cfg(test)]
mod generics {
use anyhow::Result;
use quickfig::core::{
config_types::{ JSON, TOML },
VecField,
Field,
Config,
GetInner,
};
use quickfig::derive::ConfigFields;
use super::super::utils::*;
use super::super::utils::TestFileType as TFT;
use serde::Deserialize;

/// Top level structure
#[derive(Debug, Deserialize)]
pub struct Root {
pub courses: Vec<Course>,
pub contact: Contact,
}

/// Each item in the courses array
#[derive(Debug, Deserialize)]
pub struct Course {
pub title: String,
pub credits: u32,
pub details: Option<Details>,
}

/// Wrapper around coursrs array
#[derive(Debug, Deserialize)]
pub struct Courses(Vec<Course>);

/// Nested object under details
#[derive(Debug, Deserialize)]
pub struct Details {
pub room_number: u32,
pub teacher: String,
pub keywords: Vec<String>,
}

/// Contact info at the top level
#[derive(Debug, Deserialize)]
pub struct Contact {
pub email: String,
// JSON uses null, TOML uses ""
pub phone: Option<String>,
}

#[allow(non_camel_case_types)]
#[derive(ConfigFields)]
pub enum GenericTestEnum {
#[keys("courses")]
Courses,
#[keys("contact")]
Contact,
#[keys("not_there")]
NotThere,
}

#[cfg(test)]
mod json_generics {
use super::*;
const TEST_FILE_TYPE: TestFileType = TFT::JSON;

#[test]
fn test_generic() {
let mut testfile = TestFile::new(TEST_FILE_TYPE).unwrap();
testfile.add_all_generic_entries(TEST_FILE_TYPE).unwrap();
let config = Config::<JSON>::open(testfile.get_path()).unwrap();
testfile.delete().unwrap();

// Courses
let courses = config.get(GenericTestEnum::Courses).unwrap();
courses.only_one_key().unwrap();
// Should be the array of courses
let courses_inner = courses.get_generic_inner().unwrap();
let courses_de = Courses::deserialize(courses_inner).unwrap();
let c_vec = courses_de.0;
assert!(c_vec.len() == 2);
let history = &c_vec[0];
assert_eq!(history.title, "History 101");
assert_eq!(history.credits, 3);
assert!(history.details.is_some());
let details = history.details.as_ref().unwrap();
assert_eq!(details.room_number, 413);
assert_eq!(details.teacher, "Lopez");
assert_eq!(details.keywords, vec!["US", "History", "Introduction"]);
let math = &c_vec[1];
assert_eq!(math.title, "Mathematics 201");
assert_eq!(math.credits, 4);
assert!(math.details.is_none());

// Contact
let contact = config.get(GenericTestEnum::Contact).unwrap();
contact.only_one_key().unwrap();
// Should deserialize into contact
let contact_inner = contact.get_generic_inner().unwrap();
let contact_de = Contact::deserialize(contact_inner).unwrap();
assert_eq!(contact_de.email, String::from("john.smith@example.com"));
assert_eq!(contact_de.phone, None);

// Not there
let e = config.get(GenericTestEnum::NotThere);
assert!(e.is_none());
}
}

#[cfg(test)]
mod toml_generics {
use super::*;
const TEST_FILE_TYPE: TestFileType = TFT::TOML;

#[test]
fn test_generic() {
let mut testfile = TestFile::new(TEST_FILE_TYPE).unwrap();
testfile.add_all_generic_entries(TEST_FILE_TYPE).unwrap();
let config = Config::<TOML>::open(testfile.get_path()).unwrap();
testfile.delete().unwrap();

// Courses
let courses = config.get(GenericTestEnum::Courses).unwrap();
courses.only_one_key().unwrap();
// Should be the array of courses
let courses_inner = courses.get_generic_inner().unwrap();
let courses_de = Courses::deserialize(courses_inner.clone()).unwrap();
let c_vec = courses_de.0;
assert!(c_vec.len() == 2);
let history = &c_vec[0];
assert_eq!(history.title, "History 101");
assert_eq!(history.credits, 3);
assert!(history.details.is_some());
let details = history.details.as_ref().unwrap();
assert_eq!(details.room_number, 413);
assert_eq!(details.teacher, "Lopez");
assert_eq!(details.keywords, vec!["US", "History", "Introduction"]);
let math = &c_vec[1];
assert_eq!(math.title, "Mathematics 201");
assert_eq!(math.credits, 4);
assert!(math.details.is_none());

// Contact
let contact = config.get(GenericTestEnum::Contact).unwrap();
contact.only_one_key().unwrap();
// Should deserialize into contact
let contact_inner = contact.get_generic_inner().unwrap();
let contact_de = Contact::deserialize(contact_inner.clone()).unwrap();
assert_eq!(contact_de.email, String::from("john.smith@example.com"));
// toml uses empty strings not null
assert!(contact_de.phone.is_some_and(|x| x.is_empty()));

// Not there
let e = config.get(GenericTestEnum::NotThere);
assert!(e.is_none());
}
}

}

#[allow(non_camel_case_types)]
#[derive(ConfigFields)]
pub enum TestEnum {
Expand Down
Loading