# 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 ```rust #[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, pub test_3: Option, } fn less_than_100(value: &u32) -> Result<(), Box> { if *value < 100 { Ok(()) } else { Err("should be less than 100".into()) } } ``` This would generate something like ```rust impl Test { pub async fn load_test_1() -> std::result::Result { 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, ::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, ::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 { 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(name: &str) -> Result, 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>` Names are expressions of type &str, which may optionally return Err(Box) 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>` ## 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.