1use std::collections::{HashMap, HashSet};
4
5use serde::{Deserialize, Serialize};
6
7use crate::error::FeatureFlagError;
8use crate::predicate::Predicate;
9
10pub type Variant = String;
12
13#[derive(Clone, Debug, Deserialize, Serialize)]
20pub struct Rule {
21 pub id: String,
23 pub when: Predicate,
25 pub outcome: Outcome,
27 #[serde(default)]
29 pub description: Option<String>,
30}
31
32#[derive(Clone, Debug, Deserialize, Serialize)]
34#[serde(tag = "kind", rename_all = "snake_case")]
35pub enum Outcome {
36 Variant {
38 variant: Variant,
40 },
41 Rollout {
44 variants: Vec<RolloutEntry>,
46 },
47}
48
49#[derive(Clone, Debug, Deserialize, Serialize)]
51pub struct RolloutEntry {
52 pub variant: Variant,
54 pub weight: u32,
56}
57
58#[derive(Clone, Debug, Deserialize, Serialize)]
60pub struct Flag {
61 pub id: String,
63 #[serde(default)]
65 pub description: Option<String>,
66 pub variants: Vec<Variant>,
69 pub default_variant: Variant,
71 #[serde(default)]
73 pub rules: Vec<Rule>,
74 #[serde(default = "default_true")]
77 pub enabled: bool,
78}
79
80fn default_true() -> bool {
81 true
82}
83
84impl Flag {
85 pub fn validate(&self) -> Result<(), FeatureFlagError> {
87 if self.id.is_empty() {
88 return Err(FeatureFlagError::Invalid(
89 "flag.id must be non-empty".into(),
90 ));
91 }
92 if self.variants.is_empty() {
93 return Err(FeatureFlagError::Invalid(format!(
94 "flag {}: variants must be non-empty",
95 self.id
96 )));
97 }
98 let known: HashSet<&str> = self.variants.iter().map(String::as_str).collect();
99 if !known.contains(self.default_variant.as_str()) {
100 return Err(FeatureFlagError::Invalid(format!(
101 "flag {}: default_variant {:?} not in variants",
102 self.id, self.default_variant
103 )));
104 }
105 for rule in &self.rules {
106 match &rule.outcome {
107 Outcome::Variant { variant } => {
108 if !known.contains(variant.as_str()) {
109 return Err(FeatureFlagError::Invalid(format!(
110 "flag {}: rule {:?} references unknown variant {:?}",
111 self.id, rule.id, variant
112 )));
113 }
114 }
115 Outcome::Rollout { variants } => {
116 let total: u32 = variants.iter().map(|e| e.weight).sum();
117 if total != 100 {
118 return Err(FeatureFlagError::Invalid(format!(
119 "flag {}: rule {:?} rollout weights total {} (must be 100)",
120 self.id, rule.id, total
121 )));
122 }
123 for entry in variants {
124 if !known.contains(entry.variant.as_str()) {
125 return Err(FeatureFlagError::Invalid(format!(
126 "flag {}: rule {:?} references unknown variant {:?}",
127 self.id, rule.id, entry.variant
128 )));
129 }
130 }
131 }
132 }
133 }
134 Ok(())
135 }
136}
137
138#[derive(Clone, Debug, Default, Deserialize, Serialize)]
141pub struct FlagSet {
142 #[serde(default)]
144 pub version: String,
145 pub flags: Vec<Flag>,
147}
148
149impl FlagSet {
150 pub fn from_json(raw: &str) -> Result<Self, FeatureFlagError> {
152 let parsed: Self = serde_json::from_str(raw)?;
153 parsed.validate()?;
154 Ok(parsed)
155 }
156
157 pub fn validate(&self) -> Result<(), FeatureFlagError> {
159 let mut seen: HashSet<&str> = HashSet::new();
160 for flag in &self.flags {
161 if !seen.insert(flag.id.as_str()) {
162 return Err(FeatureFlagError::Invalid(format!(
163 "duplicate flag id: {}",
164 flag.id
165 )));
166 }
167 flag.validate()?;
168 }
169 Ok(())
170 }
171
172 #[must_use]
175 pub fn index(&self) -> HashMap<&str, &Flag> {
176 self.flags.iter().map(|f| (f.id.as_str(), f)).collect()
177 }
178}