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:
serde
compatible archive formats to archive your data in your preferred way.As I'm not very good at writing English sentences, got some help from AI to write this README file. Thanks, robot!
```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, and /// 'Default'able. /// /// (Trying to finding way to remove 'Default' constraint. /// However, 'Clone' will always be required.)
struct MyConfig {
/// If you expose any field as config property, the
/// field must be marked with config_it
attribute.
#[configit]
stringfield: 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>,
}
// USAGE ///////////////////////////////////////////////////////////////////////////////////////
// 1. Storage // // Storage is basic and most important class to drive // the whole configit system. Before you can use any // of the features, you must create a storage instance. let (storage, drivertask) = configit::createstorage();
// [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::setvar("MYARRAY_VAR", "123");
// Let's get into async
local.rununtil(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.creategroup::
// 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;
});
```