From 3c717ef23688e878df6b8174b4e8bce908ea09c8 Mon Sep 17 00:00:00 2001
From: Mark Phelps <209477+markphelps@users.noreply.github.com>
Date: Mon, 22 Jul 2024 12:57:27 -0400
Subject: [PATCH 1/3] feat: add support for default variant eval

Signed-off-by: Mark Phelps <209477+markphelps@users.noreply.github.com>
---
 flipt-client-browser/src/index.ts     |  2 +-
 flipt-engine-ffi/src/parser/http.rs   |  2 +-
 flipt-evaluation/src/lib.rs           | 54 +++++++++++++++++++++++++++
 flipt-evaluation/src/models/flipt.rs  |  6 ++-
 flipt-evaluation/src/models/source.rs |  9 +++++
 flipt-evaluation/src/store/mod.rs     |  5 +++
 6 files changed, 74 insertions(+), 4 deletions(-)

diff --git a/flipt-client-browser/src/index.ts b/flipt-client-browser/src/index.ts
index ae7f72d6..6ed49945 100644
--- a/flipt-client-browser/src/index.ts
+++ b/flipt-client-browser/src/index.ts
@@ -46,7 +46,7 @@ export class FliptEvaluationClient {
 
     const headers = new Headers();
     headers.append('Accept', 'application/json');
-    headers.append('x-flipt-accept-server-version', '1.38.0');
+    headers.append('x-flipt-accept-server-version', '1.47.0');
 
     if (engine_opts.authentication) {
       if ('client_token' in engine_opts.authentication) {
diff --git a/flipt-engine-ffi/src/parser/http.rs b/flipt-engine-ffi/src/parser/http.rs
index af96322a..c6865a1e 100644
--- a/flipt-engine-ffi/src/parser/http.rs
+++ b/flipt-engine-ffi/src/parser/http.rs
@@ -132,7 +132,7 @@ impl Parser for HTTPParser {
         // version (or higher) that we can accept from the server
         headers.insert(
             "X-Flipt-Accept-Server-Version",
-            reqwest::header::HeaderValue::from_static("1.38.0"),
+            reqwest::header::HeaderValue::from_static("1.47.0"),
         );
 
         // add etag / if-none-match header if we have one
diff --git a/flipt-evaluation/src/lib.rs b/flipt-evaluation/src/lib.rs
index b432ecd0..7b2c5291 100644
--- a/flipt-evaluation/src/lib.rs
+++ b/flipt-evaluation/src/lib.rs
@@ -138,6 +138,13 @@ pub fn variant_evaluation(
     if !flag.enabled {
         variant_evaluation_response.reason = common::EvaluationReason::FlagDisabled;
         variant_evaluation_response.request_duration_millis = get_duration_millis(now)?;
+
+        if flag.default_variant.is_some() {
+            let default_variant = flag.default_variant.as_ref().unwrap();
+            variant_evaluation_response.variant_key = default_variant.key.clone();
+            variant_evaluation_response.variant_attachment = default_variant.attachment.clone();
+        }
+
         return Ok(variant_evaluation_response);
     }
 
@@ -152,6 +159,17 @@ pub fn variant_evaluation(
         }
     };
 
+    // if no rules and flag is enabled, return default variant
+    if evaluation_rules.is_empty() && flag.default_variant.is_some() {
+        let default_variant = flag.default_variant.as_ref().unwrap();
+        variant_evaluation_response.variant_key = default_variant.key.clone();
+        variant_evaluation_response.variant_attachment = default_variant.attachment.clone();
+        variant_evaluation_response.request_duration_millis = get_duration_millis(now)?;
+        variant_evaluation_response.reason = common::EvaluationReason::Default;
+
+        return Ok(variant_evaluation_response);
+    }
+
     for rule in evaluation_rules {
         if rule.rank < last_rank {
             return Err(Error::InvalidRequest(format!(
@@ -224,10 +242,18 @@ pub fn variant_evaluation(
         }
 
         // no distributions for the rule
+        // match is true here because it did match the segment/rule
         if valid_distributions.is_empty() {
             variant_evaluation_response.r#match = true;
             variant_evaluation_response.reason = common::EvaluationReason::Match;
             variant_evaluation_response.request_duration_millis = get_duration_millis(now)?;
+
+            if flag.default_variant.is_some() {
+                let default_variant = flag.default_variant.as_ref().unwrap();
+                variant_evaluation_response.variant_key = default_variant.key.clone();
+                variant_evaluation_response.variant_attachment = default_variant.attachment.clone();
+            }
+
             return Ok(variant_evaluation_response);
         }
 
@@ -242,9 +268,18 @@ pub fn variant_evaluation(
             Err(idx) => idx,
         };
 
+        // if index is outside of our existing buckets then it does not match any distribution
         if index == valid_distributions.len() {
             variant_evaluation_response.r#match = false;
             variant_evaluation_response.request_duration_millis = get_duration_millis(now)?;
+
+            if flag.default_variant.is_some() {
+                let default_variant = flag.default_variant.as_ref().unwrap();
+                variant_evaluation_response.variant_key = default_variant.key.clone();
+                variant_evaluation_response.variant_attachment = default_variant.attachment.clone();
+                variant_evaluation_response.reason = common::EvaluationReason::Default;
+            }
+
             return Ok(variant_evaluation_response);
         }
 
@@ -1006,6 +1041,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: true,
                 r#type: common::FlagType::Variant,
+                default_variant: None,
             })
         });
 
@@ -1071,6 +1107,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: true,
                 r#type: common::FlagType::Variant,
+                default_variant: None,
             })
         });
 
@@ -1145,6 +1182,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: true,
                 r#type: common::FlagType::Variant,
+                default_variant: None,
             })
         });
 
@@ -1256,6 +1294,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: true,
                 r#type: common::FlagType::Variant,
+                default_variant: None,
             })
         });
 
@@ -1351,6 +1390,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: true,
                 r#type: common::FlagType::Variant,
+                default_variant: None,
             })
         });
 
@@ -1456,6 +1496,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: true,
                 r#type: common::FlagType::Variant,
+                default_variant: None,
             })
         });
 
@@ -1567,6 +1608,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: true,
                 r#type: common::FlagType::Variant,
+                default_variant: None,
             })
         });
 
@@ -1677,6 +1719,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: true,
                 r#type: common::FlagType::Variant,
+                default_variant: None,
             })
         });
 
@@ -1774,6 +1817,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: true,
                 r#type: common::FlagType::Variant,
+                default_variant: None,
             })
         });
 
@@ -1848,6 +1892,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: true,
                 r#type: common::FlagType::Variant,
+                default_variant: None,
             })
         });
 
@@ -1957,6 +2002,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: true,
                 r#type: common::FlagType::Variant,
+                default_variant: None,
             })
         });
 
@@ -2051,6 +2097,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: true,
                 r#type: common::FlagType::Variant,
+                default_variant: None,
             })
         });
 
@@ -2147,6 +2194,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: true,
                 r#type: common::FlagType::Variant,
+                default_variant: None,
             })
         });
 
@@ -2257,6 +2305,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: true,
                 r#type: common::FlagType::Variant,
+                default_variant: None,
             })
         });
 
@@ -2366,6 +2415,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: true,
                 r#type: common::FlagType::Variant,
+                default_variant: None,
             })
         });
 
@@ -2485,6 +2535,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: true,
                 r#type: common::FlagType::Variant,
+                default_variant: None,
             })
         });
 
@@ -2567,6 +2618,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: true,
                 r#type: common::FlagType::Variant,
+                default_variant: None,
             })
         });
 
@@ -2673,6 +2725,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: false,
                 r#type: common::FlagType::Boolean,
+                default_variant: None,
             })
         });
 
@@ -2734,6 +2787,7 @@ mod tests {
                 key: String::from("foo"),
                 enabled: false,
                 r#type: common::FlagType::Boolean,
+                default_variant: None,
             })
         });
 
diff --git a/flipt-evaluation/src/models/flipt.rs b/flipt-evaluation/src/models/flipt.rs
index ccedbdbc..c401735d 100644
--- a/flipt-evaluation/src/models/flipt.rs
+++ b/flipt-evaluation/src/models/flipt.rs
@@ -1,5 +1,5 @@
 use crate::models::common;
-use serde::{Deserialize, Serialize};
+use serde::Serialize;
 use std::collections::HashMap;
 
 #[derive(Clone, Debug, Serialize)]
@@ -7,10 +7,12 @@ pub struct Flag {
     pub key: String,
     pub enabled: bool,
     pub r#type: common::FlagType,
+    pub default_variant: Option<Variant>,
 }
 
-#[derive(Clone, Debug, Deserialize)]
+#[derive(Clone, Debug, Serialize)]
 pub struct Variant {
+    pub id: String,
     pub key: String,
     pub attachment: String,
 }
diff --git a/flipt-evaluation/src/models/source.rs b/flipt-evaluation/src/models/source.rs
index cbc23ce6..be1f24e8 100644
--- a/flipt-evaluation/src/models/source.rs
+++ b/flipt-evaluation/src/models/source.rs
@@ -37,6 +37,15 @@ pub struct Flag {
     pub enabled: bool,
     pub rules: Option<Vec<Rule>>,
     pub rollouts: Option<Vec<Rollout>>,
+    pub default_variant: Option<Variant>,
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Variant {
+    pub id: String,
+    pub key: String,
+    pub attachment: String,
 }
 
 #[derive(Deserialize)]
diff --git a/flipt-evaluation/src/store/mod.rs b/flipt-evaluation/src/store/mod.rs
index 1566c4e2..026181be 100644
--- a/flipt-evaluation/src/store/mod.rs
+++ b/flipt-evaluation/src/store/mod.rs
@@ -64,6 +64,11 @@ impl Snapshot {
                 key: flag.key.clone(),
                 enabled: flag.enabled,
                 r#type: flag.r#type.unwrap_or(common::FlagType::Variant),
+                default_variant: flag.default_variant.map(|v| flipt::Variant {
+                    id: v.id,
+                    key: v.key,
+                    attachment: v.attachment,
+                }),
             };
 
             flags.insert(f.key.clone(), f);

From a788b63ae166e318abf176c9cc96ac233c236bfc Mon Sep 17 00:00:00 2001
From: Mark Phelps <209477+markphelps@users.noreply.github.com>
Date: Mon, 22 Jul 2024 14:06:47 -0400
Subject: [PATCH 2/3] chore: add tests

Signed-off-by: Mark Phelps <209477+markphelps@users.noreply.github.com>
---
 flipt-evaluation/src/lib.rs | 251 ++++++++++++++++++++++++++++++++++++
 1 file changed, 251 insertions(+)

diff --git a/flipt-evaluation/src/lib.rs b/flipt-evaluation/src/lib.rs
index 7b2c5291..eaf71d87 100644
--- a/flipt-evaluation/src/lib.rs
+++ b/flipt-evaluation/src/lib.rs
@@ -1097,6 +1097,89 @@ mod tests {
         assert_eq!(v.segment_keys, vec![String::from("segment1")]);
     }
 
+    #[test]
+    fn test_evaluator_flag_disabled() {
+        let mut mock_store = MockStore::new();
+
+        mock_store.expect_get_flag().returning(|_, _| {
+            Some(flipt::Flag {
+                key: String::from("foo"),
+                enabled: false,
+                r#type: common::FlagType::Variant,
+                default_variant: None,
+            })
+        });
+
+        let mut context: HashMap<String, String> = HashMap::new();
+        context.insert(String::from("bar"), String::from("baz"));
+        context.insert(String::from("foo"), String::from("bar"));
+
+        let variant = variant_evaluation(
+            &mock_store,
+            "default",
+            &EvaluationRequest {
+                flag_key: String::from("foo"),
+                entity_id: String::from("entity"),
+                context,
+            },
+        );
+
+        assert!(variant.is_ok());
+
+        let v = variant.unwrap();
+
+        assert_eq!(v.flag_key, String::from("foo"));
+        assert!(!v.r#match);
+        assert_eq!(v.reason, common::EvaluationReason::FlagDisabled);
+        assert!(v.variant_key.is_empty());
+        assert!(v.variant_attachment.is_empty());
+    }
+
+    #[test]
+    fn test_evaluator_flag_disabled_default_variant() {
+        let mut mock_store = MockStore::new();
+
+        mock_store.expect_get_flag().returning(|_, _| {
+            Some(flipt::Flag {
+                key: String::from("foo"),
+                enabled: false,
+                r#type: common::FlagType::Variant,
+                default_variant: Some(flipt::Variant {
+                    id: String::from("1"),
+                    key: String::from("default"),
+                    attachment: serde_json::json!({"key": "value"}).to_string(),
+                }),
+            })
+        });
+
+        let mut context: HashMap<String, String> = HashMap::new();
+        context.insert(String::from("bar"), String::from("baz"));
+        context.insert(String::from("foo"), String::from("bar"));
+
+        let variant = variant_evaluation(
+            &mock_store,
+            "default",
+            &EvaluationRequest {
+                flag_key: String::from("foo"),
+                entity_id: String::from("entity"),
+                context,
+            },
+        );
+
+        assert!(variant.is_ok());
+
+        let v = variant.unwrap();
+
+        assert_eq!(v.flag_key, String::from("foo"));
+        assert!(!v.r#match);
+        assert_eq!(v.reason, common::EvaluationReason::FlagDisabled);
+        assert_eq!(v.variant_key, String::from("default"));
+        assert_eq!(
+            v.variant_attachment,
+            serde_json::json!({"key": "value"}).to_string()
+        );
+    }
+
     // Segment Match Type ALL
     #[test]
     fn test_evaluator_match_all_no_variants_no_distributions() {
@@ -1173,6 +1256,90 @@ mod tests {
         assert_eq!(v.segment_keys, vec![String::from("segment1")]);
     }
 
+    #[test]
+    fn test_evaluator_match_all_no_distributions_default_variant() {
+        let mut mock_store = MockStore::new();
+
+        mock_store.expect_get_flag().returning(|_, _| {
+            Some(flipt::Flag {
+                key: String::from("foo"),
+                enabled: true,
+                r#type: common::FlagType::Variant,
+                default_variant: Some(flipt::Variant {
+                    id: String::from("1"),
+                    key: String::from("default"),
+                    attachment: serde_json::json!({"key": "value"}).to_string(),
+                }),
+            })
+        });
+
+        let mut segments: HashMap<String, flipt::EvaluationSegment> = HashMap::new();
+        segments.insert(
+            String::from("segment1"),
+            flipt::EvaluationSegment {
+                segment_key: String::from("segment1"),
+                match_type: common::SegmentMatchType::All,
+                constraints: vec![
+                    flipt::EvaluationConstraint {
+                        r#type: common::ConstraintComparisonType::String,
+                        property: String::from("bar"),
+                        operator: String::from("eq"),
+                        value: String::from("baz"),
+                    },
+                    flipt::EvaluationConstraint {
+                        r#type: common::ConstraintComparisonType::String,
+                        property: String::from("foo"),
+                        operator: String::from("eq"),
+                        value: String::from("bar"),
+                    },
+                ],
+            },
+        );
+
+        mock_store
+            .expect_get_evaluation_rules()
+            .returning(move |_, _| {
+                Some(vec![flipt::EvaluationRule {
+                    id: String::from("1"),
+                    flag_key: String::from("foo"),
+                    segments: segments.clone(),
+                    rank: 1,
+                    segment_operator: common::SegmentOperator::Or,
+                }])
+            });
+
+        mock_store
+            .expect_get_evaluation_distributions()
+            .returning(|_, _| Some(vec![]));
+
+        let mut context: HashMap<String, String> = HashMap::new();
+        context.insert(String::from("bar"), String::from("baz"));
+        context.insert(String::from("foo"), String::from("bar"));
+
+        let variant = variant_evaluation(
+            &mock_store,
+            "default",
+            &EvaluationRequest {
+                flag_key: String::from("foo"),
+                entity_id: String::from("entity"),
+                context,
+            },
+        );
+        assert!(variant.is_ok());
+
+        let v = variant.unwrap();
+
+        assert_eq!(v.flag_key, String::from("foo"));
+        assert!(v.r#match);
+        assert_eq!(v.reason, common::EvaluationReason::Match);
+        assert_eq!(v.segment_keys, vec![String::from("segment1")]);
+        assert_eq!(v.variant_key, String::from("default"));
+        assert_eq!(
+            v.variant_attachment,
+            serde_json::json!({"key": "value"}).to_string()
+        );
+    }
+
     #[test]
     fn test_evaluator_match_all_multiple_segments() {
         let mut mock_store = MockStore::new();
@@ -1883,6 +2050,90 @@ mod tests {
         assert_eq!(v.segment_keys, vec![String::from("segment1")]);
     }
 
+    #[test]
+    fn test_evaluator_match_any_no_distributions_default_variant() {
+        let mut mock_store = MockStore::new();
+
+        mock_store.expect_get_flag().returning(|_, _| {
+            Some(flipt::Flag {
+                key: String::from("foo"),
+                enabled: true,
+                r#type: common::FlagType::Variant,
+                default_variant: Some(flipt::Variant {
+                    id: String::from("1"),
+                    key: String::from("default"),
+                    attachment: serde_json::json!({"key": "value"}).to_string(),
+                }),
+            })
+        });
+
+        let mut segments: HashMap<String, flipt::EvaluationSegment> = HashMap::new();
+        segments.insert(
+            String::from("segment1"),
+            flipt::EvaluationSegment {
+                segment_key: String::from("segment1"),
+                match_type: common::SegmentMatchType::Any,
+                constraints: vec![
+                    flipt::EvaluationConstraint {
+                        r#type: common::ConstraintComparisonType::String,
+                        property: String::from("bar"),
+                        operator: String::from("eq"),
+                        value: String::from("baz"),
+                    },
+                    flipt::EvaluationConstraint {
+                        r#type: common::ConstraintComparisonType::String,
+                        property: String::from("foo"),
+                        operator: String::from("eq"),
+                        value: String::from("bar"),
+                    },
+                ],
+            },
+        );
+
+        mock_store
+            .expect_get_evaluation_rules()
+            .returning(move |_, _| {
+                Some(vec![flipt::EvaluationRule {
+                    id: String::from("1"),
+                    flag_key: String::from("foo"),
+                    segments: segments.clone(),
+                    rank: 1,
+                    segment_operator: common::SegmentOperator::Or,
+                }])
+            });
+
+        mock_store
+            .expect_get_evaluation_distributions()
+            .returning(|_, _| Some(vec![]));
+
+        let mut context: HashMap<String, String> = HashMap::new();
+        context.insert(String::from("bar"), String::from("baz"));
+
+        let variant = variant_evaluation(
+            &mock_store,
+            "default",
+            &EvaluationRequest {
+                flag_key: String::from("foo"),
+                entity_id: String::from("entity"),
+                context,
+            },
+        );
+
+        assert!(variant.is_ok());
+
+        let v = variant.unwrap();
+
+        assert_eq!(v.flag_key, String::from("foo"));
+        assert!(v.r#match);
+        assert_eq!(v.reason, common::EvaluationReason::Match);
+        assert_eq!(v.segment_keys, vec![String::from("segment1")]);
+        assert_eq!(v.variant_key, String::from("default"));
+        assert_eq!(
+            v.variant_attachment,
+            serde_json::json!({"key": "value"}).to_string()
+        );
+    }
+
     #[test]
     fn test_evaluator_match_any_multiple_segments() {
         let mut mock_store = MockStore::new();

From d4d3b198a337a52e2ee1e69b69a4c4c24dc2ffb5 Mon Sep 17 00:00:00 2001
From: Mark Phelps <209477+markphelps@users.noreply.github.com>
Date: Mon, 22 Jul 2024 14:47:27 -0400
Subject: [PATCH 3/3] chore: fix lint and test issues

Signed-off-by: Mark Phelps <209477+markphelps@users.noreply.github.com>
---
 flipt-evaluation/src/lib.rs          | 32 +++++++++++++++++++++-------
 flipt-evaluation/src/models/flipt.rs |  1 +
 2 files changed, 25 insertions(+), 8 deletions(-)

diff --git a/flipt-evaluation/src/lib.rs b/flipt-evaluation/src/lib.rs
index eaf71d87..bdecc716 100644
--- a/flipt-evaluation/src/lib.rs
+++ b/flipt-evaluation/src/lib.rs
@@ -141,8 +141,12 @@ pub fn variant_evaluation(
 
         if flag.default_variant.is_some() {
             let default_variant = flag.default_variant.as_ref().unwrap();
-            variant_evaluation_response.variant_key = default_variant.key.clone();
-            variant_evaluation_response.variant_attachment = default_variant.attachment.clone();
+            variant_evaluation_response
+                .variant_key
+                .clone_from(&default_variant.key);
+            variant_evaluation_response
+                .variant_attachment
+                .clone_from(&default_variant.attachment);
         }
 
         return Ok(variant_evaluation_response);
@@ -162,8 +166,12 @@ pub fn variant_evaluation(
     // if no rules and flag is enabled, return default variant
     if evaluation_rules.is_empty() && flag.default_variant.is_some() {
         let default_variant = flag.default_variant.as_ref().unwrap();
-        variant_evaluation_response.variant_key = default_variant.key.clone();
-        variant_evaluation_response.variant_attachment = default_variant.attachment.clone();
+        variant_evaluation_response
+            .variant_key
+            .clone_from(&default_variant.key);
+        variant_evaluation_response
+            .variant_attachment
+            .clone_from(&default_variant.attachment);
         variant_evaluation_response.request_duration_millis = get_duration_millis(now)?;
         variant_evaluation_response.reason = common::EvaluationReason::Default;
 
@@ -250,8 +258,12 @@ pub fn variant_evaluation(
 
             if flag.default_variant.is_some() {
                 let default_variant = flag.default_variant.as_ref().unwrap();
-                variant_evaluation_response.variant_key = default_variant.key.clone();
-                variant_evaluation_response.variant_attachment = default_variant.attachment.clone();
+                variant_evaluation_response
+                    .variant_key
+                    .clone_from(&default_variant.key);
+                variant_evaluation_response
+                    .variant_attachment
+                    .clone_from(&default_variant.attachment);
             }
 
             return Ok(variant_evaluation_response);
@@ -275,8 +287,12 @@ pub fn variant_evaluation(
 
             if flag.default_variant.is_some() {
                 let default_variant = flag.default_variant.as_ref().unwrap();
-                variant_evaluation_response.variant_key = default_variant.key.clone();
-                variant_evaluation_response.variant_attachment = default_variant.attachment.clone();
+                variant_evaluation_response
+                    .variant_key
+                    .clone_from(&default_variant.key);
+                variant_evaluation_response
+                    .variant_attachment
+                    .clone_from(&default_variant.attachment);
                 variant_evaluation_response.reason = common::EvaluationReason::Default;
             }
 
diff --git a/flipt-evaluation/src/models/flipt.rs b/flipt-evaluation/src/models/flipt.rs
index c401735d..863cc248 100644
--- a/flipt-evaluation/src/models/flipt.rs
+++ b/flipt-evaluation/src/models/flipt.rs
@@ -7,6 +7,7 @@ pub struct Flag {
     pub key: String,
     pub enabled: bool,
     pub r#type: common::FlagType,
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub default_variant: Option<Variant>,
 }