diff --git a/crates/env-var/README.md b/crates/env-var/README.md index 417b869..a5c926d 100644 --- a/crates/env-var/README.md +++ b/crates/env-var/README.md @@ -43,6 +43,17 @@ if is_feature_enabled { } ``` +The environment variable names can be customized by injecting a custom `Rename` implementation: + +```rust +/// Transforms env-flag-key to ENV_FLAG_KEY +fn underscore(flag_key: &str) -> Cow<'_, str> { + flag_key.replace("-", "_").to_uppercase().into() +} + +let provider = EnvVarProvider::new(underscore); +``` + ## Testing Run `cargo test` to execute tests. diff --git a/crates/env-var/src/lib.rs b/crates/env-var/src/lib.rs index 52c936f..06fac9b 100644 --- a/crates/env-var/src/lib.rs +++ b/crates/env-var/src/lib.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use async_trait::async_trait; use open_feature::{ provider::{FeatureProvider, ProviderMetadata, ResolutionDetails}, @@ -19,22 +21,30 @@ const METADATA: &str = "Environment Variables Provider"; /// /// The provider will return [`EvaluationResult::Err(EvaluationError)`] if the flag is not found or if the value is not of the expected type. #[derive(Debug)] -pub struct EnvVarProvider { +pub struct EnvVarProvider { metadata: ProviderMetadata, + rename: R, } /// Default implementation for the Environment Variables Provider impl Default for EnvVarProvider { fn default() -> Self { + Self::new(NoopRename) + } +} + +impl EnvVarProvider { + pub fn new(rename: R) -> Self { Self { metadata: ProviderMetadata::new(METADATA), + rename, } } } /// Implementation of the FeatureProvider trait for the Environment Variables Provider #[async_trait] -impl FeatureProvider for EnvVarProvider { +impl FeatureProvider for EnvVarProvider { /// Returns the provider metadata /// # Example /// ```rust @@ -71,7 +81,7 @@ impl FeatureProvider for EnvVarProvider { flag_key: &str, evaluation_context: &EvaluationContext, ) -> EvaluationResult> { - return evaluate_environment_variable(flag_key, evaluation_context); + return evaluate_environment_variable(&self.rename, flag_key, evaluation_context); } /// The 64-bit signed integer type. @@ -93,7 +103,7 @@ impl FeatureProvider for EnvVarProvider { flag_key: &str, evaluation_context: &EvaluationContext, ) -> EvaluationResult> { - return evaluate_environment_variable(flag_key, evaluation_context); + return evaluate_environment_variable(&self.rename, flag_key, evaluation_context); } /// A 64-bit floating point type @@ -120,7 +130,7 @@ impl FeatureProvider for EnvVarProvider { flag_key: &str, evaluation_context: &EvaluationContext, ) -> EvaluationResult> { - return evaluate_environment_variable(flag_key, evaluation_context); + return evaluate_environment_variable(&self.rename, flag_key, evaluation_context); } /// A UTF-8 encoded string. @@ -145,7 +155,7 @@ impl FeatureProvider for EnvVarProvider { flag_key: &str, evaluation_context: &EvaluationContext, ) -> EvaluationResult> { - return evaluate_environment_variable(flag_key, evaluation_context); + return evaluate_environment_variable(&self.rename, flag_key, evaluation_context); } /// Structured data, presented however is idiomatic in the implementation language, such as JSON or YAML. @@ -172,11 +182,13 @@ impl FeatureProvider for EnvVarProvider { /// assert_eq!(res.unwrap_err().code, EvaluationErrorCode::FlagNotFound); /// } /// ``` -fn evaluate_environment_variable( +fn evaluate_environment_variable( + rename: &R, flag_key: &str, _evaluation_context: &EvaluationContext, ) -> EvaluationResult> { - match std::env::var(flag_key) { + let env_var = rename.rename(flag_key); + match std::env::var(env_var.as_ref()) { Ok(value) => match value.parse::() { Ok(parsed_value) => EvaluationResult::Ok( ResolutionDetails::builder() @@ -208,6 +220,51 @@ fn error(evaluation_error_code: EvaluationErrorCode) -> EvaluationResult { .build()) } +/// Rename helps converting flag keys to environment variable names +/// +/// # Example +/// ```rust +/// fn underscore(flag_key: &str) -> std::borrow::Cow<'_, str> { +/// flag_key.replace("-", "_").to_uppercase().into() +/// } +/// +/// #[tokio::test] +/// async fn test_rename() { +/// let flag_key = "test-rename-key"; +/// let flag_value = std::f64::consts::PI.to_string(); +/// let provider = EnvVarProvider::new(underscore); +/// +/// std::env::set_var("TEST_RENAME_KEY", &flag_value); +/// +/// let result = provider +/// .resolve_float_value(flag_key, &EvaluationContext::default()) +/// .await; +/// assert!(result.is_ok()); +/// assert_eq!(result.unwrap().value, flag_value.parse::().unwrap()); +/// } +/// ``` +pub trait Rename: Send + Sync + 'static { + fn rename<'a>(&self, flag_key: &'a str) -> Cow<'a, str>; +} + +#[derive(Copy, Clone, Default, Debug)] +pub struct NoopRename; + +impl Rename for NoopRename { + fn rename<'a>(&self, flag_key: &'a str) -> Cow<'a, str> { + flag_key.into() + } +} + +impl Rename for F +where + F: Fn(&str) -> Cow<'_, str> + Send + Sync + 'static, +{ + fn rename<'a>(&self, flag_key: &'a str) -> Cow<'a, str> { + (self)(flag_key) + } +} + #[cfg(test)] mod tests { @@ -230,4 +287,30 @@ mod tests { assert!(provider.resolve_string_value("", &context).await.is_err()); assert!(provider.resolve_struct_value("", &context).await.is_err()); } + + #[test] + fn noop_rename_does_nothing() { + let flag_key = "test-key"; + assert_eq!(NoopRename.rename(flag_key), flag_key); + } + + fn underscore(flag_key: &str) -> Cow<'_, str> { + flag_key.replace("-", "_").to_uppercase().into() + } + + #[tokio::test] + async fn resolves_with_a_custom_rename() { + let provider = EnvVarProvider::new(underscore); + let context = EvaluationContext::default(); + + std::env::set_var("HELLO_WORLD", "true"); + + assert!( + provider + .resolve_bool_value("hello-world", &context) + .await + .unwrap() + .value + ); + } }