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 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
150fn 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 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 FeatureUpdated {
530 #[serde(rename = "eventId")]
531 event_id: u32,
532 feature: ClientFeature,
533 },
534 #[serde(rename_all = "camelCase")]
536 FeatureRemoved {
537 event_id: u32,
538 feature_name: String,
539 project: String,
540 },
541 SegmentUpdated {
543 #[serde(rename = "eventId")]
544 event_id: u32,
545 segment: Segment,
546 },
547 #[serde(rename_all = "camelCase")]
549 SegmentRemoved { event_id: u32, segment_id: i32 },
550 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#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
573#[serde(rename_all = "camelCase")]
574#[cfg_attr(feature = "openapi", derive(ToSchema))]
575pub struct ClientFeaturesDelta {
576 pub events: Vec<DeltaEvent>,
578}
579
580impl ClientFeatures {
581 pub fn apply_delta(&mut self, delta: &ClientFeaturesDelta) {
583 self.apply_delta_events(delta);
584 }
585
586 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}