| Crates.io | bakunin_config |
| lib.rs | bakunin_config |
| version | 1.0.0 |
| created_at | 2024-02-17 23:15:21.074778+00 |
| updated_at | 2025-09-08 23:28:39.747095+00 |
| description | Layered configuration for Rust applications. |
| homepage | |
| repository | https://github.com/Davidblkx/BakuninConfig |
| max_upload_size | |
| id | 1143598 |
| size | 150,188 |
Layered configuration for Rust applications.
BakuninConfig is a Rust library designed to provide a flexible and layered configuration system for applications. It allows you to define configuration values, load them from various sources (like files, environment variables, or in-memory). The library supports TOML and JSON formats. Values are aggregated from multiple named layers, allowing to write back to a specific layer. This is particularly useful to initialize configuration with default values and is very flexible by ignoring invalid values, so you can add new layers without breaking existing configurations.
In this example, we will create a configuration handler that looks for a global configuration file in the user's home dir or user config dir, a local configuration file in the current working directory, and environment variables prefixed with "MY_APP_". If the global configuration file does not exist, it creates it with default values.
use bakunin_config::{create_config, value_map};
let config = create_config!(".my-config" {
default: {
key1: "value1",
key2: 42,
key3: value_map! {
sub_key1: "sub_value1",
sub_key2: true
}
},
env: "MY_APP_",
"global": [UserHome, UserConfig] init: true,
"local": [WorkingDirectory]
});
let config_value = config.build_value(true).unwrap();
It also supports to write values back to the file system, so you can modify the configuration values and save them back to the file. Please read the documentation below for more details on how to use the library.
This library can be understood as 4 main domains:
This will be the main type you will work with. Can be created as any enum like:
use bakunin_config::Value;
use std::collections::HashMap;
let value = Value::Integer(42);
let value = Value::String("Hello, world!".into());
let value = Value::Array(vec![Value::Integer(1), Value::String("two".into())]);
let mut map = HashMap::new();
map.insert("key".to_string(), Value::String("value".into()));
let value = Value::Map(map);
When creating a map or array, you can use the value_map! and value_vec! macros for convenience:
use bakunin_config::{Value, value_map, value_vec};
let value = value_map! {
prop1: value_map! {
sub_prop1: "value1",
sub_prop2: 2
},
prop2: value_vec!["item1", 10, true]
};
Values can be easily converted to and from other types using the try_into method. For example, to convert a Value to an i64, you can do:
use bakunin_config::Value;
let value = Value::Integer(42);
let int_value: i64 = value.try_into().unwrap();
assert_eq!(int_value, 42);
Map values can be accessed using the get method, which returns an Value:
use bakunin_config::{Value, value_map};
let value = value_map! {
key1: "value1",
key2: value_map! {
sub_key1: "sub_value1",
sub_key2: 42
}
};
assert_eq!(value.get("key1").try_into_string().unwrap(), "value1");
assert_eq!(value.get("key2").get("sub_key1").try_into_string().unwrap(), "sub_value1");
assert_eq!(value.get("key2").get("sub_key2").try_into_i64().unwrap(), 42);
assert!(value.get("missing_key").get("other_key").is_none());
Vec values can be accessed using the at method, which returns an Option<Value>:
use bakunin_config::{Value, value_vec};
let value = value_vec!["item1", 42, true];
assert_eq!(value.at(0).try_into_string().unwrap(), "item1");
assert_eq!(value.at(1).try_into_i64().unwrap(), 42);
assert_eq!(value.at(2).try_into_bool().unwrap(), true);
assert!(value.at(3).is_none()); // Out of bounds
The FileFinder is used to locate configuration files in the filesystem. It can search for files in predefined OS folders or in custom folders, and it supports searching for files with specific extensions.
use bakunin_config::file_finder::FileFinder;
let finder = FileFinder::new(".my-config") // will search for files named ".my-config.[extension]"
.with_supported_extensions() // This depends on the flags enabled, by default it will search for ".toml"
.with_working_directory() // This will search in the current working directory
.with_user_home() // This will search in the user's home directory
.with_path("/path/to/custom/folder".to_string()); // This will search in the specified path
let search_result = finder.find_first(true).unwrap(); // Find the first file that exists or the first file that matches the criteria
Note: This examples assume you have flag toml (default) and json enabled, so it will search for files with
.tomland.jsonextensions.
Looks for the first file that exists in the specified directories, using the example, it would search for the following files:
It stops searching as soon as it finds the first file that exists. If no file is found, throws an error.
The method receives a boolean parameter "allow_missing", if true and no file is found, it will return "/<current_working_dir>/.my-config.toml"
Looks for the first file that exists in the specified directories, using the example, it would search for the following files:
It stops searching as soon as it finds the first file that exists. If no file is found, throws an error.
The method receives a boolean parameter "allow_missing", if true and no file is found, it will return "/path/to/custom/folder/.my-config.json"
Looks for the all files in the specified directories, using the example, it would search for the following files:
If no file is found, throws an error.
The method receives a boolean parameter "allow_missing", if true, returns all paths that match the criteria, even if they do not exist.
Looks for all files in the specified directories, using the example, it would search for the following files:
If no file is found, it will return the first file that matches the criteria.
It's a trait that defines how to read or load a configration value. It can be added to a BakuninConfig instance to provide a source of configuration values.
Three main implementations are provided:
use bakunin_config::{BakuninConfig, config_layer::MemoryConfigLayer, Value, value_map};
MemoryConfigLayer::new(value_map! {
key1: "value1",
key3: value_map! {
sub_key1: "sub_value1",
sub_key2: true
}
});
use bakunin_config::{BakuninConfig, config_layer::EnvironmentConfigLayer};
EnvironmentConfigLayer::new("MY_APP_"); // env var MY_APP_key1 will be used to override key1
use bakunin_config::{BakuninConfig, config_layer::FileConfigLayer, config_layer::handlers::TomlFileHandler};
use std::path::PathBuf;
FileConfigLayer::<TomlFileHandler>::new(PathBuf::from("path/to/config.toml"));
It's the main struct, allows you to create a configuration handler that can read configuration values from multiple layers. It can be used to create a configuration value that is a combination of multiple layers, such as files, environment variables, and default values. Layers are added with a name, and can later be accessed by it.
Example of a full configuration handler:
Looks for a global configuration file in the user's home directory, a local configuration file in the current working directory, and environment variables prefixed with "MY_APP_". If the global configuration file does not exist, it creates it with default values.
use bakunin_config::{BakuninConfig, Value, value_map, file_finder::FileFinder};
let default_value = value_map! {
key1: "value1",
key2: 42,
key3: value_map! {
sub_key1: "sub_value1",
sub_key2: true
}
};
let mut config = BakuninConfig::new()
.with_memory_layer("default", default_value.clone());
let global_config = FileFinder::new(".my-config")
.with_supported_extensions()
.with_user_home()
.with_user_config()
.find_first(true);
match global_config {
Ok(result) => {
config.add_file_layer("global", result.path.clone());
if !result.path.exists() {
// Create the file if it does not exist
if let Some(cfg) = config.get_layer("global") {
cfg.write_value(&default_value).unwrap();
}
}
}
_=> {
eprintln!("No global config found, using defaults");
}
}
let local_config = FileFinder::new(".my-config")
.with_supported_extensions()
.with_working_directory()
.find_first(false);
match local_config {
Ok(result) => {
config.add_file_layer("local", result.path.clone());
}
_=> {
eprintln!("No local config found, using defaults");
}
}
// add at end to override any previous values
config.add_environment_layer("env", "MY_APP_");
let config_value = config.build_value(true).unwrap();
previous example can be simplified using the create_config! macro:
use bakunin_config::{create_config, value_map};
let config = create_config!(".my-config" {
default: {
key1: "value1",
key2: 42,
key3: value_map! {
sub_key1: "sub_value1",
sub_key2: true
}
},
env: "MY_APP_",
"global": [UserHome, UserConfig] init: true,
"local": [WorkingDirectory]
});
let config_value = config.build_value(true).unwrap();
local and global are arbitarily named, and can be any name you want, they are just used to identify the layers. The create_config! macro will automatically create the layers and add them to the configuration handler ny prder. The values in [] match enum OSDirectory.
Since this is built on top of serde, You can try to deserialize any Value to a struct.
use bakunin_config::{create_config, value_map, value_vec};
#[derive(Debug, serde::Deserialize)]
struct Person {
name: String,
age: i32,
children: Vec<Child>,
}
#[derive(Debug, serde::Deserialize)]
struct Child {
name: String,
age: i32,
}
let config = create_config!(".my-config" {
default: {
key1: "value1",
key2: 42,
user: value_map! {
name: "John Doe",
age: 30,
children: value_vec![
value_map! { name: "Child1", age: 5 },
value_map! { name: "Child2", age: 3 }
]
}
},
env: "MY_APP_",
});
let config_value = config.build_value(true).unwrap();
let p: Person = config_value.get("user").deserialize().unwrap();
assert_eq!(p.name, "John Doe");
assert_eq!(p.age, 30);
assert_eq!(p.children.len(), 2);
assert_eq!(p.children[0].name, "Child1");
assert_eq!(p.children[0].age, 5);
assert_eq!(p.children[1].name, "Child2");
assert_eq!(p.children[1].age, 3);