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, and 'Default'able. /// /// (Trying to finding way to remove 'Default' constraint. However, 'Clone' will always be /// required.)

[derive(config_it::Template, Clone, Default)]

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

}); ```