mirror of
https://github.com/block/buzz.git
synced 2026-10-28 04:51:40 +00:00
## Why Buzz server components need one typed feature-flag contract without forcing the open-source build to depend on LaunchDarkly. A staging or production relay must not be able to choose the wrong provider through runtime configuration. ## What - Adds typed boolean and signed `i64` flags with static, environment, and compile-gated LaunchDarkly evaluators. - Defines an executable relay artifact contract that compiles exactly one provider. - Rejects zero or multiple relay provider features at compile time. - Runs the LaunchDarkly feature tests and the provider compile matrix in the unit CI lane and fallback test script. ## How `buzz-feature-flags` stays provider-neutral. The future relay composition root will expose one build path per artifact feature: `static-feature-flags`, `environment-feature-flags`, or `launchdarkly-feature-flags`. Runtime configuration carries only settings for the selected provider. It never carries a provider name, fallback, or precedence rule. The crate still has no relay consumer. A locked compile fixture proves each singleton builds and every zero or multi-provider combination fails until the relay adopts this contract. This removes the proposed runtime `ProviderMode` and leaves one provider, one construction path, and one evaluator per artifact. ## Risk This PR still has no relay or app consumer, so it changes no user-visible behavior. Risk is limited to the new feature-flag crate, its compile contract, and its unit-test lane. ## Testing An external Rust consumer at `30cf968bfe85e82980fda7e63f5bebe649ca939f` exercised six environment-evaluator scenarios and 59 assertions. The compile-time correction adds no live consumer to exercise manually. ## Bigger picture When `buzz-relay` adopts this crate, its build manifests must map each distribution to one provider feature. That consumer PR must verify live Relay Proxy reachability, secret ingestion, lifecycle ownership, and user-visible behavior. Generated with Codex --------- Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
218 lines
7.2 KiB
Rust
218 lines
7.2 KiB
Rust
#![deny(unsafe_code)]
|
|
#![warn(missing_docs)]
|
|
//! Typed boolean and integer feature flags for Buzz server components.
|
|
|
|
use buzz_core::{CommunityId, PublicKey};
|
|
|
|
pub mod environment;
|
|
pub use environment::{EnvironmentDiagnostic, EnvironmentEvaluator};
|
|
|
|
pub mod flags;
|
|
pub use flags::{BooleanFlag, IntegerFlag};
|
|
|
|
#[cfg(feature = "launchdarkly")]
|
|
/// LaunchDarkly-backed evaluator adapter.
|
|
pub mod launchdarkly;
|
|
|
|
/// Stable targeting context for one Buzz community and an optional actor.
|
|
///
|
|
/// Community is always required because the same Nostr identity can participate
|
|
/// in multiple isolated Buzz communities. Community-only contexts support
|
|
/// background and system evaluations that have no actor.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct EvaluationContext {
|
|
community: CommunityId,
|
|
actor_pubkey: Option<PublicKey>,
|
|
}
|
|
|
|
impl EvaluationContext {
|
|
/// Construct a community-only evaluation context.
|
|
pub const fn for_community(community: CommunityId) -> Self {
|
|
Self {
|
|
community,
|
|
actor_pubkey: None,
|
|
}
|
|
}
|
|
|
|
/// Construct an evaluation context for an actor within one community.
|
|
pub const fn for_actor(community: CommunityId, actor_pubkey: PublicKey) -> Self {
|
|
Self {
|
|
community,
|
|
actor_pubkey: Some(actor_pubkey),
|
|
}
|
|
}
|
|
|
|
/// Community that owns this evaluation.
|
|
pub const fn community(&self) -> CommunityId {
|
|
self.community
|
|
}
|
|
|
|
/// Optional stable actor key for actor-targeted evaluation.
|
|
pub const fn actor_pubkey(&self) -> Option<&PublicKey> {
|
|
self.actor_pubkey.as_ref()
|
|
}
|
|
}
|
|
|
|
/// Evaluates typed feature flags.
|
|
pub trait FlagEvaluator: Send + Sync {
|
|
/// Resolve a boolean flag for one Buzz evaluation context.
|
|
///
|
|
/// Implementations must return [`BooleanFlag::default`] whenever no valid
|
|
/// boolean value is available, including provider absence or failure.
|
|
fn evaluate_bool(&self, flag: BooleanFlag, context: &EvaluationContext) -> bool;
|
|
|
|
/// Resolve an integer flag for one Buzz evaluation context.
|
|
///
|
|
/// Implementations must return [`IntegerFlag::default`] whenever no valid
|
|
/// integer value is available, including provider absence or failure.
|
|
fn evaluate_int(&self, flag: IntegerFlag, context: &EvaluationContext) -> i64;
|
|
}
|
|
|
|
/// Static evaluator that should return each flag's declared default.
|
|
#[derive(Debug, Default, Clone, Copy)]
|
|
pub struct StaticEvaluator;
|
|
|
|
impl FlagEvaluator for StaticEvaluator {
|
|
fn evaluate_bool(&self, flag: BooleanFlag, _context: &EvaluationContext) -> bool {
|
|
flag.default()
|
|
}
|
|
|
|
fn evaluate_int(&self, flag: IntegerFlag, _context: &EvaluationContext) -> i64 {
|
|
flag.default()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use buzz_core::CommunityId;
|
|
|
|
fn community(id: &str) -> CommunityId {
|
|
CommunityId::from_uuid(id.parse().expect("valid community UUID"))
|
|
}
|
|
|
|
fn actor() -> PublicKey {
|
|
PublicKey::from_hex("c4f0623bdc8c4f7ecab9f7457f501f3e8f4efcf8f8f6ef6f4d76f42f5bb6f2cb")
|
|
.expect("valid pubkey")
|
|
}
|
|
|
|
#[test]
|
|
fn static_evaluator_returns_declared_defaults_for_community() {
|
|
let context =
|
|
EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"));
|
|
|
|
assert!(StaticEvaluator.evaluate_bool(BooleanFlag::new("enabled", true), &context));
|
|
assert!(!StaticEvaluator.evaluate_bool(BooleanFlag::new("disabled", false), &context));
|
|
}
|
|
|
|
#[test]
|
|
fn static_evaluator_returns_declared_integer_defaults_for_community() {
|
|
let context =
|
|
EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"));
|
|
|
|
assert_eq!(
|
|
StaticEvaluator.evaluate_int(IntegerFlag::new("positive", 7), &context),
|
|
7
|
|
);
|
|
assert_eq!(
|
|
StaticEvaluator.evaluate_int(IntegerFlag::new("negative", -9), &context),
|
|
-9
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn non_launchdarkly_evaluator_can_target_community_through_public_trait() {
|
|
struct CommunityEvaluator {
|
|
enabled_community: CommunityId,
|
|
}
|
|
|
|
impl FlagEvaluator for CommunityEvaluator {
|
|
fn evaluate_bool(&self, flag: BooleanFlag, context: &EvaluationContext) -> bool {
|
|
if context.community() == self.enabled_community {
|
|
true
|
|
} else {
|
|
flag.default()
|
|
}
|
|
}
|
|
|
|
fn evaluate_int(&self, flag: IntegerFlag, context: &EvaluationContext) -> i64 {
|
|
if context.community() == self.enabled_community {
|
|
41
|
|
} else {
|
|
flag.default()
|
|
}
|
|
}
|
|
}
|
|
|
|
let enabled_community = community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa");
|
|
let other_community = community("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb");
|
|
let evaluator: &dyn FlagEvaluator = &CommunityEvaluator { enabled_community };
|
|
|
|
assert!(evaluator.evaluate_bool(
|
|
BooleanFlag::new("community-targeted-bool", false),
|
|
&EvaluationContext::for_community(enabled_community),
|
|
));
|
|
assert!(!evaluator.evaluate_bool(
|
|
BooleanFlag::new("community-targeted-bool", false),
|
|
&EvaluationContext::for_community(other_community),
|
|
));
|
|
|
|
assert_eq!(
|
|
evaluator.evaluate_int(
|
|
IntegerFlag::new("community-targeted-int", -1),
|
|
&EvaluationContext::for_community(enabled_community),
|
|
),
|
|
41
|
|
);
|
|
assert_eq!(
|
|
evaluator.evaluate_int(
|
|
IntegerFlag::new("community-targeted-int", -1),
|
|
&EvaluationContext::for_community(other_community),
|
|
),
|
|
-1
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn same_actor_in_two_communities_is_distinguishable_for_integer_evaluation() {
|
|
struct CommunityRolloutEvaluator {
|
|
enabled_community: CommunityId,
|
|
expected_actor: PublicKey,
|
|
}
|
|
|
|
impl FlagEvaluator for CommunityRolloutEvaluator {
|
|
fn evaluate_bool(&self, flag: BooleanFlag, _context: &EvaluationContext) -> bool {
|
|
flag.default()
|
|
}
|
|
|
|
fn evaluate_int(&self, flag: IntegerFlag, context: &EvaluationContext) -> i64 {
|
|
if context.community() == self.enabled_community
|
|
&& context.actor_pubkey() == Some(&self.expected_actor)
|
|
{
|
|
99
|
|
} else {
|
|
flag.default()
|
|
}
|
|
}
|
|
}
|
|
|
|
let actor = actor();
|
|
let community_a = community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa");
|
|
let community_b = community("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb");
|
|
let evaluator: &dyn FlagEvaluator = &CommunityRolloutEvaluator {
|
|
enabled_community: community_a,
|
|
expected_actor: actor,
|
|
};
|
|
let flag = IntegerFlag::new("community-rollout", -11);
|
|
|
|
assert_eq!(
|
|
evaluator.evaluate_int(flag, &EvaluationContext::for_actor(community_a, actor)),
|
|
99
|
|
);
|
|
assert_eq!(
|
|
evaluator.evaluate_int(flag, &EvaluationContext::for_actor(community_b, actor)),
|
|
-11
|
|
);
|
|
}
|
|
}
|