# Quickfig
## Overview
**Quickfig** defines a simple API for reading config files in applications.
**Quickfig**'s goal is to replace a big chunk of boilerplate that you
most likely have/will have to write many times as an application developer.
This crate is *mostly* a wrapper around [Serde](https://serde.rs/) and crates that
implement Serde's de/serialization model, currently including:
- [serde_json](https://github.com/serde-rs/json)
- [toml](https://docs.rs/toml)
## Modules
* `quickfig::core` - Core exports for reading configuration files
* `quickfig::derive` - Derive macro for config fields
## Features
* `derive` - Enables the derive macro for ConfigFields
---
## Quickstart
```
$ cargo add quickfig --features derive
```
Imagine you want to read a user's config file at `/path/to/config.json` with:
```json
{ "id": 9 , "title": "foo" }
```
In your project:
```rust
use quickfig::derive::ConfigFields;
use quickfig::core::{
Config, Field,
config_types::JSON
};
// Define the fields you may want to read
#[derive(ConfigFields)]
enum MyFields {
#[keys("id", "ID")]
Id,
// A missing `keys` attribute defaults to (case-sensitive) variant name "Title"
Title,
}
fn main() -> Result<()> {
// create a "Config" instance, errors if file doesnt exist/no permissions/etc
let config = Config::<JSON>::open("/path/to/config.json").unwrap();
// Getting the id
let Some(id): Option<Vec<Field<'_, JSON>>> = config.get(MyFields::Id) else {
// Config didn't have "id" or "ID" key
return Err(String::from("Config must have an id or ID key"));
}
// Notice that id is a Vec<Field>. That is because the config could
// contain multiple matching keys, for example {"id": 1, "ID": 2}
// and you may want to handle that situation explicitely.
//
// However, most of the time you probably only want to accept 1
// matching key, and otherwise you want to error.
if id.only_one_key().is_err() {
return Err(String::from("Config must have an id or ID key, but not both"));
};
// Lastly, getting out the value:
// Reminder that the file contained {"id": 9, "title": "foo"}
let id_u8: Option<u8> = id.get_u8();
if id_u8.is_none() {
return Err(String::from("Config id must be a valid u8 integer"));
};
assert!(id.get_u8().is_some_and(|id| id == 9u8));
let id_string: Option<String> = id.get_string();
assert!(id_string.is_none());
}
```
---
## Cookbook
A few more usage examples to show features/recommended usage:
* `Config::open` requires a **FULL** path. A crate like [dirs](https://crates.io/crates/dirs) can be helpful to create these
```rust
use dirs::*;
use std::path::PathBuf;
let path_to_config: PathBuf = {
let mut home_dir = dirs::config_dir().unwrap();
home_dir.push("my_app/config.json");
home_dir
};
```
---
* 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<&serde_json::Value> = field.get_generic_inner();
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();
let f: Option<u16> = field.get_u16();
let f: Option<u32> = field.get_u32();
let f: Option<u64> = field.get_u64();
let f: Option<u128> = field.get_u128();
let f: Option<i8> = field.get_i8();
let f: Option<i16> = field.get_i16();
let f: Option<i32> = field.get_i32();
let f: Option<i64> = field.get_i64();
let f: Option<i128> = field.get_i128();
let f: Option<f32> = field.get_f32();
let f: Option<f64> = field.get_f64();
```
---
* 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 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"...
```
For that situation there is a helper method when creating a Config:
```rust
// List of paths you want to check (order does matter!)
let paths = vec![
"~/.config/MyApp/config.json",
"~/.MyApp/config.json",
"~/.local/share/MyApp/config.json"
];
// Search function that determines whether a path should be used or not.
// Return Some(path) to use a path or None to continue iterating.
// Will short-circuit first Some(path) return.
Some(path)
} else {
None
}
});
// Will try to create a Config from the first path that your function returns
// Some(path) on. Errors if there is no match or problem creating Config.
// If no search function is provided then default is same as search above.
let config: Result<Config<JSON>> = Config::<JSON>::open_first_match(
paths,
Some(search)
);
```