Skip to main content

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