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:

As I'm not very good at writing English sentences, got some help from AI to write this README file. Thanks, robot!

Usage

``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 [configit::Template::defaultconfig`]

[derive(config_it::Template, Clone)]

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>,

/// 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 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::(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;

});

```