4 releases

0.1.3 Dec 29, 2023
0.1.2 Dec 28, 2023
0.1.1 Dec 27, 2023
0.1.0 Dec 27, 2023

#26 in #passed

Download history 71/week @ 2024-04-21 147/week @ 2024-04-28 42/week @ 2024-05-05 40/week @ 2024-05-12 19/week @ 2024-05-19 26/week @ 2024-05-26 21/week @ 2024-06-02 23/week @ 2024-06-09 55/week @ 2024-06-16 56/week @ 2024-06-23 121/week @ 2024-06-30 71/week @ 2024-07-07 13/week @ 2024-07-14 17/week @ 2024-07-21

222 downloads per month
Used in yacm

Apache-2.0

25KB
336 lines

yacm

Yacm is yet another config macro.

Why?

Given the Long history of Yet Another projects, not even it's name is original. However, I was not finding what I wanted in a config macro and was tired of rewriting the same dull code to load structures that I subsequently passed into things that need configured.

Project Goals

  1. Dry config code
  2. Able to load config from async sources (e.g. AWS SSM Parameters)
  3. Fail fast when config is bad
  4. Make how config is loaded highly configurable

The general idea

#[derive(Yacm)]
#[yacm(prefix = "derived", name_provider = yacm::name_provider::screaming_snake_case, default_loader = yacm::env::read_env)]
struct Test {
    #[yacm(
        name = "TEST_TEST_ONE",
        default = 142u32,
        validator = less_than_100
    )]
    pub test_1: u32,
    #[yacm(
        name = Test::load_test_1().await?,
        loader = yacm::env::read_env,
    )]
    pub test_2: Option<String>,
    pub test_3: Option<String>,
}


fn less_than_100(value: &u32) -> Result<(), Box<dyn std::error::Error + Sync + Send>> {
    if *value < 100 {
        Ok(())
    } else {
        Err("should be less than 100".into())
    }
}

This would generate something like

impl Test {         
    pub async fn load_test_1() -> std::result::Result<u32,::yacm::Error> {             
        let name = "TEST_TEST_ONE";             
        let mut value = yacm::env::read_env(&name).await;             
        if let Ok(None) = value { value = Ok(Some(142u32.into())) }             
        if let Ok(v) = value.as_ref() {                 
            if let Err(e) = less_than_100(v) { 
                return Err(::yacm::Error::ValidationError(name.to_string(), e)); 
            }             
        };             
        match value {                 
            Ok(Some(v)) => Ok(v),                 
            Ok(None) => Err(::yacm::Error::NotFound(name.to_string())),                 
            Err(e) => Err(e),             
        }         
    }         
    pub async fn load_test_2() -> std::result::Result<Option<String>, ::yacm::Error> {             
        let name = Test::load_test_1().await?;             
        let mut value = yacm::env::read_env(&name).await;             
        value         
    }         
    pub async fn load_test_3() -> std::result::Result<Option<String>, ::yacm::Error> {             
        let name = yacm::name_provider::screaming_snake_case("test_3", Some("derived")).await
            .map_err(|e| ::yacm::Error::Read("test_3".to_string(), e))?;             
        let mut value = yacm::env::read_env(&name).await;             
        value         
    }         
    pub async fn load() -> Result<Self, ::yacm::Error> { 
        Ok(Self { 
            test_1: Self::load_test_1().await?, 
            test_2: Self::load_test_2().await?, 
            test_3: Self::load_test_3().await? 
        }) 
    }     
}

Using Yacm

yacm derives code to load both individual fields of a struct and the entire struct based on the specified or default loader for each field. Where a load should have a signature like:

pub async fn foo_loader<T>(name: &str) -> Result<Option<T>, yacm::Error>

The returned yacm::Error should either be yacm::Error::Read or yacm::Error::Parse.

The name passed into the load come from either a name_provider or a field specific name. name_providers can be specified for an individual field or for the entire struct, where the yacm default is ::yacm::env::screaming_snake_case.

Name_providers should have a signature like:

pub async fn foo(field: &str, prefix: Option<&str>) -> Result<String, Box<dyn std::error::Error + Sync + Send>>

Names are expressions of type &str, which may optionally return Err(Box<dyn std::error::Error + Sync + Send>)

For example name be a literal &str such as "FOO_BAR_SAMPLE" or can be a bit more complicated such as &format!("{}.sample",Config::load_env().await?)

Struct Level Attributes:

#[yacm(prefix = "foo", name_provider = path::custom_name_convention, default_loader = path::custom_loader )]

  • prefix: An optional String representing the prefix for the configuration struct, which would be passed to any name providers when generating a name to use when loading the config. For example one might specify #[yacm(prefix = "sample")] so that fields foo and bar might be loaded from environment variables named SAMPLE_FOO AND SAMPLE_BAR.

  • name_provider: An optional path to a function used as the default name_provider for each field in the struct.

  • default_loader: An optional path overriding the yacm default of ::yacm::env::read_env

Field level attributes

#[yacm(name = "bar", name_provider = .., loader = path::custom_loader, default = 42, validator = path::custom_validator )]

  • name: An optional expression of the exact &str to use as name

  • name_provider: An optional path to a function used as the default name_provider for each field in the struct.

  • loader: An optional path overriding the struct default or yacm default of ::yacm::env::read_env

  • default: An optional default, which should have a type matching the field type

  • validator: An optional path to a validator, which should have a signature like fn custom_val(value: &FieldType) -> Result<(), Box<dyn std::error::Error + Sync + Send>>

Road Map

  1. Use it in a few of my own projects until I have some confidence in the interface
  2. Add Testing
  3. Document
  4. Maybe make async a feature, instead of the default (I don't need the non async, but some might)

Why you shouldn't use it yet

It is brand spanking new and I have not even used it all the places I intend to yet. i.e. interface is expected to be highly volitional.

Feed back is welcome (yes, even at this early stage)

While I've been coding for 40 years, I'm new to Rust, and I'm especially new to Meta Programming in Rust. Anything from suggestions for incremental improvement, to links to crates I should be using instead of wasting my time on yet another config macro are welcome.

Dependencies

~3.5MB
~64K SLoC