flagsmith_flag_engine/features/
mod.rs1use super::utils;
2use super::utils::hashing;
3use serde::{Deserialize, Serialize};
4
5use super::types::FlagsmithValue;
6
7#[derive(Eq, PartialEq, Hash, Serialize, Deserialize, Clone, Debug)]
8pub struct Feature {
9 pub id: u32,
10 pub name: String,
11 #[serde(rename = "type")]
12 feature_type: Option<String>,
13}
14
15#[derive(Serialize, Deserialize, Clone, Debug)]
16pub struct MultivariateFeatureOption {
17 pub value: FlagsmithValue,
18 pub id: Option<u32>,
19}
20
21#[derive(Serialize, Deserialize, Clone, Debug)]
22pub struct MultivariateFeatureStateValue {
23 pub multivariate_feature_option: MultivariateFeatureOption,
24 pub percentage_allocation: f32,
25 pub id: Option<u32>,
26
27 #[serde(default = "utils::get_uuid")]
28 pub mv_fs_value_uuid: String,
29}
30
31#[derive(Serialize, Deserialize, Clone, Debug)]
32pub struct FeatureSegment {
33 pub priority: u32,
34}
35
36#[derive(Serialize, Deserialize, Clone, Debug)]
37pub struct FeatureState {
38 pub feature: Feature,
39 pub enabled: bool,
40 pub django_id: Option<u32>,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub feature_segment: Option<FeatureSegment>,
44
45 #[serde(default = "utils::get_uuid")]
46 pub featurestate_uuid: String,
47
48 #[serde(default)]
49 pub multivariate_feature_state_values: Vec<MultivariateFeatureStateValue>,
50
51 #[serde(rename = "feature_state_value")]
52 value: FlagsmithValue,
53}
54
55impl FeatureState {
56 pub fn get_value(&self, identity_id: Option<&str>) -> FlagsmithValue {
57 let value = match identity_id {
58 Some(id) if self.multivariate_feature_state_values.len() > 0 => {
59 self.get_multivariate_value(id)
60 }
61 _ => self.value.clone(),
62 };
63 return value;
64 }
65
66 pub fn is_higher_segment_priority(&self, other: &FeatureState) -> bool {
76 match &other.feature_segment {
77 None if self.feature_segment.is_some() => true,
78 Some(feature_segment) if self.feature_segment.is_some() => {
79 self.feature_segment.as_ref().unwrap().priority < feature_segment.priority
80 }
81 _ => false,
82 }
83 }
84 fn get_multivariate_value(&self, identity_id: &str) -> FlagsmithValue {
85 let object_id = match self.django_id {
86 Some(django_id) => django_id.to_string(),
87 None => self.featurestate_uuid.clone(),
88 };
89 let percentage_value =
90 hashing::get_hashed_percentage_for_object_ids(vec![&object_id, identity_id], 1);
91 let mut start_percentage = 0.0;
92 let mut mv_fs_values = self.multivariate_feature_state_values.clone();
98 mv_fs_values.sort_by_key(|mv_fs_value| match mv_fs_value.id {
99 Some(id) => id.to_string(),
100 _ => mv_fs_value.mv_fs_value_uuid.clone(),
101 });
102 for mv_value in mv_fs_values {
103 let limit = mv_value.percentage_allocation + start_percentage;
104 if start_percentage <= percentage_value && percentage_value < limit {
105 return mv_value.multivariate_feature_option.value;
106 }
107
108 start_percentage = limit
109 }
110 return self.value.clone();
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use rstest::*;
120
121 #[test]
122 fn deserializing_fs_creates_default_uuid_if_not_present() {
123 let feature_state_json = r#"{
124 "multivariate_feature_state_values": [
125 {
126 "id": 3404,
127 "multivariate_feature_option": {
128 "value": "baz"
129 },
130 "percentage_allocation": 30
131 }
132 ],
133 "feature_state_value": 1,
134 "django_id": 1,
135 "feature": {
136 "name": "feature1",
137 "type": null,
138 "id": 1
139 },
140 "segment_id": null,
141 "enabled": false
142 }"#;
143
144 let feature_state: FeatureState = serde_json::from_str(feature_state_json).unwrap();
145 assert_eq!(feature_state.featurestate_uuid.is_empty(), false);
146 assert_eq!(
147 feature_state.multivariate_feature_state_values[0]
148 .mv_fs_value_uuid
149 .is_empty(),
150 false
151 );
152 }
153
154 #[test]
155 fn serialize_and_deserialize_feature_state() {
156 let feature_state_json = serde_json::json!(
157 {
158 "multivariate_feature_state_values": [],
159 "feature_state_value": 1,
160 "featurestate_uuid":"a6ff815f-63ed-4e72-99dc-9124c442ce4d",
161 "django_id": 1,
162 "feature": {
163 "name": "feature1",
164 "type": null,
165 "id": 1
166 },
167 "enabled": false
168 }
169 );
170 let feature_state: FeatureState =
171 serde_json::from_value(feature_state_json.clone()).unwrap();
172
173 let given_json = serde_json::to_value(&feature_state).unwrap();
174 assert_eq!(given_json, feature_state_json)
175 }
176
177 #[test]
178 fn feature_state_is_higher_segment_priority() {
179 let feature_state_json = serde_json::json!(
181 {
182 "multivariate_feature_state_values": [],
183 "feature_state_value": 1,
184 "featurestate_uuid":"a6ff815f-63ed-4e72-99dc-9124c442ce4d",
185 "django_id": 1,
186 "feature": {
187 "name": "feature1",
188 "type": null,
189 "id": 1
190 },
191 "enabled": false
192 }
193 );
194 let mut feature_state_1: FeatureState =
195 serde_json::from_value(feature_state_json.clone()).unwrap();
196 let mut feature_state_2: FeatureState =
197 serde_json::from_value(feature_state_json.clone()).unwrap();
198
199 assert_eq!(
201 feature_state_1.is_higher_segment_priority(&feature_state_2),
202 false
203 );
204 assert_eq!(
205 feature_state_2.is_higher_segment_priority(&feature_state_1),
206 false
207 );
208
209 feature_state_2.feature_segment = Some(FeatureSegment { priority: 1 });
211
212 assert_eq!(
214 feature_state_1.is_higher_segment_priority(&feature_state_2),
215 false
216 );
217 assert_eq!(
219 feature_state_2.is_higher_segment_priority(&feature_state_1),
220 true
221 );
222
223 feature_state_1.feature_segment = Some(FeatureSegment { priority: 0 });
225 assert_eq!(
226 feature_state_1.is_higher_segment_priority(&feature_state_2),
227 true
228 );
229 assert_eq!(
230 feature_state_2.is_higher_segment_priority(&feature_state_1),
231 false
232 );
233 }
234
235 #[rstest]
236 #[case("2", "foo".to_string())] #[case("8", "bar".to_string())] #[case("1", "control".to_string())] fn feature_state_get_value_mv_values(
240 #[case] identity_id: &str,
241 #[case] expected_value: String,
242 ) {
243 let mv_feature_value_1 = "foo";
244 let mv_feature_value_2 = "bar";
245 let feature_state_json = serde_json::json!(
246 {
247 "multivariate_feature_state_values": [
248 {
249 "id": 1,
250 "multivariate_feature_option": {
251 "id":1,
252 "value": mv_feature_value_1
253 },
254 "percentage_allocation": 30
255 },
256 {
257 "id": 2,
258 "multivariate_feature_option": {
259 "id":2,
260 "value": mv_feature_value_2
261 },
262 "percentage_allocation": 30
263 }
264 ],
265 "feature_state_value": "control",
266 "featurestate_uuid":"a6ff815f-63ed-4e72-99dc-9124c442ce4d",
267 "django_id": 1,
268 "feature": {
269 "name": "feature1",
270 "type": null,
271 "id": 1
272 },
273 "enabled": true
274 }
275 );
276 let feature_state: FeatureState =
277 serde_json::from_value(feature_state_json.clone()).unwrap();
278 let value = feature_state.get_value(Some(identity_id));
279 assert_eq!(value.value, expected_value);
280 }
281
282 #[rstest]
283 fn feature_state_get_value_uses_django_id_for_multivariate_value_calculation_if_not_none() {
284 let mv_feature_value_1 = "foo";
285 let mv_feature_value_2 = "bar";
286 let fs_uuid = "a6ff815f-63ed-4e72-99dc-9124c442ce4d";
287 let feature_state_json = serde_json::json!(
288 {
289 "multivariate_feature_state_values": [
290 {
291 "id": 1,
292 "multivariate_feature_option": {
293 "id":1,
294 "value": mv_feature_value_1
295 },
296 "percentage_allocation": 30
297 },
298 {
299 "id": 2,
300 "multivariate_feature_option": {
301 "id":2,
302 "value": mv_feature_value_2
303 },
304 "percentage_allocation": 30
305 }
306 ],
307 "feature_state_value": "control",
308 "featurestate_uuid":fs_uuid,
309 "django_id": 1,
310 "feature": {
311 "name": "feature1",
312 "type": null,
313 "id": 1
314 },
315 "enabled": true
316 }
317 );
318 let feature_state: FeatureState =
320 serde_json::from_value(feature_state_json.clone()).unwrap();
321
322 let value = feature_state.get_value(Some("1"));
324 assert_eq!(value.value, "control".to_string());
328 }
329}