moonflags

MoonBit-native feature flag, segment targeting, rollout and validation toolkit

feature-flags
rollout
targeting
segments
validation
wasm
moon add WhisperJXH/moonflags@0.1.0
Download zip
Version
0.1.0
License
Apache-2.0
Last updated
23 hours ago
Downloads
2
README

#MoonFlags

MoonFlags 是一个 MoonBit 原生的功能开关、用户分群、灰度分流和配置校验库。它面向需要在 MoonBit / WebAssembly 应用中做功能发布控制、A/B 测试、边缘配置判定和规则解释的开发者。

#解决的问题

在实际应用中,功能开关通常不只是一个布尔值,还需要按用户属性、名单、分群和百分比进行稳定分流。MoonFlags 提供纯 MoonBit 实现的规则模型和评估引擎,让库作者可以在不引入外部运行时的情况下完成这些逻辑。

#安装

moon add WhisperJXH/moonflags

Mooncakes 包名:

WhisperJXH/moonflags

#最小示例

let rollout = @moonflags.Rollout([
@moonflags.WeightedVariant("control", 6000),
@moonflags.WeightedVariant("beta", 4000),
], seed="checkout-v1")

let rule = @moonflags.Rule(
"paid-cn-users",
conditions=[
@moonflags.str_eq("country", "CN"),
@moonflags.bool_eq("paid", true),
],
rollout~,
reason="paid_user_rollout",
)

let flag = @moonflags.Flag(
"checkout_flow",
["control", "beta"],
"control",
rules=[rule],
)

let ctx = @moonflags.Context(user_key="user-42")
.with_str("country", "CN")
.with_bool("paid", true)

let result = flag.evaluate(ctx)

#本地运行

moon check moon build moon test moon run cmd/main moon publish --dry-run

#核心功能

  • Context:保存一次评估所需的 user_key 和属性。
  • Condition:支持存在/缺失、字符串等值、整数比较、列表包含、前缀和后缀匹配。
  • Segment:复用用户名单和属性规则,可被多个 flag 引用。
  • Rule:按顺序执行目标规则,支持直接变体和百分比分流。
  • Rollout:使用稳定哈希桶进行 0 到 10000 基点分流。
  • Flag / FlagSet:评估单个开关或一组开关。
  • validate_flag / validate_set:检查无效变体、重复值、未知 segment 和 rollout 权重错误。

#支持范围

  • 支持字符串、整数、布尔值和字符串列表属性。
  • 支持规则 AND 语义;需要 OR 时可声明多条规则。
  • 支持命名 segment、用户 include/exclude 名单和属性条件。
  • 支持 0 到 10000 基点的稳定百分比分流。
  • 支持可解释的评估结果:返回命中规则、原因、桶号和警告。

#暂不支持范围

  • 不内置远程配置拉取、数据库存储或网络同步。
  • 不内置 JSON / YAML 配置解析器,当前以 MoonBit API 方式声明规则。
  • 不提供加密级随机数;rollout 使用确定性哈希,适用于稳定分流而非安全用途。

#许可证与合规

本项目采用 Apache-2.0 许可证。核心代码为原创 MoonBit 实现,不移植第三方源码,不包含来源不明的素材或私有代码。

#
Attribute

pub(all) enum Attribute {
Str(String)
Int(Int)
Bool(Bool)
Strings(Array[String])
} derive(Eq,
Debug
)

Values that can be attached to an evaluation context.

#
Condition

pub struct Condition {
attr : String
matcher : Matcher
} derive(Eq,
Debug
)

A single attribute predicate.

#
Condition::Condition

#alias(new, deprecated="Use `Condition()` instead")
fn Condition::Condition(attr : StringView, matcher : Matcher) -> Condition

Build a condition.

#
Condition::explain

fn Condition::explain(self : Condition, ctx : Context) -> String

Return a compact explanation for why a condition passed or failed.

#
Condition::matches

fn Condition::matches(self : Condition, ctx : Context) -> Bool

Evaluate one condition against a context.

#
Context

pub struct Context {
user_key : String?
attributes : Map[String, Attribute]
} derive(Eq,
Debug
)

Request attributes used for a single flag evaluation.

#
Context::Context

#alias(new, deprecated="Use `Context()` instead")
fn Context::Context(user_key? : StringView, attributes? : Map[String, Attribute]) -> Context

Create a context for evaluating flags.

#
Context::contains

fn Context::contains(self : Context, key : StringView) -> Bool

Check whether the context contains an attribute key.

#
Context::get

fn Context::get(self : Context, key : StringView) -> Attribute?

Read an attribute by key.

#
Context::with_bool

fn Context::with_bool(self : Context, key : StringView, value : Bool) -> Context

Attach a boolean attribute and return the same context for chaining.

#
Context::with_int

fn Context::with_int(self : Context, key : StringView, value : Int) -> Context

Attach an integer attribute and return the same context for chaining.

#
Context::with_str

fn Context::with_str(self : Context, key : StringView, value : StringView) -> Context

Attach a string attribute and return the same context for chaining.

#
Context::with_strings

fn Context::with_strings(self : Context, key : StringView, values : ArrayView[String]) -> Context

Attach a string-list attribute and return the same context for chaining.

#
Diagnostic

pub struct Diagnostic {
severity : Severity
path : String
message : String
} derive(Eq,
Debug
)

Validation message returned by validate_flag and validate_set.

#
Diagnostic::Diagnostic

#alias(new, deprecated="Use `Diagnostic()` instead")
fn Diagnostic::Diagnostic(severity : Severity, path : StringView, message : StringView) -> Diagnostic

Build a diagnostic message.

#
Diagnostic::error

fn Diagnostic::error(path : StringView, message : StringView) -> Diagnostic

Build an error diagnostic.

#
Diagnostic::warning

fn Diagnostic::warning(path : StringView, message : StringView) -> Diagnostic

Build a warning diagnostic.

#
Evaluation

pub struct Evaluation {
flag_key : String
variant : String
reason : String
matched : Bool
rule_key : String?
bucket : Int?
warnings : Array[String]
} derive(Eq,
Debug
)

Result returned by flag evaluation.

#
Flag

pub struct Flag {
key : String
enabled : Bool
variants : Array[String]
default_variant : String
off_variant : String
rules : Array[Rule]
fallthrough_variant : String?
fallthrough_rollout : Rollout?
} derive(Eq,
Debug
)

A feature flag with ordered rules and a fallthrough behavior.

#
Flag::Flag

#alias(new, deprecated="Use `Flag()` instead")
fn Flag::Flag(key : StringView, variants : ArrayView[String], default_variant : StringView, enabled? : Bool, off_variant? : StringView, rules? : ArrayView[Rule], fallthrough_variant? : StringView, fallthrough_rollout? : Rollout) -> Flag

Build a feature flag.

#
Flag::evaluate

fn Flag::evaluate(self : Flag, ctx : Context) -> Evaluation

Evaluate a single flag without named segments.

#
FlagSet

pub struct FlagSet {
flags : Map[String, Flag]
segments : Map[String, Segment]
} derive(Eq,
Debug
)

A reusable collection of flags and named segments.

#
FlagSet::FlagSet

#alias(new, deprecated="Use `FlagSet()` instead")
fn FlagSet::FlagSet(flags? : ArrayView[Flag], segments? : ArrayView[Segment]) -> FlagSet

Create an empty flag set.

#
FlagSet::evaluate

fn FlagSet::evaluate(self : FlagSet, flag_key : StringView, ctx : Context) -> Evaluation?

Evaluate a flag from a flag set.

#
FlagSet::evaluate_all

fn FlagSet::evaluate_all(self : FlagSet, ctx : Context) -> Array[Evaluation]

Evaluate all flags in a set and return results in insertion order.

#
FlagSet::with_flag

fn FlagSet::with_flag(self : FlagSet, flag : Flag) -> FlagSet

Add or replace a flag in a flag set.

#
FlagSet::with_segment

fn FlagSet::with_segment(self : FlagSet, segment : Segment) -> FlagSet

Add or replace a segment in a flag set.

#
Matcher

pub(all) enum Matcher {
Exists
Missing
Is(Attribute)
IsNot(Attribute)
OneOf(Array[Attribute])
Contains(String)
ContainsAny(Array[String])
StartsWith(String)
EndsWith(String)
GreaterThan(Int)
GreaterEq(Int)
LessThan(Int)
LessEq(Int)
} derive(Eq,
Debug
)

Matchers supported by MoonFlags conditions.

#
Rollout

pub struct Rollout {
seed : String
allocations : Array[WeightedVariant]
} derive(Eq,
Debug
)

Deterministic percentage rollout configuration.

#
Rollout::Rollout

#alias(new, deprecated="Use `Rollout()` instead")
fn Rollout::Rollout(allocations : ArrayView[WeightedVariant], seed? : StringView) -> Rollout

Build a deterministic rollout.

#
Rollout::pick

fn Rollout::pick(self : Rollout, bucket : Int) -> String?

Pick a variant from a rollout for a bucket.

#
Rule

pub struct Rule {
key : String
conditions : Array[Condition]
segments : Array[String]
variant : String?
rollout : Rollout?
reason : String
} derive(Eq,
Debug
)

A targeting rule. All conditions must pass. Referenced segments must also pass when a FlagSet is used for evaluation.

#
Rule::Rule

#alias(new, deprecated="Use `Rule()` instead")
fn Rule::Rule(key : StringView, conditions? : ArrayView[Condition], segments? : ArrayView[String], variant? : StringView, rollout? : Rollout, reason? : StringView) -> Rule

Build a targeting rule.

#
Rule::matches

fn Rule::matches(self : Rule, ctx : Context, segments : Map[String, Segment]) -> Bool

Check whether a rule targets a context.

#
Segment

pub struct Segment {
key : String
include_users : Array[String]
exclude_users : Array[String]
conditions : Array[Condition]
} derive(Eq,
Debug
)

Named segment used to reuse a targeting predicate across flags.

#
Segment::Segment

#alias(new, deprecated="Use `Segment()` instead")
fn Segment::Segment(key : StringView, include_users? : ArrayView[String], exclude_users? : ArrayView[String], conditions? : ArrayView[Condition]) -> Segment

Build a segment.

#
Segment::matches

fn Segment::matches(self : Segment, ctx : Context) -> Bool

Evaluate a segment.

#
Severity

pub(all) enum Severity {
Error
Warning
} derive(Eq,
Debug
)

Validation severity.

#
WeightedVariant

pub struct WeightedVariant {
variant : String
weight : Int
} derive(Eq,
Debug
)

One weighted variant inside a percentage rollout.

Weight is expressed in basis points: 10000 means 100%.

#
WeightedVariant::WeightedVariant

#alias(new, deprecated="Use `WeightedVariant()` instead")
fn WeightedVariant::WeightedVariant(variant : StringView, weight : Int) -> WeightedVariant

Build a weighted variant.

#
all_conditions_match

fn all_conditions_match(conditions : ArrayView[Condition], ctx : Context) -> Bool

Evaluate all conditions with AND semantics.

#
any_condition_matches

fn any_condition_matches(conditions : ArrayView[Condition], ctx : Context) -> Bool

Evaluate any condition with OR semantics.

#
attr_bool

fn attr_bool(value : Bool) -> Attribute

Create a single boolean attribute.

#
attr_int

fn attr_int(value : Int) -> Attribute

Create a single integer attribute.

#
attr_str

fn attr_str(value : StringView) -> Attribute

Create a single string attribute.

#
attr_strings

fn attr_strings(values : ArrayView[String]) -> Attribute

Create a string-list attribute.

#
bool_eq

fn bool_eq(attr : StringView, value : Bool) -> Condition

Attribute equals a boolean value.

#
contains

fn contains(attr : StringView, value : StringView) -> Condition

String or list attribute contains a value.

#
contains_any

fn contains_any(attr : StringView, values : ArrayView[String]) -> Condition

String-list attribute contains at least one value from the expected set.

#
ends_with

fn ends_with(attr : StringView, suffix : StringView) -> Condition

String attribute ends with a suffix.

#
exists

fn exists(attr : StringView) -> Condition

Attribute exists.

#
greater_eq

fn greater_eq(attr : StringView, value : Int) -> Condition

Integer attribute is greater than or equal to a threshold.

#
greater_than

fn greater_than(attr : StringView, value : Int) -> Condition

Integer attribute is greater than a threshold.

#
has_errors

fn has_errors(diagnostics : ArrayView[Diagnostic]) -> Bool

Return true if diagnostics contain at least one error.

#
int_eq

fn int_eq(attr : StringView, value : Int) -> Condition

Attribute equals an integer value.

#
less_eq

fn less_eq(attr : StringView, value : Int) -> Condition

Integer attribute is less than or equal to a threshold.

#
less_than

fn less_than(attr : StringView, value : Int) -> Condition

Integer attribute is less than a threshold.

#
missing

fn missing(attr : StringView) -> Condition

Attribute is missing.

#
rollout_bucket

fn rollout_bucket(seed : StringView, flag_key : StringView, user_key : StringView) -> Int

Deterministically map a user to a rollout bucket in the range 0..<10000.

#
starts_with

fn starts_with(attr : StringView, prefix : StringView) -> Condition

String attribute starts with a prefix.

#
str_eq

fn str_eq(attr : StringView, value : StringView) -> Condition

Attribute equals a string value.

#
str_not_eq

fn str_not_eq(attr : StringView, value : StringView) -> Condition

Attribute does not equal a string value.

#
str_one_of

fn str_one_of(attr : StringView, values : ArrayView[String]) -> Condition

String attribute is one of the given values.

#
validate_flag

fn validate_flag(flag : Flag) -> Array[Diagnostic]

Validate a single flag without segment cross-reference checks.

#
validate_set

fn validate_set(set : FlagSet) -> Array[Diagnostic]

Validate a flag set including segment references.