///|
/// A typed request used by the batch evaluator. The default value also acts as
/// the expected type, so a configuration mistake remains visible instead of
/// being silently converted.
pub(all) struct EvaluationRequest {
key : String
default_value : FlagValue
} derive(Debug, Eq)
///|
pub fn request(key : String, default_value : FlagValue) -> EvaluationRequest {
{ key, default_value }
}
///|
pub(all) struct BatchStats {
mut total : Int
mut successes : Int
mut defaults : Int
mut disabled : Int
mut target_misses : Int
mut rollout_misses : Int
mut type_mismatches : Int
} derive(Debug, Eq)
///|
pub(all) struct BatchResult {
evaluations : Array[ValueEvaluation]
stats : BatchStats
reason_counts : Map[String, Int]
} derive(Debug)
///|
fn same_value_kind(left : FlagValue, right : FlagValue) -> Bool {
match (left, right) {
(BoolValue(_), BoolValue(_)) => true
(StringValue(_), StringValue(_)) => true
(IntValue(_), IntValue(_)) => true
(DoubleValue(_), DoubleValue(_)) => true
_ => false
}
}
///|
fn[P : FeatureProvider] evaluate_request(
provider : P,
item : EvaluationRequest,
ctx : EvalContext,
) -> ValueEvaluation {
match provider.get_flag_definition(item.key) {
Some(flag) if !same_value_kind(flag.value, item.default_value) =>
{ flag_key: item.key, value: item.default_value, reason: "type_mismatch" }
_ => evaluate_value(provider, item.key, ctx, default=item.default_value)
}
}
///|
fn record_reason(stats : BatchStats, reason : String) -> Unit {
stats.total += 1
match reason {
"default" => stats.defaults += 1
"disabled" => stats.disabled += 1
"target_miss" => stats.target_misses += 1
"rollout_miss" => stats.rollout_misses += 1
"type_mismatch" => stats.type_mismatches += 1
_ => stats.successes += 1
}
}
///|
pub fn[P : FeatureProvider] evaluate_batch(
provider : P,
requests : Array[EvaluationRequest],
ctx : EvalContext,
) -> BatchResult {
let evaluations = []
let stats = {
total: 0,
successes: 0,
defaults: 0,
disabled: 0,
target_misses: 0,
rollout_misses: 0,
type_mismatches: 0,
}
let reasons : Map[String, Int] = Map([])
for item in requests {
let evaluation = evaluate_request(provider, item, ctx)
evaluations.push(evaluation)
record_reason(stats, evaluation.reason)
match reasons.get(evaluation.reason) {
Some(count) => reasons[evaluation.reason] = count + 1
None => reasons[evaluation.reason] = 1
}
}
{ evaluations, stats, reason_counts: reasons }
}
///|
pub fn BatchResult::reason_count(self : BatchResult, reason : String) -> Int {
self.reason_counts.get_or_default(reason, 0)
}
///|
pub fn BatchResult::summary(self : BatchResult) -> String {
let parts = []
for reason, count in self.reason_counts {
parts.push(reason + "=" + count.to_string())
}
self.stats.total.to_string() + " evaluations (" + parts.join(", ") + ")"
}
///|
pub fn BatchResult::has_failures(self : BatchResult) -> Bool {
self.stats.defaults > 0 || self.stats.type_mismatches > 0
}
///|
pub fn BatchResult::successful_ratio(self : BatchResult) -> Double {
if self.stats.total == 0 {
0.0
} else {
self.stats.successes.to_double() / self.stats.total.to_double()
}
}
///|
pub fn ValueEvaluation::is_match(self : ValueEvaluation) -> Bool {
self.reason == "static" ||
self.reason == "target_match" ||
self.reason == "rollout_match"
}
///|
pub fn ValueEvaluation::uses_default(self : ValueEvaluation) -> Bool {
!self.is_match()
}