# `config-it`
**Asynchronous Centralized Configuration Management for Rust**
`config-it` is an asynchronous library that offers centralized configuration management for Rust applications. Here are its key features:
- Define custom configuration templates using structs.
- Create multiple instances with different paths based on a single template.
- Receive notifications when your instance is updated.
- Use `serde` compatible archive formats to archive your data in your preferred way.
- Manage property-wise dirty flags for precise, responsive updates to your system.
> As I'm not very good at writing English sentences, got some help from AI to write this README file. Thanks, robot!
# Usage
- See #[example](tests/api.rs)
```rust
/// This is a 'Template' struct, which is minimal unit of
/// instantiation. Put required properties to configure
/// your program.
///
/// All 'Template' classes must be 'Clone'able. Its alternative default constructor will be
/// provided as [`config_it::Template::default_config`]
#[derive(config_it::Template, Clone)]
struct MyConfig {
/// If you expose any field as config property, the
/// field must be marked with `config_it` attribute.
#[config_it]
string_field: String,
/// You can also specify default value, or min/max
/// constraints for this field.
#[config_it(default = 3, min = 1, max = 5)]
int_field: i32,
/// This field will be aliased as 'alias'.
///
/// > **Warning** Don't use `~(tilde)` characters in
/// > alias name. In current implementation, `~` is
/// > used to indicate group object in archive
/// > representation during serialization.
#[config_it(alias = "alias")]
non_alias: f32,
/// Only specified set of values are allowed for
/// this field, however, default field can be
/// excluded from this set.
#[config_it(default = "default", one_of("a", "b", "c"))]
one_of_field: String,
/// Any 'serde' compatible type can be used as config field.
#[config_it]
c_string_type: Box<std::ffi::CStr>,
/// This will find value from environment variable
/// `MY_ENV_VAR`. Currently, only values that can be
/// `TryParse`d from `str` are supported.
///
/// Environment variables are imported when the
/// group is firstly instantiated.
/// i.e. call to `Storage::create_group`
#[config_it(env = "MY_ARRAY_VAR")]
env_var: i64,
/// Complicated default value are represented as expression.
#[config_it(default_expr = "[1,2,3,4,5].into()")]
array_init: Vec<i32>,
/// This field is not part of config_it system.
_not_part_of: (),
/// This field won't be imported or exported from
/// archiving operation
#[config_it(no_import, no_export)]
no_imp_exp: Vec<f64>,
/// `transient` flag is equivalent to `no_import` and
/// `no_export` flags.
#[config_it(transient)]
no_imp_exp_2: Vec<f64>,
/// Alternative attribute is allowed.
#[config]
another_attr: i32,
/// If any non-default-able but excluded field exists, you can provide
/// your own default value to make this template default-constructible.
#[nocfg = "std::num::NonZeroUsize::new(1).unwrap()"]
_nonzero_var: std::num::NonZeroUsize,
}
// USAGE ///////////////////////////////////////////////////////////////////////////////////////
// 1. Storage
//
// Storage is basic and most important class to drive
// the whole config_it system. Before you can use any
// of the features, you must create a storage instance.
let (storage, driver_task) = config_it::create_storage();
// `[config_it::create_storage]` returns a tuple of
// `(Storage, Task)`. `Storage` is the handle to the
// storage, and `Task` is the driver task that must
// be spawned to drive the storage operations(actor).
// You can spawn the task using any async runtime.
//
// Basically, config_it is designed to be used with
// async runtime, we're run this example under async
// environment.
let mut local = futures::executor::LocalPool::new();
let spawn = local.spawner();
// Storage driver task must be running somewhere.
use futures::task::SpawnExt;
spawn.spawn(driver_task).unwrap();
// before starting this, let's set environment variable to see if it works.
std::env::set_var("MY_ARRAY_VAR", "123");
// Let's get into async
local.run_until(async {
// 2. Groups and Templates
//
// A group is an instance of a template. You can
// create multiple groups from a single template.
// Each group has its own set of properties, and
// can be configured independently.
//
// When instantiating a group, you must provide a
// path to the group. Path is a list of short string
// tokens, which is used to identify the group. You
// can use any string as path, but it's recommended
// to use a short string, which does not contain any
// special characters. (Since it usually encoded as a
// key of a key-value store of some kind of data
// serialization formats, such as JSON, YAML, etc.)
let path = &["path", "to", "my", "group"];
let mut group = storage.create_group::<MyConfig>(path).await.unwrap();
// Note, duplicated path name is not allowed.
assert!(storage.create_group::<MyConfig>(path).await.is_err());
// `update()` call to group, will check for asynchronously
// queued updates, and apply changes to the group instance.
// Since this is the first call to update,
//
// You can understand `update()` as clearing dirty flag.
assert!(group.update() == true);
// After `update()`, as long as there's no new update,
// `update()` will return false.
assert!(group.update() == false);
// Every individual properties has their own dirty flag.
assert!(true == group.check_elem_update(&group.array_init));
assert!(true == group.check_elem_update(&group.c_string_type));
assert!(true == group.check_elem_update(&group.env_var));
assert!(true == group.check_elem_update(&group.no_imp_exp));
assert!(true == group.check_elem_update(&group.no_imp_exp_2));
assert!(true == group.check_elem_update(&group.non_alias));
assert!(true == group.check_elem_update(&group.int_field));
assert!(true == group.check_elem_update(&group.one_of_field));
assert!(true == group.check_elem_update(&group.string_field));
assert!(false == group.check_elem_update(&group.array_init));
assert!(false == group.check_elem_update(&group.c_string_type));
assert!(false == group.check_elem_update(&group.env_var));
assert!(false == group.check_elem_update(&group.no_imp_exp));
assert!(false == group.check_elem_update(&group.no_imp_exp_2));
assert!(false == group.check_elem_update(&group.int_field));
assert!(false == group.check_elem_update(&group.non_alias));
assert!(false == group.check_elem_update(&group.one_of_field));
assert!(false == group.check_elem_update(&group.string_field));
// Any field that wasn't marked as 'config_it' attribute will not be part of
// config_it system.
// // Invoking next line will panic:
// group.check_elem_update(&group.nothing_here);
// 3. Properties
//
// You can access each field of the group instance in common deref manner.
assert!(group.string_field == "");
assert!(group.array_init == &[1, 2, 3, 4, 5]);
assert!(group.env_var == 123);
// 4. Importing and Exporting
//
// You can export the whole storage using 'Export' method.
// (currently, there is no way to export a specific group
// instance. To separate groups into different archiving
// categories, you can use multiple storage instances)
let archive = storage.export(Default::default()).await.unwrap();
// `config_it::Archive` implements `serde::Serialize` and
// `serde::Deserialize`. You can use it to serialize/
// deserialize the whole storage.
let yaml = serde_yaml::to_string(&archive).unwrap();
let json = serde_json::to_string_pretty(&archive).unwrap();
println!("{}", yaml);
// OUTPUT:
//
// ~path: # all path tokens of group hierarchy are prefixed with '~'
// ~to: # (in near future, this will be made customizable)
// ~my:
// ~group:
// alias: 0.0
// array_init:
// - 1
// - 2
// - 3
// - 4
// - 5
// c_string_type: []
// env_var: 0
// int_field: 3
// one_of_field: default
// string_field: ''
//
println!("{}", json);
// {
// "~path": {
// "~to": {
// "~my": {
// "~group": {
// "alias": 0.0,
// "array_init": [
// 1,
// 2,
// 3,
// 4,
// 5
// ],
// "c_string_type": [],
// "env_var": 0,
// "int_field": 3,
// "one_of_field": "default",
// "string_field": ""
// }
// }
// }
// }
// }
// Importing is similar to exporting. You can import a
// whole storage from an archive. For this, you should
// create a new archive. Archive can be created using serde either.
let yaml = r##"
~path:
~to:
~my:
~group:
alias: 3.14
array_init:
- 1
- 145
int_field: 3 # If there's no change, it won't be updated.
# This behavior can be overridden by import options.
env_var: 59
one_of_field: "hello" # This is not in the 'one_of' list...
"##;
let archive: config_it::Archive = serde_yaml::from_str(yaml).unwrap();
storage.import(archive, Default::default()).await.unwrap();
storage.fence().await; // Since import operation is asynchronous, you must fence
// to make sure all changes are applied.
// Now, let's check if the changes are applied.
assert!(group.update() == true);
// Data update is regardless of the individual properties' dirty flag control.
// Data is modified only when `group.update()` is called.
assert!(group.non_alias == 3.14); // That was aliased property
assert!(group.array_init == [1, 145]);
assert!(group.env_var == 59);
assert!(group.int_field == 3); // No change
assert!(group.one_of_field == "default"); // Not in the 'one_of' list. no change.
// Only updated properties' dirty flag will be set.
assert!(true == group.check_elem_update(&group.non_alias));
assert!(true == group.check_elem_update(&group.array_init));
assert!(true == group.check_elem_update(&group.env_var));
// Since this property had no change, dirty flag was not set.
assert!(false == group.check_elem_update(&group.int_field));
// Since this property was not in the 'one_of' list, it was ignored.
assert!(false == group.check_elem_update(&group.one_of_field));
// These were simply not in the list.
assert!(false == group.check_elem_update(&group.c_string_type));
assert!(false == group.check_elem_update(&group.no_imp_exp));
assert!(false == group.check_elem_update(&group.no_imp_exp_2));
assert!(false == group.check_elem_update(&group.string_field));
// 5. Other features
// 5.1. Watch update
// When group is possible to updated, you can be notified
// through asynchronous channel. This is useful when you
// want to immediately response to any configuration updates.
let mut monitor = group.watch_update();
assert!(false == monitor.try_recv().is_ok());
let archive: config_it::Archive = serde_yaml::from_str(yaml).unwrap();
storage
.import(
archive,
config_it::ImportOptions {
apply_as_patch: false, // This will force all properties to be updated.
..Default::default()
},
)
.await
.unwrap();
assert!(true == monitor.recv().await.is_ok());
assert!(group.update());
// 5.2. Commit
// Any property value changes on group is usually local,
// however, if you want to
// archive those changes, you can commit it.
group.int_field = 15111; // This does not affected by
// constraint and visible from export,
// however, in next time you import
// it from exported archive,
// its constraint will be applied.
// If you set the second boolean parameter 'true', it will
// be notified to 'monitor'
group.commit_elem(&group.int_field, false);
let archive = storage.export(Default::default()).await.unwrap();
assert!(
archive.find_path(path.iter().map(|x| *x)).unwrap().values["int_field"]
.as_i64()
.unwrap()
== 15111
);
// As the maximum value of 'int_field' is 5, in next import, it will be 5.
storage
.import(
archive,
config_it::ImportOptions {
// Since we create patch from archive content ...
// Need to forcibly invalidate all
apply_as_patch: false,
..Default::default()
},
)
.await
.unwrap();
storage.fence().await;
assert!(group.update());
assert!(group.int_field == 5);
// 5.3. Monitor
//
// All events to update storage can be monitored though
// this channel.
//
// As this is advanced topic, and currently its design
// is not finalized, just give a look for fun and don't
// use it in production.
let _ch = storage.monitor_open_replication_channel().await;
});
```