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..bdecc716 100644 --- a/flipt-evaluation/src/lib.rs +++ b/flipt-evaluation/src/lib.rs @@ -138,6 +138,17 @@ 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 + .clone_from(&default_variant.key); + variant_evaluation_response + .variant_attachment + .clone_from(&default_variant.attachment); + } + return Ok(variant_evaluation_response); } @@ -152,6 +163,21 @@ 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 + .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; + + return Ok(variant_evaluation_response); + } + for rule in evaluation_rules { if rule.rank < last_rank { return Err(Error::InvalidRequest(format!( @@ -224,10 +250,22 @@ 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 + .clone_from(&default_variant.key); + variant_evaluation_response + .variant_attachment + .clone_from(&default_variant.attachment); + } + return Ok(variant_evaluation_response); } @@ -242,9 +280,22 @@ 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 + .clone_from(&default_variant.key); + variant_evaluation_response + .variant_attachment + .clone_from(&default_variant.attachment); + variant_evaluation_response.reason = common::EvaluationReason::Default; + } + return Ok(variant_evaluation_response); } @@ -1006,6 +1057,7 @@ mod tests { key: String::from("foo"), enabled: true, r#type: common::FlagType::Variant, + default_variant: None, }) }); @@ -1061,6 +1113,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 = 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 = 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() { @@ -1071,6 +1206,7 @@ mod tests { key: String::from("foo"), enabled: true, r#type: common::FlagType::Variant, + default_variant: None, }) }); @@ -1136,6 +1272,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 = 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 = 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(); @@ -1145,6 +1365,7 @@ mod tests { key: String::from("foo"), enabled: true, r#type: common::FlagType::Variant, + default_variant: None, }) }); @@ -1256,6 +1477,7 @@ mod tests { key: String::from("foo"), enabled: true, r#type: common::FlagType::Variant, + default_variant: None, }) }); @@ -1351,6 +1573,7 @@ mod tests { key: String::from("foo"), enabled: true, r#type: common::FlagType::Variant, + default_variant: None, }) }); @@ -1456,6 +1679,7 @@ mod tests { key: String::from("foo"), enabled: true, r#type: common::FlagType::Variant, + default_variant: None, }) }); @@ -1567,6 +1791,7 @@ mod tests { key: String::from("foo"), enabled: true, r#type: common::FlagType::Variant, + default_variant: None, }) }); @@ -1677,6 +1902,7 @@ mod tests { key: String::from("foo"), enabled: true, r#type: common::FlagType::Variant, + default_variant: None, }) }); @@ -1774,6 +2000,7 @@ mod tests { key: String::from("foo"), enabled: true, r#type: common::FlagType::Variant, + default_variant: None, }) }); @@ -1839,6 +2066,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 = 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 = 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(); @@ -1848,6 +2159,7 @@ mod tests { key: String::from("foo"), enabled: true, r#type: common::FlagType::Variant, + default_variant: None, }) }); @@ -1957,6 +2269,7 @@ mod tests { key: String::from("foo"), enabled: true, r#type: common::FlagType::Variant, + default_variant: None, }) }); @@ -2051,6 +2364,7 @@ mod tests { key: String::from("foo"), enabled: true, r#type: common::FlagType::Variant, + default_variant: None, }) }); @@ -2147,6 +2461,7 @@ mod tests { key: String::from("foo"), enabled: true, r#type: common::FlagType::Variant, + default_variant: None, }) }); @@ -2257,6 +2572,7 @@ mod tests { key: String::from("foo"), enabled: true, r#type: common::FlagType::Variant, + default_variant: None, }) }); @@ -2366,6 +2682,7 @@ mod tests { key: String::from("foo"), enabled: true, r#type: common::FlagType::Variant, + default_variant: None, }) }); @@ -2485,6 +2802,7 @@ mod tests { key: String::from("foo"), enabled: true, r#type: common::FlagType::Variant, + default_variant: None, }) }); @@ -2567,6 +2885,7 @@ mod tests { key: String::from("foo"), enabled: true, r#type: common::FlagType::Variant, + default_variant: None, }) }); @@ -2673,6 +2992,7 @@ mod tests { key: String::from("foo"), enabled: false, r#type: common::FlagType::Boolean, + default_variant: None, }) }); @@ -2734,6 +3054,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..863cc248 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,13 @@ 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, } -#[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>, pub rollouts: Option>, + pub default_variant: Option, +} + +#[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);