pub mod types;
mod client;
mod user;
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
use client::ApiClient;
pub use types::{Config, EvaluationEvent, FlagData, FlagSegment, PerformanceEvent, TraitCondition};
use types::WsMessage;
use user::get_user_id;
struct SdkState {
client: ApiClient,
flags: HashMap<String, FlagData>,
traits: HashMap<String, String>,
base_url: String,
public_token: String,
}
static SDK: Mutex<Option<SdkState>> = Mutex::new(None);
fn hash_key(input: &str) -> i32 {
let mut h: i32 = 0;
for ch in input.chars() {
h = h.wrapping_shl(5).wrapping_sub(h).wrapping_add(ch as i32);
}
(h.wrapping_abs()) % 100
}
fn match_condition(cond: &TraitCondition, traits: &HashMap<String, String>) -> bool {
match cond.condition_type.as_str() {
"match" => {
let key = cond.trait_key.as_deref().unwrap_or("");
let expected = cond.trait_value.as_deref().unwrap_or("");
traits.get(key).map_or(false, |v| v == expected)
}
"and" => cond
.conditions
.as_ref()
.map_or(true, |cs| cs.iter().all(|c| match_condition(c, traits))),
"or" => cond
.conditions
.as_ref()
.map_or(false, |cs| cs.iter().any(|c| match_condition(c, traits))),
_ => false,
}
}
fn merge_traits(
base: &HashMap<String, String>,
overrides: Option<&HashMap<String, String>>,
) -> HashMap<String, String> {
let mut merged = base.clone();
if let Some(o) = overrides {
for (k, v) in o {
merged.insert(k.clone(), v.clone());
}
}
merged
}
pub fn init(config: Config) -> Result<(), String> {
let base_url = config.base_url.trim_end_matches('/').to_string();
let client = ApiClient::new(base_url.clone(), config.public_token.clone());
let resp = client.fetch_flags(
Some(get_user_id().to_string()),
Some(config.traits.clone()),
)?;
let mut flag_map = HashMap::new();
for f in resp.flags {
flag_map.insert(f.name.clone(), f);
}
let mut sdk = SDK.lock().unwrap();
*sdk = Some(SdkState {
client,
flags: flag_map,
traits: config.traits,
base_url,
public_token: config.public_token,
});
Ok(())
}
pub fn set_traits(traits: HashMap<String, String>) {
if let Ok(mut sdk) = SDK.lock() {
if let Some(ref mut state) = *sdk {
state.traits = traits;
}
}
}
pub fn flag(name: &str, trait_overrides: Option<&HashMap<String, String>>) -> bool {
let sdk = SDK.lock().unwrap();
let state = match sdk.as_ref() {
Some(s) => s,
None => return false,
};
let f = match state.flags.get(name) {
Some(f) => f,
None => return false,
};
if !f.enabled {
return false;
}
let traits = merge_traits(&state.traits, trait_overrides);
let user_id = get_user_id();
if let Some(ref segments) = f.segments {
for segment in segments {
if match_condition(&segment.condition, &traits) {
if segment.sticky {
if let Some(ref sticky) = f.sticky_assignments {
if sticky.contains(&segment.segment_id) {
return true;
}
}
}
let bucket = hash_key(&format!("{}:{}:{}", name, user_id, segment.segment_id));
return bucket < segment.rollout_percentage;
}
}
}
if f.rollout_percentage >= 100 {
return true;
}
if f.rollout_percentage <= 0 {
return false;
}
let bucket = hash_key(&format!("{}:{}", name, user_id));
bucket < f.rollout_percentage
}
pub fn measure<T, F1: FnOnce() -> T, F2: FnOnce() -> T>(
name: &str,
enabled_fn: F1,
disabled_fn: F2,
trait_overrides: Option<&HashMap<String, String>>,
) -> T {
let is_enabled = flag(name, trait_overrides);
let start = std::time::Instant::now();
let result = if is_enabled {
enabled_fn()
} else {
disabled_fn()
};
let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
let branch = if is_enabled { "enabled" } else { "disabled" };
let sdk = SDK.lock().unwrap();
if let Some(ref state) = *sdk {
let traits = merge_traits(&state.traits, trait_overrides);
let _event = PerformanceEvent {
event_type: "performance".to_string(),
flag_name: name.to_string(),
duration_ms,
branch: branch.to_string(),
traits,
user_id: get_user_id().to_string(),
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64,
};
}
result
}
pub fn report(
error_msg: &str,
stack: Option<&str>,
flag_name: Option<&str>,
trait_overrides: Option<&HashMap<String, String>>,
) {
let sdk = SDK.lock().unwrap();
let state = match sdk.as_ref() {
Some(s) => s,
None => return,
};
let traits = merge_traits(&state.traits, trait_overrides);
ApiClient::report_error(
state.base_url.clone(),
state.public_token.clone(),
flag_name.unwrap_or("unknown").to_string(),
error_msg.to_string(),
stack.map(|s| s.to_string()),
Some(get_user_id().to_string()),
Some(traits),
);
}
pub fn close() {
let mut sdk = SDK.lock().unwrap();
*sdk = None;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_key_range() {
for input in &["", "hello", "flag:user:seg", "test-flag:user-123"] {
let h = hash_key(input);
assert!(h >= 0 && h < 100, "hash_key({input}) = {h} out of range");
}
}
#[test]
fn test_hash_key_deterministic() {
let a = hash_key("test-flag:user-123");
let b = hash_key("test-flag:user-123");
assert_eq!(a, b);
}
#[test]
fn test_match_condition_match_type() {
let cond = TraitCondition {
condition_type: "match".to_string(),
trait_key: Some("env".to_string()),
trait_value: Some("prod".to_string()),
conditions: None,
};
let mut traits = HashMap::new();
traits.insert("env".to_string(), "prod".to_string());
assert!(match_condition(&cond, &traits));
traits.insert("env".to_string(), "staging".to_string());
assert!(!match_condition(&cond, &traits));
}
#[test]
fn test_match_condition_and() {
let cond = TraitCondition {
condition_type: "and".to_string(),
trait_key: None,
trait_value: None,
conditions: Some(vec![
TraitCondition {
condition_type: "match".to_string(),
trait_key: Some("env".to_string()),
trait_value: Some("prod".to_string()),
conditions: None,
},
TraitCondition {
condition_type: "match".to_string(),
trait_key: Some("plan".to_string()),
trait_value: Some("enterprise".to_string()),
conditions: None,
},
]),
};
let mut traits = HashMap::new();
traits.insert("env".to_string(), "prod".to_string());
traits.insert("plan".to_string(), "enterprise".to_string());
assert!(match_condition(&cond, &traits));
traits.insert("plan".to_string(), "free".to_string());
assert!(!match_condition(&cond, &traits));
}
#[test]
fn test_match_condition_or() {
let cond = TraitCondition {
condition_type: "or".to_string(),
trait_key: None,
trait_value: None,
conditions: Some(vec![
TraitCondition {
condition_type: "match".to_string(),
trait_key: Some("env".to_string()),
trait_value: Some("prod".to_string()),
conditions: None,
},
TraitCondition {
condition_type: "match".to_string(),
trait_key: Some("env".to_string()),
trait_value: Some("staging".to_string()),
conditions: None,
},
]),
};
let mut traits = HashMap::new();
traits.insert("env".to_string(), "prod".to_string());
assert!(match_condition(&cond, &traits));
traits.insert("env".to_string(), "dev".to_string());
assert!(!match_condition(&cond, &traits));
}
#[test]
fn test_match_condition_unknown() {
let cond = TraitCondition {
condition_type: "unknown".to_string(),
trait_key: None,
trait_value: None,
conditions: None,
};
assert!(!match_condition(&cond, &HashMap::new()));
}
#[test]
fn test_flag_before_init() {
assert!(!flag("anything", None));
}
#[test]
fn test_merge_traits() {
let mut base = HashMap::new();
base.insert("a".to_string(), "1".to_string());
base.insert("b".to_string(), "2".to_string());
let mut overrides = HashMap::new();
overrides.insert("b".to_string(), "3".to_string());
overrides.insert("c".to_string(), "4".to_string());
let merged = super::merge_traits(&base, Some(&overrides));
assert_eq!(merged.get("a").unwrap(), "1");
assert_eq!(merged.get("b").unwrap(), "3");
assert_eq!(merged.get("c").unwrap(), "4");
}
}