# 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<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
```rust
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.