use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum FlagVal {
Bool(bool),
Number(f64),
Str(String),
Json(String),
}
impl FlagVal {
fn off_value(&self) -> FlagVal {
match self {
FlagVal::Bool(_) => FlagVal::Bool(false),
FlagVal::Number(_) => FlagVal::Number(0.0),
FlagVal::Str(_) => FlagVal::Str(String::new()),
FlagVal::Json(_) => FlagVal::Json("null".to_string()),
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct EvalFlag {
pub flag_id: String,
pub flag_key: String,
pub project_id: String,
pub environment: String,
pub value: FlagVal,
pub enabled: bool,
pub rollout_percentage: i32,
pub rollout_context_key: String,
pub revision: i64,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct EvalContext {
pub project_id: String,
pub environment: String,
pub attributes: HashMap<String, String>,
}
fn fnv1a64(bytes: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for &b in bytes {
hash ^= u64::from(b);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
pub(crate) fn rollout_bucket(flag_key: &str, subject: &str) -> i32 {
let combined = format!("{flag_key}:{subject}");
(fnv1a64(combined.as_bytes()) % 100) as i32
}
fn scope_rank(flag: &EvalFlag, ctx: &EvalContext) -> i32 {
let env_ok = flag.environment.is_empty() || flag.environment == ctx.environment;
let proj_ok = flag.project_id.is_empty() || flag.project_id == ctx.project_id;
if !env_ok || !proj_ok {
return -1;
}
let mut rank = 0;
if !flag.environment.is_empty() {
rank += 2;
}
if !flag.project_id.is_empty() {
rank += 1;
}
rank
}
pub(crate) fn resolve_flag<'a>(
candidates: &'a [EvalFlag],
ctx: &EvalContext,
) -> Option<&'a EvalFlag> {
candidates
.iter()
.filter_map(|f| {
let rank = scope_rank(f, ctx);
(rank >= 0).then_some((rank, f))
})
.max_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.flag_id.cmp(&b.1.flag_id)))
.map(|(_, f)| f)
}
pub(crate) fn evaluate_flag(flag: &EvalFlag, ctx: &EvalContext) -> FlagVal {
if !flag.enabled {
return flag.value.off_value();
}
let pct = flag.rollout_percentage.clamp(0, 100);
if pct >= 100 {
return flag.value.clone();
}
if pct <= 0 {
return flag.value.off_value();
}
let subject = ctx
.attributes
.get(&flag.rollout_context_key)
.map(String::as_str)
.unwrap_or("");
if rollout_bucket(&flag.flag_key, subject) < pct {
flag.value.clone()
} else {
flag.value.off_value()
}
}
pub(crate) fn bump_revision(current: i64) -> i64 {
current.saturating_add(1)
}