use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::error::ApiError;
#[derive(Debug, Clone)]
pub struct FlagConfig {
pub environment: String,
}
impl Default for FlagConfig {
fn default() -> Self {
Self {
environment: "development".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlagContext {
pub user_id: Option<String>,
pub email: Option<String>,
pub attributes: HashMap<String, String>,
}
impl FlagContext {
pub fn new() -> Self {
Self {
user_id: None,
email: None,
attributes: HashMap::new(),
}
}
pub fn with_user(mut self, user_id: String) -> Self {
self.user_id = Some(user_id);
self
}
pub fn with_email(mut self, email: String) -> Self {
self.email = Some(email);
self
}
pub fn with_attribute(mut self, key: String, value: String) -> Self {
self.attributes.insert(key, value);
self
}
}
impl Default for FlagContext {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct FlagResult {
pub enabled: bool,
pub variant: Option<String>,
pub reason: String,
}
#[async_trait]
pub trait FlagProvider: Send + Sync {
async fn is_enabled(
&self,
flag_key: &str,
context: Option<&FlagContext>,
) -> Result<bool, ApiError>;
async fn get_variant(
&self,
flag_key: &str,
context: Option<&FlagContext>,
) -> Result<FlagResult, ApiError>;
async fn get_all_flags(
&self,
context: Option<&FlagContext>,
) -> Result<HashMap<String, bool>, ApiError>;
}
pub struct InMemoryFlagProvider {
flags: Arc<RwLock<HashMap<String, FlagDefinition>>>,
}
#[derive(Debug, Clone)]
struct FlagDefinition {
enabled: bool,
variant: Option<String>,
targeting: Option<FlagTargeting>,
}
#[derive(Debug, Clone)]
struct FlagTargeting {
user_ids: Vec<String>,
attributes: HashMap<String, Vec<String>>,
}
impl InMemoryFlagProvider {
pub fn new() -> Self {
Self {
flags: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn set_flag(&self, key: String, enabled: bool) {
let mut flags = self.flags.write().await;
flags.insert(
key,
FlagDefinition {
enabled,
variant: None,
targeting: None,
},
);
}
pub async fn set_flag_with_variant(&self, key: String, enabled: bool, variant: String) {
let mut flags = self.flags.write().await;
flags.insert(
key,
FlagDefinition {
enabled,
variant: Some(variant),
targeting: None,
},
);
}
pub async fn set_targeting(
&self,
key: String,
user_ids: Vec<String>,
attributes: HashMap<String, Vec<String>>,
) {
let mut flags = self.flags.write().await;
if let Some(flag) = flags.get_mut(&key) {
flag.targeting = Some(FlagTargeting {
user_ids,
attributes,
});
}
}
pub async fn remove_flag(&self, key: &str) {
let mut flags = self.flags.write().await;
flags.remove(key);
}
pub async fn clear_all(&self) {
let mut flags = self.flags.write().await;
flags.clear();
}
}
impl Default for InMemoryFlagProvider {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl FlagProvider for InMemoryFlagProvider {
async fn is_enabled(
&self,
flag_key: &str,
context: Option<&FlagContext>,
) -> Result<bool, ApiError> {
let flags = self.flags.read().await;
if let Some(flag) = flags.get(flag_key) {
if let Some(targeting) = &flag.targeting {
if let Some(ctx) = context {
if let Some(user_id) = &ctx.user_id {
if targeting.user_ids.contains(user_id) {
return Ok(true);
}
}
for (key, values) in &targeting.attributes {
if let Some(user_value) = ctx.attributes.get(key) {
if values.contains(user_value) {
return Ok(true);
}
}
}
return Ok(false);
}
}
Ok(flag.enabled)
} else {
Ok(false)
}
}
async fn get_variant(
&self,
flag_key: &str,
context: Option<&FlagContext>,
) -> Result<FlagResult, ApiError> {
let enabled = self.is_enabled(flag_key, context).await?;
let flags = self.flags.read().await;
let variant = flags
.get(flag_key)
.and_then(|f| f.variant.clone());
Ok(FlagResult {
enabled,
variant,
reason: if enabled {
"Flag is enabled".to_string()
} else {
"Flag is disabled".to_string()
},
})
}
async fn get_all_flags(
&self,
context: Option<&FlagContext>,
) -> Result<HashMap<String, bool>, ApiError> {
let flags = self.flags.read().await;
let mut result = HashMap::new();
for (key, _) in flags.iter() {
let enabled = self.is_enabled(key, context).await?;
result.insert(key.clone(), enabled);
}
Ok(result)
}
}
pub struct FeatureFlags {
provider: Box<dyn FlagProvider>,
config: FlagConfig,
}
impl FeatureFlags {
pub fn new(config: FlagConfig) -> Self {
Self {
provider: Box::new(InMemoryFlagProvider::new()),
config,
}
}
pub fn with_provider(provider: impl FlagProvider + 'static, config: FlagConfig) -> Self {
Self {
provider: Box::new(provider),
config,
}
}
pub async fn is_enabled(
&self,
flag_key: &str,
context: Option<&FlagContext>,
) -> Result<bool, ApiError> {
self.provider.is_enabled(flag_key, context).await
}
pub async fn get_variant(
&self,
flag_key: &str,
context: Option<&FlagContext>,
) -> Result<FlagResult, ApiError> {
self.provider.get_variant(flag_key, context).await
}
pub async fn get_all_flags(
&self,
context: Option<&FlagContext>,
) -> Result<HashMap<String, bool>, ApiError> {
self.provider.get_all_flags(context).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_feature_flags() {
let provider = InMemoryFlagProvider::new();
provider.set_flag("new_ui".to_string(), true).await;
provider.set_flag("beta_feature".to_string(), false).await;
let flags = FeatureFlags::with_provider(provider, FlagConfig::default());
assert!(flags.is_enabled("new_ui", None).await.unwrap());
assert!(!flags.is_enabled("beta_feature", None).await.unwrap());
assert!(!flags.is_enabled("unknown_flag", None).await.unwrap());
}
#[tokio::test]
async fn test_flag_targeting() {
let provider = InMemoryFlagProvider::new();
provider.set_flag("premium_feature".to_string(), false).await;
provider
.set_targeting(
"premium_feature".to_string(),
vec!["user-123".to_string()],
HashMap::new(),
)
.await;
let flags = FeatureFlags::with_provider(provider, FlagConfig::default());
let context = FlagContext::new().with_user("user-123".to_string());
assert!(flags.is_enabled("premium_feature", Some(&context)).await.unwrap());
let other_context = FlagContext::new().with_user("user-456".to_string());
assert!(!flags.is_enabled("premium_feature", Some(&other_context)).await.unwrap());
}
}