1#[cfg(feature = "hashes")]
2use base64::Engine;
3use std::collections::HashMap;
4use std::hash::{Hash, Hasher};
5use std::{cmp::Ordering, collections::BTreeMap};
6#[cfg(feature = "openapi")]
7use utoipa::{IntoParams, ToSchema};
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11#[cfg(feature = "hashes")]
12use xxhash_rust::xxh3::xxh3_128;
13
14use crate::{Deduplicate, Merge, Upsert};
15
16#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
17#[cfg_attr(feature = "openapi", derive(ToSchema, IntoParams))]
18#[serde(rename_all = "camelCase")]
19pub struct Query {
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub tags: Option<Vec<Vec<String>>>,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub projects: Option<Vec<String>>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub name_prefix: Option<String>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub environment: Option<String>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub inline_segment_constraints: Option<bool>,
30}
31
32#[derive(Serialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
33#[cfg_attr(feature = "openapi", derive(ToSchema))]
34#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
35pub enum Operator {
36 NotIn,
37 In,
38 StrEndsWith,
39 StrStartsWith,
40 StrContains,
41 NumEq,
42 NumGt,
43 NumGte,
44 NumLt,
45 NumLte,
46 DateAfter,
47 DateBefore,
48 SemverEq,
49 SemverLt,
50 SemverGt,
51 Unknown(String),
52}
53
54#[derive(Deserialize, Serialize, Debug, Clone)]
55#[cfg_attr(feature = "openapi", derive(ToSchema, IntoParams))]
56#[cfg_attr(feature = "openapi", into_params(style = Form, parameter_in = Query))]
57#[serde(rename_all = "camelCase")]
58pub struct Context {
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub user_id: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub session_id: Option<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub environment: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub app_name: Option<String>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub current_time: Option<String>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub remote_address: Option<String>,
71 #[serde(default)]
72 #[serde(
73 deserialize_with = "remove_null_properties",
74 serialize_with = "optional_ordered_map",
75 skip_serializing_if = "Option::is_none"
76 )]
77 #[cfg_attr(feature = "openapi", param(style = Form, explode = false, value_type = Object))]
78 pub properties: Option<HashMap<String, String>>,
79}
80
81fn remove_null_properties<'de, D>(
94 deserializer: D,
95) -> Result<Option<HashMap<String, String>>, D::Error>
96where
97 D: Deserializer<'de>,
98{
99 let props: Option<HashMap<String, Option<String>>> = Option::deserialize(deserializer)?;
100 Ok(props.map(|props| {
101 props
102 .into_iter()
103 .filter(|x| x.1.is_some())
104 .map(|x| (x.0, x.1.unwrap()))
105 .collect()
106 }))
107}
108
109fn optional_ordered_map<S>(
112 value: &Option<HashMap<String, String>>,
113 serializer: S,
114) -> Result<S::Ok, S::Error>
115where
116 S: Serializer,
117{
118 match value {
119 Some(m) => {
120 let ordered: BTreeMap<_, _> = m.iter().collect();
121 ordered.serialize(serializer)
122 }
123 None => serializer.serialize_none(),
124 }
125}
126
127impl Default for Context {
128 fn default() -> Self {
129 Self {
130 user_id: None,
131 session_id: None,
132 environment: None,
133 current_time: None,
134 app_name: None,
135 remote_address: None,
136 properties: Some(HashMap::new()),
137 }
138 }
139}
140
141impl<'de> Deserialize<'de> for Operator {
142 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
143 where
144 D: Deserializer<'de>,
145 {
146 let s = String::deserialize(deserializer)?;
147 Ok(match s.as_str() {
148 "NOT_IN" => Operator::NotIn,
149 "IN" => Operator::In,
150 "STR_ENDS_WITH" => Operator::StrEndsWith,
151 "STR_STARTS_WITH" => Operator::StrStartsWith,
152 "STR_CONTAINS" => Operator::StrContains,
153 "NUM_EQ" => Operator::NumEq,
154 "NUM_GT" => Operator::NumGt,
155 "NUM_GTE" => Operator::NumGte,
156 "NUM_LT" => Operator::NumLt,
157 "NUM_LTE" => Operator::NumLte,
158 "DATE_AFTER" => Operator::DateAfter,
159 "DATE_BEFORE" => Operator::DateBefore,
160 "SEMVER_EQ" => Operator::SemverEq,
161 "SEMVER_LT" => Operator::SemverLt,
162 "SEMVER_GT" => Operator::SemverGt,
163 _ => Operator::Unknown(s),
164 })
165 }
166}
167
168#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
169#[cfg_attr(feature = "openapi", derive(ToSchema))]
170#[serde(rename_all = "camelCase")]
171pub struct Constraint {
172 pub context_name: String,
173 pub operator: Operator,
174 #[serde(default)]
175 pub case_insensitive: bool,
176 #[serde(default)]
177 pub inverted: bool,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 pub values: Option<Vec<String>>,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub value: Option<String>,
182}
183
184#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
185#[cfg_attr(feature = "openapi", derive(ToSchema))]
186#[serde(rename_all = "camelCase")]
187pub enum WeightType {
188 Fix,
189 Variable,
190}
191
192#[derive(Serialize, Deserialize, Debug, Clone, Eq)]
193#[cfg_attr(feature = "openapi", derive(ToSchema))]
194#[serde(rename_all = "camelCase")]
195pub struct Strategy {
196 pub name: String,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub sort_order: Option<i32>,
199 #[serde(skip_serializing_if = "Option::is_none")]
200 pub segments: Option<Vec<i32>>,
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub constraints: Option<Vec<Constraint>>,
203 #[serde(
204 serialize_with = "optional_ordered_map",
205 skip_serializing_if = "Option::is_none"
206 )]
207 pub parameters: Option<HashMap<String, String>>,
208 #[serde(serialize_with = "serialize_option_vec")]
209 pub variants: Option<Vec<StrategyVariant>>,
210}
211
212fn serialize_option_vec<S, T>(value: &Option<Vec<T>>, serializer: S) -> Result<S::Ok, S::Error>
213where
214 S: Serializer,
215 T: Serialize,
216{
217 match value {
218 Some(ref v) => v.serialize(serializer),
219 None => Vec::<T>::new().serialize(serializer),
220 }
221}
222
223impl PartialEq for Strategy {
224 fn eq(&self, other: &Self) -> bool {
225 self.name == other.name
226 && self.sort_order == other.sort_order
227 && self.segments == other.segments
228 && self.constraints == other.constraints
229 && self.parameters == other.parameters
230 }
231}
232impl PartialOrd for Strategy {
233 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
234 Some(self.cmp(other))
235 }
236}
237impl Ord for Strategy {
238 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
239 match self.sort_order.cmp(&other.sort_order) {
240 Ordering::Equal => self.name.cmp(&other.name),
241 ord => ord,
242 }
243 }
244}
245
246#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
247#[cfg_attr(feature = "openapi", derive(ToSchema))]
248#[serde(rename_all = "camelCase")]
249pub struct Override {
250 pub context_name: String,
251 pub values: Vec<String>,
252}
253
254#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
255#[cfg_attr(feature = "openapi", derive(ToSchema))]
256pub struct Payload {
257 #[serde(rename = "type")]
258 pub payload_type: String,
259 pub value: String,
260}
261#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
262#[cfg_attr(feature = "openapi", derive(ToSchema))]
263#[serde(rename_all = "camelCase")]
264pub struct Variant {
265 pub name: String,
266 pub weight: i32,
267 #[serde(skip_serializing_if = "Option::is_none")]
268 pub weight_type: Option<WeightType>,
269 #[serde(skip_serializing_if = "Option::is_none")]
270 pub stickiness: Option<String>,
271 #[serde(skip_serializing_if = "Option::is_none")]
272 pub payload: Option<Payload>,
273 #[serde(skip_serializing_if = "Option::is_none")]
274 pub overrides: Option<Vec<Override>>,
275}
276
277#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
278#[cfg_attr(feature = "openapi", derive(ToSchema))]
279#[serde(rename_all = "camelCase")]
280pub struct StrategyVariant {
281 pub name: String,
282 pub weight: i32,
283 #[serde(skip_serializing_if = "Option::is_none")]
284 pub payload: Option<Payload>,
285 #[serde(skip_serializing_if = "Option::is_none")]
286 pub stickiness: Option<String>,
287}
288
289impl PartialOrd for Variant {
290 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
291 Some(self.cmp(other))
292 }
293}
294impl Ord for Variant {
295 fn cmp(&self, other: &Self) -> Ordering {
296 self.name.cmp(&other.name)
297 }
298}
299
300#[derive(Serialize, Deserialize, Debug, Clone, Eq)]
301#[cfg_attr(feature = "openapi", derive(ToSchema))]
302#[serde(rename_all = "camelCase")]
303pub struct Segment {
304 pub id: i32,
305 pub constraints: Vec<Constraint>,
306}
307
308impl PartialEq for Segment {
309 fn eq(&self, other: &Self) -> bool {
310 self.id == other.id
311 }
312}
313
314impl PartialOrd for Segment {
315 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
316 Some(self.cmp(other))
317 }
318}
319
320impl Ord for Segment {
321 fn cmp(&self, other: &Self) -> Ordering {
322 self.id.cmp(&other.id)
323 }
324}
325
326impl Hash for Segment {
327 fn hash<H: Hasher>(&self, state: &mut H) {
328 self.id.hash(state);
329 }
330}
331
332#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
333#[cfg_attr(feature = "openapi", derive(ToSchema))]
334#[serde(rename_all = "camelCase")]
335pub struct FeatureDependency {
336 pub feature: String,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 pub enabled: Option<bool>,
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub variants: Option<Vec<String>>,
341}
342
343#[derive(Serialize, Deserialize, Debug, Clone, Eq, Default)]
344#[cfg_attr(feature = "openapi", derive(ToSchema))]
345#[serde(rename_all = "camelCase")]
346pub struct ClientFeature {
347 pub name: String,
348 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
349 pub feature_type: Option<String>,
350 #[serde(skip_serializing_if = "Option::is_none")]
351 pub description: Option<String>,
352 #[serde(skip_serializing_if = "Option::is_none")]
353 pub created_at: Option<DateTime<Utc>>,
354 #[serde(skip_serializing_if = "Option::is_none")]
355 pub last_seen_at: Option<DateTime<Utc>>,
356 pub enabled: bool,
357 #[serde(skip_serializing_if = "Option::is_none")]
358 pub stale: Option<bool>,
359 #[serde(skip_serializing_if = "Option::is_none")]
360 pub impression_data: Option<bool>,
361 #[serde(skip_serializing_if = "Option::is_none")]
362 pub project: Option<String>,
363 #[serde(skip_serializing_if = "Option::is_none")]
364 pub strategies: Option<Vec<Strategy>>,
365 #[serde(skip_serializing_if = "Option::is_none")]
366 pub variants: Option<Vec<Variant>>,
367 #[serde(skip_serializing_if = "Option::is_none")]
368 pub dependencies: Option<Vec<FeatureDependency>>,
369}
370
371#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
372#[cfg_attr(feature = "openapi", derive(ToSchema))]
373#[serde(rename_all = "camelCase")]
374pub struct Meta {
375 pub etag: Option<String>,
376 pub revision_id: Option<usize>,
377 pub query_hash: Option<String>,
378}
379
380impl Merge for ClientFeatures {
381 fn merge(self, other: Self) -> Self {
382 let mut features = self.features.merge(other.features);
383 features.sort();
384 let segments = match (self.segments, other.segments) {
385 (Some(mut s), Some(o)) => {
386 s.extend(o);
387 Some(s.deduplicate())
388 }
389 (Some(s), None) => Some(s),
390 (None, Some(o)) => Some(o),
391 (None, None) => None,
392 };
393 ClientFeatures {
394 version: self.version.max(other.version),
395 features,
396 segments: segments.map(|mut s| {
397 s.sort();
398 s
399 }),
400 query: self.query.or(other.query),
401 meta: other.meta.or(self.meta),
402 }
403 }
404}
405
406impl Upsert for ClientFeatures {
407 fn upsert(self, other: Self) -> Self {
408 let mut features = self.features.upsert(other.features);
409 features.sort();
410 let segments = match (self.segments, other.segments) {
411 (Some(s), Some(mut o)) => {
412 o.extend(s);
413 Some(o.deduplicate())
414 }
415 (Some(s), None) => Some(s),
416 (None, Some(o)) => Some(o),
417 (None, None) => None,
418 };
419 ClientFeatures {
420 version: self.version.max(other.version),
421 features,
422 segments: segments.map(|mut s| {
423 s.sort();
424 s
425 }),
426 query: self.query.or(other.query),
427 meta: other.meta.or(self.meta),
428 }
429 }
430}
431
432impl PartialOrd for ClientFeature {
433 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
434 Some(self.cmp(other))
435 }
436}
437
438impl Ord for ClientFeature {
439 fn cmp(&self, other: &Self) -> Ordering {
440 self.name.cmp(&other.name)
441 }
442}
443
444impl PartialEq for ClientFeature {
445 fn eq(&self, other: &Self) -> bool {
446 self.name == other.name
447 }
448}
449
450impl Hash for ClientFeature {
451 fn hash<H: Hasher>(&self, state: &mut H) {
452 self.name.hash(state);
453 }
454}
455
456#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
457#[cfg_attr(feature = "openapi", derive(ToSchema))]
458pub struct ClientFeatures {
459 pub version: u32,
460 pub features: Vec<ClientFeature>,
461 #[serde(skip_serializing_if = "Option::is_none")]
462 pub segments: Option<Vec<Segment>>,
463 pub query: Option<Query>,
464 #[serde(skip_serializing_if = "Option::is_none")]
465 pub meta: Option<Meta>,
466}
467
468#[cfg(feature = "hashes")]
469impl ClientFeatures {
470 pub fn xx3_hash(&self) -> Result<String, serde_json::Error> {
474 serde_json::to_string(self)
475 .map(|s| xxh3_128(s.as_bytes()))
476 .map(|xxh_hash| base64::prelude::BASE64_URL_SAFE.encode(xxh_hash.to_le_bytes()))
477 }
478}
479
480#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
481#[serde(tag = "type", rename_all = "kebab-case")]
482#[cfg_attr(feature = "openapi", derive(ToSchema))]
483pub enum DeltaEvent {
484 FeatureUpdated {
486 #[serde(rename = "eventId")]
487 event_id: u32,
488 feature: ClientFeature,
489 },
490 #[serde(rename_all = "camelCase")]
492 FeatureRemoved {
493 event_id: u32,
494 feature_name: String,
495 project: String,
496 },
497 SegmentUpdated {
499 #[serde(rename = "eventId")]
500 event_id: u32,
501 segment: Segment,
502 },
503 #[serde(rename_all = "camelCase")]
505 SegmentRemoved { event_id: u32, segment_id: i32 },
506 Hydration {
508 #[serde(rename = "eventId")]
509 event_id: u32,
510 features: Vec<ClientFeature>,
511 segments: Vec<Segment>,
512 },
513}
514
515impl DeltaEvent {
516 pub fn get_event_id(&self) -> u32 {
517 match self {
518 DeltaEvent::FeatureUpdated { event_id, .. }
519 | DeltaEvent::FeatureRemoved { event_id, .. }
520 | DeltaEvent::SegmentUpdated { event_id, .. }
521 | DeltaEvent::SegmentRemoved { event_id, .. }
522 | DeltaEvent::Hydration { event_id, .. } => *event_id,
523 }
524 }
525}
526
527#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
529#[serde(rename_all = "camelCase")]
530#[cfg_attr(feature = "openapi", derive(ToSchema))]
531pub struct ClientFeaturesDelta {
532 pub events: Vec<DeltaEvent>,
534}
535
536impl ClientFeatures {
537 pub fn apply_delta(&mut self, delta: &ClientFeaturesDelta) {
539 self.apply_delta_events(delta);
540 }
541
542 pub fn create_from_delta(delta: &ClientFeaturesDelta) -> ClientFeatures {
544 let mut client_features = ClientFeatures::default();
545 client_features.apply_delta_events(delta);
546 client_features
547 }
548
549 fn apply_delta_events(&mut self, delta: &ClientFeaturesDelta) {
550 let segments = &mut self.segments;
551 let features = &mut self.features;
552 for event in &delta.events {
553 match event {
554 DeltaEvent::FeatureUpdated { feature, .. } => {
555 if let Some(existing) = features.iter_mut().find(|f| f.name == feature.name) {
556 *existing = feature.clone();
557 } else {
558 features.push(feature.clone());
559 }
560 }
561 DeltaEvent::FeatureRemoved { feature_name, .. } => {
562 features.retain(|f| f.name != *feature_name);
563 }
564 DeltaEvent::SegmentUpdated { segment, .. } => {
565 let segments_list = segments.get_or_insert_with(Vec::new);
566 if let Some(existing) = segments_list.iter_mut().find(|s| s.id == segment.id) {
567 *existing = segment.clone();
568 } else {
569 segments_list.push(segment.clone());
570 }
571 }
572 DeltaEvent::SegmentRemoved { segment_id, .. } => {
573 if let Some(segments_list) = segments {
574 segments_list.retain(|s| s.id != *segment_id);
575 }
576 }
577 DeltaEvent::Hydration {
578 features: new_features,
579 segments: new_segments,
580 ..
581 } => {
582 *features = new_features.clone();
583 *segments = Some(new_segments.clone());
584 }
585 }
586 }
587
588 features.sort();
589 }
590}
591
592impl Default for ClientFeatures {
593 fn default() -> Self {
594 Self {
595 version: 2,
596 features: vec![],
597 segments: None,
598 query: None,
599 meta: None,
600 }
601 }
602}
603
604impl From<ClientFeaturesDelta> for ClientFeatures {
605 fn from(value: ClientFeaturesDelta) -> Self {
606 ClientFeatures::create_from_delta(&value)
607 }
608}
609
610impl From<&ClientFeaturesDelta> for ClientFeatures {
611 fn from(value: &ClientFeaturesDelta) -> Self {
612 ClientFeatures::create_from_delta(value)
613 }
614}
615
616#[cfg(test)]
617mod tests {
618 use crate::{
619 client_features::{ClientFeature, ClientFeaturesDelta},
620 Merge, Upsert,
621 };
622 use serde_json::{from_reader, to_string};
623 use serde_qs::Config;
624 use std::{fs::File, io::BufReader, path::PathBuf};
625
626 use super::{ClientFeatures, Constraint, DeltaEvent, Operator, Segment, Strategy};
627 use crate::client_features::Context;
628 use test_case::test_case;
629
630 #[derive(Debug)]
631 pub enum EdgeError {
632 SomethingWentWrong,
633 }
634 #[test]
635 pub fn ordering_is_stable_for_constraints() {
636 let c1 = Constraint {
637 context_name: "acontext".into(),
638 operator: super::Operator::DateAfter,
639 case_insensitive: true,
640 inverted: false,
641 values: Some(vec![]),
642 value: None,
643 };
644 let c2 = Constraint {
645 context_name: "acontext".into(),
646 operator: super::Operator::DateBefore,
647 case_insensitive: false,
648 inverted: false,
649 values: None,
650 value: Some("value".into()),
651 };
652 let c3 = Constraint {
653 context_name: "bcontext".into(),
654 operator: super::Operator::NotIn,
655 case_insensitive: false,
656 inverted: false,
657 values: None,
658 value: None,
659 };
660 let mut v = vec![c3.clone(), c1.clone(), c2.clone()];
661 v.sort();
662 assert_eq!(v, vec![c1, c2, c3]);
663 }
664
665 fn read_file(path: PathBuf) -> Result<BufReader<File>, EdgeError> {
666 File::open(path)
667 .map_err(|_| EdgeError::SomethingWentWrong)
668 .map(BufReader::new)
669 }
670
671 #[test_case("./examples/features_with_variantType.json".into() ; "features with variantType")]
672 #[test_case("./examples/15-global-constraints.json".into(); "global-constraints")]
673 pub fn client_features_parsing_is_stable(path: PathBuf) {
674 let client_features: ClientFeatures =
675 serde_json::from_reader(read_file(path).unwrap()).unwrap();
676
677 let to_string = serde_json::to_string(&client_features).unwrap();
678 let reparsed_to_string: ClientFeatures = serde_json::from_str(to_string.as_str()).unwrap();
679 assert_eq!(client_features, reparsed_to_string);
680 }
681
682 #[cfg(feature = "hashes")]
683 #[test_case("./examples/features_with_variantType.json".into() ; "features with variantType")]
684 #[cfg(feature = "hashes")]
685 #[test_case("./examples/15-global-constraints.json".into(); "global-constraints")]
686 pub fn client_features_hashing_is_stable(path: PathBuf) {
687 let client_features: ClientFeatures =
688 serde_json::from_reader(read_file(path.clone()).unwrap()).unwrap();
689
690 let second_read: ClientFeatures =
691 serde_json::from_reader(read_file(path).unwrap()).unwrap();
692
693 let first_hash = client_features.xx3_hash().unwrap();
694 let second_hash = client_features.xx3_hash().unwrap();
695 assert_eq!(first_hash, second_hash);
696
697 let first_hash_from_second_read = second_read.xx3_hash().unwrap();
698 assert_eq!(first_hash, first_hash_from_second_read);
699 }
700
701 #[test]
702 fn merging_two_client_features_takes_both_feature_sets() {
703 let client_features_one = ClientFeatures {
704 version: 2,
705 features: vec![
706 ClientFeature {
707 name: "feature1".into(),
708 ..ClientFeature::default()
709 },
710 ClientFeature {
711 name: "feature2".into(),
712 ..ClientFeature::default()
713 },
714 ],
715 segments: None,
716 query: None,
717 meta: None,
718 };
719
720 let client_features_two = ClientFeatures {
721 version: 2,
722 features: vec![ClientFeature {
723 name: "feature3".into(),
724 ..ClientFeature::default()
725 }],
726 segments: None,
727 query: None,
728 meta: None,
729 };
730
731 let merged = client_features_one.merge(client_features_two);
732 assert_eq!(merged.features.len(), 3);
733 }
734
735 #[test]
736 fn upserting_client_features_prioritizes_new_data_but_keeps_uniques() {
737 let client_features_one = ClientFeatures {
738 version: 2,
739 features: vec![
740 ClientFeature {
741 name: "feature1".into(),
742 ..ClientFeature::default()
743 },
744 ClientFeature {
745 name: "feature2".into(),
746 ..ClientFeature::default()
747 },
748 ],
749 segments: None,
750 query: None,
751 meta: None,
752 };
753 let mut updated_strategies = client_features_one.clone();
754 let updated_feature_one_with_strategy = ClientFeature {
755 name: "feature1".into(),
756 strategies: Some(vec![Strategy {
757 name: "default".into(),
758 sort_order: Some(124),
759 segments: None,
760 constraints: None,
761 parameters: None,
762 variants: None,
763 }]),
764 ..ClientFeature::default()
765 };
766 let feature_the_third = ClientFeature {
767 name: "feature3".into(),
768 strategies: Some(vec![Strategy {
769 name: "default".into(),
770 sort_order: Some(124),
771 segments: None,
772 constraints: None,
773 parameters: None,
774 variants: None,
775 }]),
776 ..ClientFeature::default()
777 };
778 updated_strategies.features = vec![updated_feature_one_with_strategy, feature_the_third];
779 let updated_features = client_features_one.upsert(updated_strategies);
780 let client_features = updated_features.features;
781 assert_eq!(client_features.len(), 3);
782 let updated_feature_one = client_features
783 .iter()
784 .find(|f| f.name == "feature1")
785 .unwrap();
786 assert_eq!(updated_feature_one.strategies.as_ref().unwrap().len(), 1);
787 assert!(client_features.iter().any(|f| f.name == "feature3"));
788 assert!(client_features.iter().any(|f| f.name == "feature2"));
789 }
790
791 #[test]
792 pub fn can_parse_properties_map_from_get_query_string() {
793 let config = Config::new(5, false);
794 let query_string =
795 "userId=123123&properties[email]=test@test.com&properties%5BcompanyId%5D=bricks&properties%5Bhello%5D=world";
796 let context: Context = config
797 .deserialize_str(query_string)
798 .expect("Could not parse query string");
799 assert_eq!(context.user_id, Some("123123".to_string()));
800 let prop_map = context.properties.unwrap();
801 assert_eq!(prop_map.len(), 3);
802 assert!(prop_map.contains_key("companyId"));
803 assert!(prop_map.contains_key("hello"));
804 assert!(prop_map.contains_key("email"));
805 }
806
807 #[test_case("./examples/delta_base.json".into(), "./examples/delta_patch.json".into(); "Base and delta")]
808 pub fn can_take_delta_updates(base: PathBuf, delta: PathBuf) {
809 let base_delta: ClientFeaturesDelta = from_reader(read_file(base).unwrap()).unwrap();
810 let mut features = ClientFeatures {
811 version: 2,
812 features: vec![],
813 segments: None,
814 query: None,
815 meta: None,
816 };
817 features.apply_delta(&base_delta);
818 assert_eq!(features.features.len(), 3);
819 let delta: ClientFeaturesDelta = from_reader(read_file(delta).unwrap()).unwrap();
820 features.apply_delta(&delta);
821 assert_eq!(features.features.len(), 2);
822 }
823
824 #[test_case("./examples/delta_base.json".into(), "./examples/delta_patch.json".into(); "Base and delta")]
825 pub fn validate_delta_updates(base_path: PathBuf, delta_path: PathBuf) {
826 let base_delta: ClientFeaturesDelta = from_reader(read_file(base_path).unwrap()).unwrap();
827
828 let mut updated_features = ClientFeatures::create_from_delta(&base_delta);
829 let expected_feature_count = base_delta
830 .events
831 .iter()
832 .filter(|event| matches!(event, DeltaEvent::FeatureUpdated { .. }))
833 .count();
834 assert_eq!(updated_features.features.len(), expected_feature_count);
835
836 let delta_update: ClientFeaturesDelta =
837 from_reader(read_file(delta_path).unwrap()).unwrap();
838 updated_features.apply_delta(&delta_update);
839
840 let mut sorted_delta_features: Vec<ClientFeature> = delta_update
841 .events
842 .iter()
843 .filter_map(|event| {
844 if let DeltaEvent::FeatureUpdated { feature, .. } = event {
845 Some(feature.clone())
846 } else {
847 None
848 }
849 })
850 .collect();
851 sorted_delta_features.sort();
852
853 let serialized_delta_updates = to_string(&sorted_delta_features).unwrap();
854 let serialized_final_features = to_string(&updated_features.features).unwrap();
855
856 assert_eq!(serialized_delta_updates, serialized_final_features);
857 }
858
859 #[test]
860 pub fn when_strategy_variants_is_none_default_to_empty_vec() {
861 let client_features = ClientFeatures {
862 version: 2,
863 features: vec![ClientFeature {
864 name: "feature1".into(),
865 strategies: Some(vec![Strategy {
866 name: "default".into(),
867 sort_order: Some(124),
868 segments: None,
869 constraints: None,
870 parameters: None,
871 variants: None,
872 }]),
873 ..ClientFeature::default()
874 }],
875 segments: None,
876 query: None,
877 meta: None,
878 };
879 let client_features_json = serde_json::to_string(&client_features).unwrap();
880 let client_features_parsed: ClientFeatures =
881 serde_json::from_str(&client_features_json).unwrap();
882 let strategy = client_features_parsed
883 .features
884 .first()
885 .unwrap()
886 .strategies
887 .as_ref()
888 .unwrap()
889 .first()
890 .unwrap();
891 assert_eq!(strategy.variants.as_ref().unwrap().len(), 0);
892 }
893
894 #[test]
895 pub fn upserting_features_with_segments_overrides_constraints_on_segments_with_same_id_but_keeps_non_overlapping_segments(
896 ) {
897 let client_features_one = ClientFeatures {
898 version: 2,
899 features: vec![],
900 segments: Some(vec![
901 Segment {
902 constraints: vec![Constraint {
903 case_insensitive: false,
904 values: None,
905 context_name: "location".into(),
906 inverted: false,
907 operator: Operator::In,
908 value: Some("places".into()),
909 }],
910 id: 1,
911 },
912 Segment {
913 constraints: vec![Constraint {
914 case_insensitive: false,
915 values: None,
916 context_name: "hometown".into(),
917 inverted: false,
918 operator: Operator::In,
919 value: Some("somewhere_nice".into()),
920 }],
921 id: 2,
922 },
923 ]),
924 query: None,
925 meta: None,
926 };
927 let client_features_two = ClientFeatures {
928 version: 2,
929 features: vec![],
930 segments: Some(vec![
931 Segment {
932 constraints: vec![Constraint {
933 case_insensitive: false,
934 values: None,
935 context_name: "location".into(),
936 inverted: false,
937 operator: Operator::In,
938 value: Some("other-places".into()),
939 }],
940 id: 1,
941 },
942 Segment {
943 constraints: vec![Constraint {
944 case_insensitive: false,
945 values: None,
946 context_name: "origin".into(),
947 inverted: false,
948 operator: Operator::In,
949 value: Some("africa".into()),
950 }],
951 id: 3,
952 },
953 ]),
954 query: None,
955 meta: None,
956 };
957
958 let expected = vec![
959 Constraint {
960 case_insensitive: false,
961 values: None,
962 context_name: "hometown".into(),
963 inverted: false,
964 operator: Operator::In,
965 value: Some("somewhere_nice".into()),
966 },
967 Constraint {
968 case_insensitive: false,
969 values: None,
970 context_name: "location".into(),
971 inverted: false,
972 operator: Operator::In,
973 value: Some("other-places".into()),
974 },
975 Constraint {
976 case_insensitive: false,
977 values: None,
978 context_name: "origin".into(),
979 inverted: false,
980 operator: Operator::In,
981 value: Some("africa".into()),
982 },
983 ];
984
985 let upserted = client_features_one
986 .clone()
987 .upsert(client_features_two.clone());
988 let mut new_constraints = upserted
989 .segments
990 .unwrap()
991 .iter()
992 .flat_map(|segment| segment.constraints.clone())
993 .collect::<Vec<Constraint>>();
994 new_constraints.sort_by(|a, b| a.context_name.cmp(&b.context_name));
995
996 assert_eq!(new_constraints, expected);
997 }
998
999 #[test]
1000 pub fn when_meta_is_in_client_features_it_is_serialized() {
1001 let client_features = ClientFeatures {
1002 version: 2,
1003 features: vec![],
1004 segments: None,
1005 query: None,
1006 meta: Some(super::Meta {
1007 etag: Some("123:wqeqwe".into()),
1008 revision_id: Some(123),
1009 query_hash: Some("wqeqwe".into()),
1010 }),
1011 };
1012 let serialized = serde_json::to_string(&client_features).unwrap();
1013 assert!(serialized.contains("meta"));
1014 }
1015
1016 #[test_case("./examples/nuno-response.json".into() ; "features with meta tag")]
1017 pub fn can_parse_meta_from_upstream(path: PathBuf) {
1018 let features: ClientFeatures = serde_json::from_reader(read_file(path).unwrap()).unwrap();
1019 assert!(features.meta.is_some());
1020 let meta = features.meta.unwrap();
1021 assert_eq!(meta.etag, Some("\"537b2ba0:3726\"".into()));
1022 assert_eq!(meta.revision_id, Some(3726));
1023 assert_eq!(meta.query_hash, Some("537b2ba0".into()));
1024 }
1025}