unleash_types/
client_features.rs

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
81// I know this looks silly but it's also important for two reasons:
82// The first is that the client spec tests have a test case that has a context defined like:
83// {
84//   "properties": {
85//      "someValue": null
86//    }
87// }
88// Passing around an Option<HashMap<String, Option<String>>> is awful and unnecessary, we should scrub ingested data
89// before trying to execute our logic, so we scrub out those empty values instead, they do nothing useful for us.
90// The second reason is that we can't shield the Rust code from consumers using the FFI layers and potentially doing
91// exactly the same thing in languages that allow it. They should not do that. But if they do we have enough information
92// to understand the intent of the executed code clearly and there's no reason to fail
93fn 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
109///
110/// We need this to ensure that ClientFeatures gets a deterministic serialization.
111fn 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    ///
471    /// Returns a base64 encoded xx3_128 hash for the json representation of ClientFeatures
472    ///
473    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    /// Event for a feature update.
485    FeatureUpdated {
486        #[serde(rename = "eventId")]
487        event_id: u32,
488        feature: ClientFeature,
489    },
490    /// Event for a feature removal.
491    #[serde(rename_all = "camelCase")]
492    FeatureRemoved {
493        event_id: u32,
494        feature_name: String,
495        project: String,
496    },
497    /// Event for a segment update.
498    SegmentUpdated {
499        #[serde(rename = "eventId")]
500        event_id: u32,
501        segment: Segment,
502    },
503    /// Event for a segment removal.
504    #[serde(rename_all = "camelCase")]
505    SegmentRemoved { event_id: u32, segment_id: i32 },
506    /// Hydration event for features and segments.
507    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/// Schema for delta updates of feature configurations.
528#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
529#[serde(rename_all = "camelCase")]
530#[cfg_attr(feature = "openapi", derive(ToSchema))]
531pub struct ClientFeaturesDelta {
532    /// A list of delta events.
533    pub events: Vec<DeltaEvent>,
534}
535
536impl ClientFeatures {
537    /// Modifies the current ClientFeatures instance by applying the events.
538    pub fn apply_delta(&mut self, delta: &ClientFeaturesDelta) {
539        self.apply_delta_events(delta);
540    }
541
542    /// Returns a new ClientFeatures instance with the events applied.
543    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}