///|
pub(all) struct Attribute {
key : String
value : String
} derive(Eq, Debug)
///|
pub(all) struct EvalContext {
subject : String
environment : String
attributes : Array[Attribute]
} derive(Eq, Debug)
///|
pub(all) enum Operator {
Eq
Ne
Gt
Lt
Ge
Le
Contains
In
NotIn
} derive(Eq, Debug)
///|
pub(all) enum Rule {
Allow(String)
Deny(String)
When(String, Operator, String)
AllowVariant(String, String)
WhenVariant(String, Operator, String, String)
} derive(Eq, Debug)
///|
pub(all) struct Variant {
name : String
weight : Int
} derive(Eq, Debug)
///|
pub(all) struct Flag {
name : String
enabled : Bool
default_enabled : Bool
rollout : Int
salt : String
rules : Array[Rule]
is_multivariate : Bool
variants : Array[Variant]
default_variant : String
} derive(Eq, Debug)
///|
pub(all) struct Decision {
flag : String
enabled : Bool
variant : String
reason : String
audit_trail : Array[String]
} derive(Eq, Debug)
///|
pub(all) struct FlagDiagnostic {
kind : String
flag : String
message : String
} derive(Eq, Debug)
///|
pub(all) enum FlagError {
ParseError(Int, String)
UnknownFlag(Int, String)
} derive(Eq, Debug)
///|
pub fn evaluate(flag : Flag, context : EvalContext) -> Decision {
let audit : Array[String] = []
audit.push("Evaluating flag " + flag.name)
if !flag.enabled {
audit.push(
"Flag is disabled, returning default variant " + flag.default_variant,
)
let def_enabled = flag.default_enabled
return {
flag: flag.name,
enabled: def_enabled,
variant: flag.default_variant,
reason: "flag disabled",
audit_trail: audit,
}
}
// 1. Evaluate Deny rules first
for rule in flag.rules {
match rule {
Deny(subject) =>
if subject == context.subject {
audit.push("Subject denied by rule: " + subject)
let variant = if flag.is_multivariate {
flag.default_variant
} else {
"off"
}
return {
flag: flag.name,
enabled: false,
variant,
reason: "subject denied by rule",
audit_trail: audit,
}
}
_ => ()
}
}
// 2. Evaluate Allow rules next
for rule in flag.rules {
match rule {
Allow(subject) =>
if subject == context.subject {
audit.push("Subject allowed by rule: " + subject)
let variant = if flag.is_multivariate {
if flag.variants.length() > 0 {
flag.variants[0].name
} else {
"on"
}
} else {
"on"
}
return {
flag: flag.name,
enabled: true,
variant,
reason: "subject allowed by rule",
audit_trail: audit,
}
}
AllowVariant(subject, variant) =>
if subject == context.subject {
audit.push("Subject allowed by rule for variant " + variant)
return {
flag: flag.name,
enabled: true,
variant,
reason: "subject allowed variant rule",
audit_trail: audit,
}
}
_ => ()
}
}
// 3. Evaluate Activation Conditions (When rules)
for rule in flag.rules {
match rule {
When(key, op, val) => {
let matched = match lookup_context(context, key) {
Some(ctx_val) => eval_operator(ctx_val, op, val)
None => false
}
if !matched {
audit.push(
"Activation condition not matched: " +
key +
" " +
op_to_str(op) +
" " +
val,
)
let variant = if flag.is_multivariate {
flag.default_variant
} else {
"off"
}
return {
flag: flag.name,
enabled: false,
variant,
reason: "condition not matched",
audit_trail: audit,
}
}
audit.push(
"Activation condition matched: " +
key +
" " +
op_to_str(op) +
" " +
val,
)
}
_ => ()
}
}
// 4. Evaluate WhenVariant rules
for rule in flag.rules {
match rule {
WhenVariant(key, op, val, variant) => {
let matched = match lookup_context(context, key) {
Some(ctx_val) => eval_operator(ctx_val, op, val)
None => false
}
if matched {
audit.push(
"Condition variant matched rule: " +
key +
" " +
op_to_str(op) +
" " +
val +
" -> " +
variant,
)
let is_enabled = variant != flag.default_variant
return {
flag: flag.name,
enabled: is_enabled,
variant,
reason: "condition variant matched rule",
audit_trail: audit,
}
}
}
_ => ()
}
}
// 5. Percentage Rollout evaluation
if flag.is_multivariate {
let score = bucket(context.subject, flag.name + ":" + flag.salt)
audit.push("Subject bucket score: " + score.to_string())
let mut sum = 0
for v in flag.variants {
sum += v.weight
if score < sum {
audit.push("Subject falls into variant bucket: " + v.name)
let is_enabled = v.name != flag.default_variant
return {
flag: flag.name,
enabled: is_enabled,
variant: v.name,
reason: "rollout bucket selection",
audit_trail: audit,
}
}
}
audit.push(
"Subject score outside all variant buckets, returning default variant " +
flag.default_variant,
)
return {
flag: flag.name,
enabled: false,
variant: flag.default_variant,
reason: "rollout bucket selection fallback",
audit_trail: audit,
}
} else {
if flag.rollout <= 0 {
audit.push("Boolean rollout is 0%, flag disabled")
return {
flag: flag.name,
enabled: false,
variant: "off",
reason: "rollout bucket closed",
audit_trail: audit,
}
}
if flag.rollout >= 100 {
audit.push("Boolean rollout is 100%, flag enabled")
return {
flag: flag.name,
enabled: true,
variant: "on",
reason: "rollout bucket opened",
audit_trail: audit,
}
}
let score = bucket(context.subject, flag.name + ":" + flag.salt)
audit.push(
"Subject bucket score: " +
score.to_string() +
", rollout: " +
flag.rollout.to_string(),
)
if score < flag.rollout {
return {
flag: flag.name,
enabled: true,
variant: "on",
reason: "rollout bucket opened",
audit_trail: audit,
}
} else {
return {
flag: flag.name,
enabled: false,
variant: "off",
reason: "rollout bucket closed",
audit_trail: audit,
}
}
}
}
///|
pub fn evaluate_all(
flags : Array[Flag],
context : EvalContext,
) -> Array[Decision] {
let decisions : Array[Decision] = []
for flag in flags {
decisions.push(evaluate(flag, context))
}
decisions
}
///|
pub fn bucket(key : String, salt : String) -> Int {
let input = key + "|" + salt
let mut hash = 5381
for c in input.iter() {
hash = (hash * 33 + c.to_int()) % 1000003
if hash < 0 {
hash = 0 - hash
}
}
hash % 100
}
///|
pub fn parse_flags(input : String) -> Result[Array[Flag], FlagError] {
let mut flags : Array[Flag] = []
let lines = input.split("\n").to_array()
for i in 0..
match parse_flag_line(tokens, line_number) {
Ok(flag) => flags.push(flag)
Err(err) => return Err(err)
}
"allow" =>
match attach_rule(flags, tokens, line_number, "allow") {
Ok(next) => flags = next
Err(err) => return Err(err)
}
"deny" =>
match attach_rule(flags, tokens, line_number, "deny") {
Ok(next) => flags = next
Err(err) => return Err(err)
}
"when" =>
match attach_rule(flags, tokens, line_number, "when") {
Ok(next) => flags = next
Err(err) => return Err(err)
}
"allow_variant" =>
match attach_rule(flags, tokens, line_number, "allow_variant") {
Ok(next) => flags = next
Err(err) => return Err(err)
}
"when_variant" =>
match attach_rule(flags, tokens, line_number, "when_variant") {
Ok(next) => flags = next
Err(err) => return Err(err)
}
other => return Err(ParseError(line_number, "unknown directive " + other))
}
}
Ok(flags)
}
///|
pub fn validate_flags(flags : Array[Flag]) -> Array[FlagDiagnostic] {
let diagnostics : Array[FlagDiagnostic] = []
for i in 0.. 0 {
diagnostics.push({
kind: "duplicate",
flag: flag.name,
message: "duplicate flag name " + flag.name,
})
}
}
if flag.rollout < 0 || flag.rollout > 100 {
diagnostics.push({
kind: "invalid_rollout",
flag: flag.name,
message: "rollout must be between 0 and 100",
})
}
if flag.name.length() == 0 {
diagnostics.push({
kind: "empty_name",
flag: flag.name,
message: "flag name cannot be empty",
})
}
if flag.is_multivariate {
if flag.variants.length() == 0 {
diagnostics.push({
kind: "no_variants",
flag: flag.name,
message: "multivariate flag must define variants",
})
} else {
let mut sum = 0
for v in flag.variants {
sum += v.weight
if v.weight < 0 || v.weight > 100 {
diagnostics.push({
kind: "invalid_variant_weight",
flag: flag.name,
message: "variant " + v.name + " weight must be between 0 and 100",
})
}
}
if sum != 100 {
diagnostics.push({
kind: "invalid_weight_sum",
flag: flag.name,
message: "sum of variant weights must be exactly 100, got " +
sum.to_string(),
})
}
}
}
}
diagnostics
}
///|
pub fn explain(decision : Decision) -> String {
let state = if decision.enabled { "on" } else { "off" }
let base = decision.flag +
" " +
state +
" (variant: " +
decision.variant +
"): " +
decision.reason
if decision.audit_trail.length() > 0 {
let mut trail = base + "\n Audit Trail:"
for step in decision.audit_trail {
trail = trail + "\n - " + step
}
trail
} else {
base
}
}
///|
pub fn format_error(err : FlagError) -> String {
match err {
ParseError(line, message) => "line \{line}: " + message
UnknownFlag(line, name) => "line \{line}: unknown flag " + name
}
}
///|
fn lookup_context(context : EvalContext, key : String) -> String? {
if key == "environment" || key == "env" {
return Some(context.environment)
}
for attr in context.attributes {
if attr.key == key {
return Some(attr.value)
}
}
None
}
///|
fn words(line : String) -> Array[String] {
let parts : Array[String] = []
for part in line.split(" ") {
let token = part.trim().to_owned()
if token.length() > 0 {
parts.push(token)
}
}
parts
}
///|
fn parse_flag_line(
tokens : Array[String],
line : Int,
) -> Result[Flag, FlagError] {
if tokens.length() < 4 {
return Err(ParseError(line, "flag requires name enabled and rollout"))
}
let name = tokens[1]
if !tokens[2].has_prefix("enabled=") {
return Err(ParseError(line, "expected enabled=true|false token"))
}
let enabled_text = after(tokens[2], "enabled=")
let enabled = if enabled_text == "true" {
true
} else if enabled_text == "false" {
false
} else {
return Err(ParseError(line, "expected enabled=true|false token"))
}
if !tokens[3].has_prefix("rollout=") {
return Err(ParseError(line, "expected rollout=0..100 token"))
}
let rollout_text = after(tokens[3], "rollout=")
let rollout = match parse_int(rollout_text) {
Some(value) => value
None => return Err(ParseError(line, "rollout must be an integer"))
}
let mut salt = name
let mut is_multivariate = false
let mut default_variant = if enabled { "on" } else { "off" }
let variants : Array[Variant] = []
for i in 4.. {
let weight = match parse_int(v_weight_str.to_owned()) {
Some(w) => w
None =>
return Err(
ParseError(line, "variant weight must be an integer"),
)
}
variants.push({ name: v_name.to_owned(), weight })
}
None =>
return Err(
ParseError(line, "variants must be formatted as name:weight"),
)
}
}
}
}
Ok({
name,
enabled,
default_enabled: false,
rollout,
salt,
rules: [],
is_multivariate,
variants,
default_variant,
})
}
///|
fn attach_rule(
flags : Array[Flag],
tokens : Array[String],
line : Int,
directive : String,
) -> Result[Array[Flag], FlagError] {
if tokens.length() < 3 {
return Err(ParseError(line, directive + " requires flag name and value"))
}
let flag_name = tokens[1]
let rule = match directive {
"allow" => Allow(tokens[2])
"deny" => Deny(tokens[2])
"when" =>
match parse_condition(tokens[2], line) {
Ok(cond) => When(cond.0, cond.1, cond.2)
Err(err) => return Err(err)
}
"allow_variant" => {
if tokens.length() < 4 {
return Err(
ParseError(
line, "allow_variant requires flag name, subject and variant name",
),
)
}
AllowVariant(tokens[2], tokens[3])
}
"when_variant" => {
if tokens.length() < 4 {
return Err(
ParseError(
line, "when_variant requires flag name, condition and variant name",
),
)
}
match parse_condition(tokens[2], line) {
Ok(cond) => WhenVariant(cond.0, cond.1, cond.2, tokens[3])
Err(err) => return Err(err)
}
}
_ => return Err(ParseError(line, "unknown rule directive"))
}
let out : Array[Flag] = []
let mut found = false
for flag in flags {
if flag.name == flag_name {
let rules : Array[Rule] = []
for existing in flag.rules {
rules.push(existing)
}
rules.push(rule)
out.push({
name: flag.name,
enabled: flag.enabled,
default_enabled: flag.default_enabled,
rollout: flag.rollout,
salt: flag.salt,
rules,
is_multivariate: flag.is_multivariate,
variants: flag.variants,
default_variant: flag.default_variant,
})
found = true
} else {
out.push(flag)
}
}
if !found {
return Err(UnknownFlag(line, flag_name))
}
Ok(out)
}
///|
fn parse_condition(
token : String,
line : Int,
) -> Result[(String, Operator, String), FlagError] {
if token.contains(">=") {
let parts = split_once(token, ">=")
return Ok((parts.0, Ge, parts.1))
}
if token.contains("<=") {
let parts = split_once(token, "<=")
return Ok((parts.0, Le, parts.1))
}
if token.contains("!=") {
let parts = split_once(token, "!=")
return Ok((parts.0, Ne, parts.1))
}
if token.contains("~=") {
let parts = split_once(token, "~=")
return Ok((parts.0, Contains, parts.1))
}
if token.contains("=") {
let parts = split_once(token, "=")
return Ok((parts.0, Eq, parts.1))
}
if token.contains(">") {
let parts = split_once(token, ">")
return Ok((parts.0, Gt, parts.1))
}
if token.contains("<") {
let parts = split_once(token, "<")
return Ok((parts.0, Lt, parts.1))
}
if token.contains(":in:") {
let parts = split_once(token, ":in:")
return Ok((parts.0, In, parts.1))
}
if token.contains(":not_in:") {
let parts = split_once(token, ":not_in:")
return Ok((parts.0, NotIn, parts.1))
}
Err(ParseError(line, "invalid condition format " + token))
}
///|
fn split_once(token : String, sep : String) -> (String, String) {
match token.split_once(sep[:]) {
Some((a, b)) => (a.to_owned(), b.to_owned())
None => (token, "")
}
}
///|
fn after(input : String, prefix : String) -> String {
input[prefix.length():].to_owned()
}
///|
fn parse_int(input : String) -> Int? {
if input.length() == 0 {
return None
}
let mut value = 0
for c in input.iter() {
if c < '0' || c > '9' {
return None
}
value = value * 10 + c.to_int() - '0'.to_int()
}
Some(value)
}
///|
fn op_to_str(op : Operator) -> String {
match op {
Eq => "="
Ne => "!="
Gt => ">"
Lt => "<"
Ge => ">="
Le => "<="
Contains => "~="
In => "in"
NotIn => "not_in"
}
}
///|
fn eval_operator(ctx_val : String, op : Operator, rule_val : String) -> Bool {
match op {
Eq => ctx_val == rule_val
Ne => ctx_val != rule_val
Gt => ctx_val > rule_val
Lt => ctx_val < rule_val
Ge => ctx_val >= rule_val
Le => ctx_val <= rule_val
Contains => ctx_val.contains(rule_val[:])
In => {
let mut found = false
for part in rule_val.split(",") {
if part.trim().to_owned() == ctx_val {
found = true
break
}
}
found
}
NotIn => {
let mut found = false
for part in rule_val.split(",") {
if part.trim().to_owned() == ctx_val {
found = true
break
}
}
!found
}
}
}
///|
pub trait ConfigProvider {
fn load_flags(self : Self) -> Result[Array[Flag], String]
}
///|
pub struct JsonConfigProvider {
json_string : String
}
///|
pub fn JsonConfigProvider::new(json_string : String) -> JsonConfigProvider {
{ json_string, }
}
///|
fn parse_op_str(op_str : String) -> Operator? {
match op_str {
"Eq" | "=" => Some(Eq)
"Ne" | "!=" => Some(Ne)
"Gt" | ">" => Some(Gt)
"Lt" | "<" => Some(Lt)
"Ge" | ">=" => Some(Ge)
"Le" | "<=" => Some(Le)
"Contains" | "~=" => Some(Contains)
"In" => Some(In)
"NotIn" => Some(NotIn)
_ => None
}
}
///|
fn get_string(json : Json, key : String) -> String? {
match json {
Object(m) =>
match m.get(key) {
Some(String(s)) => Some(s)
_ => None
}
_ => None
}
}
///|
fn get_bool(json : Json, key : String) -> Bool? {
match json {
Object(m) =>
match m.get(key) {
Some(True) => Some(true)
Some(False) => Some(false)
_ => None
}
_ => None
}
}
///|
fn get_int(json : Json, key : String) -> Int? {
match json {
Object(m) =>
match m.get(key) {
Some(Number(d, ..)) => Some(d.to_int())
_ => None
}
_ => None
}
}
///|
fn get_array(json : Json, key : String) -> Array[Json]? {
match json {
Object(m) =>
match m.get(key) {
Some(Array(arr)) => Some(arr)
_ => None
}
_ => None
}
}
///|
pub impl ConfigProvider for JsonConfigProvider with fn load_flags(
self : JsonConfigProvider,
) -> Result[Array[Flag], String] {
let json = @json.parse(self.json_string[:]) catch {
e => return Err("JSON parse error: \{e}")
}
let flags : Array[Flag] = []
let flags_json = match get_array(json, "flags") {
Some(arr) => arr
None => return Err("JSON config missing 'flags' array")
}
for flag_j in flags_json {
let name = match get_string(flag_j, "name") {
Some(s) => s
None => return Err("flag missing 'name' field")
}
let enabled = match get_bool(flag_j, "enabled") {
Some(b) => b
None => true
}
let default_enabled = match get_bool(flag_j, "default_enabled") {
Some(b) => b
None => false
}
let rollout = match get_int(flag_j, "rollout") {
Some(r) => r
None => 100
}
let salt = match get_string(flag_j, "salt") {
Some(s) => s
None => name
}
let is_multivariate = match get_bool(flag_j, "is_multivariate") {
Some(b) => b
None => false
}
let default_variant = match get_string(flag_j, "default_variant") {
Some(s) => s
None => "off"
}
let variants : Array[Variant] = []
if is_multivariate {
match get_array(flag_j, "variants") {
Some(variants_j) =>
for v_j in variants_j {
let v_name = match get_string(v_j, "name") {
Some(s) => s
None => return Err("variant missing 'name' field")
}
let v_weight = match get_int(v_j, "weight") {
Some(w) => w
None => return Err("variant missing 'weight' field")
}
variants.push({ name: v_name, weight: v_weight })
}
None => ()
}
}
let rules : Array[Rule] = []
match get_array(flag_j, "rules") {
Some(rules_j) =>
for rule_j in rules_j {
let r_type = match get_string(rule_j, "type") {
Some(s) => s
None => return Err("rule missing 'type' field")
}
match r_type {
"Allow" => {
let subject = match get_string(rule_j, "subject") {
Some(s) => s
None => return Err("Allow rule missing 'subject'")
}
rules.push(Allow(subject))
}
"Deny" => {
let subject = match get_string(rule_j, "subject") {
Some(s) => s
None => return Err("Deny rule missing 'subject'")
}
rules.push(Deny(subject))
}
"AllowVariant" => {
let subject = match get_string(rule_j, "subject") {
Some(s) => s
None => return Err("AllowVariant rule missing 'subject'")
}
let variant = match get_string(rule_j, "variant") {
Some(s) => s
None => return Err("AllowVariant rule missing 'variant'")
}
rules.push(AllowVariant(subject, variant))
}
"When" => {
let key = match get_string(rule_j, "key") {
Some(s) => s
None => return Err("When rule missing 'key'")
}
let op_str = match get_string(rule_j, "op") {
Some(s) => s
None => return Err("When rule missing 'op'")
}
let op = match parse_op_str(op_str) {
Some(o) => o
None =>
return Err("When rule has invalid operator '" + op_str + "'")
}
let value = match get_string(rule_j, "value") {
Some(s) => s
None => return Err("When rule missing 'value'")
}
rules.push(When(key, op, value))
}
"WhenVariant" => {
let key = match get_string(rule_j, "key") {
Some(s) => s
None => return Err("WhenVariant rule missing 'key'")
}
let op_str = match get_string(rule_j, "op") {
Some(s) => s
None => return Err("WhenVariant rule missing 'op'")
}
let op = match parse_op_str(op_str) {
Some(o) => o
None =>
return Err(
"WhenVariant rule has invalid operator '" + op_str + "'",
)
}
let value = match get_string(rule_j, "value") {
Some(s) => s
None => return Err("WhenVariant rule missing 'value'")
}
let variant = match get_string(rule_j, "variant") {
Some(s) => s
None => return Err("WhenVariant rule missing 'variant'")
}
rules.push(WhenVariant(key, op, value, variant))
}
_ => return Err("unknown rule type '" + r_type + "'")
}
}
None => ()
}
flags.push({
name,
enabled,
default_enabled,
rollout,
salt,
rules,
is_multivariate,
variants,
default_variant,
})
}
Ok(flags)
}