diff --git a/.github/workflows/test-engine.yml b/.github/workflows/test-engine.yml index 75aa2a5f..a8dd6d0e 100644 --- a/.github/workflows/test-engine.yml +++ b/.github/workflows/test-engine.yml @@ -51,3 +51,27 @@ jobs: with: command: clippy args: -- -D warnings + + coverage: + name: Coverage + runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + run: rustup update stable + + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + + - name: Generate code coverage + run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: lcov.info + fail_ci_if_error: true diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 14321b11..639b2260 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,4 @@ { - "flipt-engine-ffi": "0.1.6" + "flipt-engine-ffi": "0.1.5", + "flipt-evaluation": "0.0.3" } diff --git a/flipt-engine-ffi/Cargo.toml b/flipt-engine-ffi/Cargo.toml index 6c30da94..82f55c38 100644 --- a/flipt-engine-ffi/Cargo.toml +++ b/flipt-engine-ffi/Cargo.toml @@ -11,6 +11,10 @@ publish = false libc = "0.2.150" serde = { version = "1.0.147", features = ["derive"] } serde_json = { version = "1.0.89", features = ["raw_value"] } +reqwest = { version = "0.11.22", features = ["json", "blocking"] } +tokio = { version = "1.33.0", features = ["full"] } +futures = "0.3" +openssl = { version = "0.10", features = ["vendored"] } [dependencies.flipt-evaluation] path = "../flipt-evaluation" diff --git a/flipt-engine-ffi/examples/evaluation.rs b/flipt-engine-ffi/examples/evaluation.rs index 25920076..fe80127a 100644 --- a/flipt-engine-ffi/examples/evaluation.rs +++ b/flipt-engine-ffi/examples/evaluation.rs @@ -1,8 +1,10 @@ // cargo run --example evaluation -use fliptengine::{self}; -use fliptevaluation::parser::{Authentication, HTTPParserBuilder}; -use fliptevaluation::{EvaluationRequest, Evaluator}; +use fliptengine::{ + evaluator::Evaluator, + parser::http::{Authentication, HTTPParserBuilder}, +}; +use fliptevaluation::EvaluationRequest; use std::collections::HashMap; fn main() { diff --git a/flipt-engine-ffi/src/evaluator/mod.rs b/flipt-engine-ffi/src/evaluator/mod.rs new file mode 100644 index 00000000..b0ca2fb7 --- /dev/null +++ b/flipt-engine-ffi/src/evaluator/mod.rs @@ -0,0 +1,107 @@ +use std::sync::{Arc, RwLock}; + +use fliptevaluation::{ + batch_evalution, boolean_evaluation, + error::Error, + models::{flipt, source::Document}, + parser::Parser, + store::{Snapshot, Store}, + variant_evaluation, BatchEvaluationResponse, BooleanEvaluationResponse, EvaluationRequest, + VariantEvaluationResponse, +}; + +pub struct Evaluator +where + P: Parser + Send, + S: Store + Send, +{ + namespace: String, + parser: P, + store: S, + mtx: Arc>, +} + +impl

Evaluator +where + P: Parser + Send, +{ + pub fn new_snapshot_evaluator(namespace: &str, parser: P) -> Result { + let doc = parser.parse(namespace)?; + let snap = Snapshot::build(namespace, doc)?; + Ok(Evaluator::new(namespace, parser, snap)) + } + + pub fn replace_snapshot(&mut self) { + let doc = match self.parser.parse(&self.namespace) { + Ok(d) => d, + Err(_) => { + // TODO: log::error!("error parsing document: {}"", e); + Document::default() + } + }; + + match Snapshot::build(&self.namespace, doc) { + Ok(s) => { + self.replace_store(s); + } + Err(_) => { + // TODO: log::error!("error building snapshot: {}", e); + } + }; + } +} + +impl Evaluator +where + P: Parser + Send, + S: Store + Send, +{ + pub fn new(namespace: &str, parser: P, store: S) -> Self { + Self { + namespace: namespace.to_string(), + parser, + store, + mtx: Arc::new(RwLock::new(0)), + } + } + + pub fn replace_store(&mut self, store: S) { + let _w_lock = self.mtx.write().unwrap(); + self.store = store; + } + + pub fn list_flags(&self) -> Result, Error> { + let _r_lock = self.mtx.read().unwrap(); + match self.store.list_flags(&self.namespace) { + Some(f) => Ok(f), + None => Err(Error::Unknown(format!( + "failed to get flags for {}", + self.namespace, + ))), + } + } + + pub fn variant( + &self, + evaluation_request: &EvaluationRequest, + ) -> Result { + let _r_lock = self.mtx.read().unwrap(); + variant_evaluation(&self.store, &self.namespace, evaluation_request) + } + + pub fn boolean( + &self, + evaluation_request: &EvaluationRequest, + ) -> Result { + let _r_lock = self.mtx.read().unwrap(); + boolean_evaluation(&self.store, &self.namespace, evaluation_request) + } + + pub fn batch( + &self, + requests: Vec, + ) -> Result { + let _r_lock = self.mtx.read().unwrap(); + batch_evalution(&self.store, &self.namespace, requests) + } +} diff --git a/flipt-engine-ffi/src/lib.rs b/flipt-engine-ffi/src/lib.rs index 998bb91c..4cd4f327 100644 --- a/flipt-engine-ffi/src/lib.rs +++ b/flipt-engine-ffi/src/lib.rs @@ -1,9 +1,14 @@ +pub mod evaluator; +pub mod parser; + +use evaluator::Evaluator; +use parser::http::{Authentication, HTTPParser, HTTPParserBuilder}; + use fliptevaluation::error::Error; use fliptevaluation::models::flipt; -use fliptevaluation::parser::{Authentication, HTTPParser, HTTPParserBuilder}; use fliptevaluation::store::Snapshot; use fliptevaluation::{ - BatchEvaluationResponse, BooleanEvaluationResponse, EvaluationRequest, Evaluator, + BatchEvaluationResponse, BooleanEvaluationResponse, EvaluationRequest, VariantEvaluationResponse, }; use libc::c_void; @@ -197,7 +202,7 @@ pub unsafe extern "C" fn initialize_engine( }; let parser = parser_builder.build(); - let evaluator = Evaluator::new_snapshot_evaluator(namespace.to_string(), parser).unwrap(); + let evaluator = Evaluator::new_snapshot_evaluator(namespace, parser).unwrap(); Box::into_raw(Box::new(Engine::new(evaluator, engine_opts))) as *mut c_void } diff --git a/flipt-engine-ffi/src/parser/http.rs b/flipt-engine-ffi/src/parser/http.rs new file mode 100644 index 00000000..95f49a0b --- /dev/null +++ b/flipt-engine-ffi/src/parser/http.rs @@ -0,0 +1,228 @@ +use reqwest::header::{self, HeaderMap}; +use serde::Deserialize; + +use fliptevaluation::error::Error; +use fliptevaluation::models::source; +use fliptevaluation::parser::Parser; + +#[derive(Debug, Clone, Default, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +#[serde(rename_all = "snake_case")] +pub enum Authentication { + #[default] + None, + ClientToken(String), + JwtToken(String), +} + +impl Authentication { + pub fn with_client_token(token: String) -> Self { + Authentication::ClientToken(token) + } + + pub fn with_jwt_token(token: String) -> Self { + Authentication::JwtToken(token) + } + + pub fn authenticate(&self) -> Option { + match self { + Authentication::ClientToken(token) => { + let header_format: String = format!("Bearer {}", token).parse().unwrap(); + Some(header_format) + } + Authentication::JwtToken(token) => { + let header_format: String = format!("JWT {}", token).parse().unwrap(); + Some(header_format) + } + Authentication::None => None, + } + } +} + +impl From for HeaderMap { + fn from(value: Authentication) -> Self { + let mut header_map = HeaderMap::new(); + match value.authenticate() { + Some(val) => { + header_map.insert( + header::AUTHORIZATION, + header::HeaderValue::from_str(&val).unwrap(), + ); + + header_map + } + None => header_map, + } + } +} + +pub struct HTTPParser { + http_client: reqwest::blocking::Client, + http_url: String, + authentication: HeaderMap, + reference: Option, +} + +pub struct HTTPParserBuilder { + http_url: String, + authentication: HeaderMap, + reference: Option, +} + +impl HTTPParserBuilder { + pub fn new(http_url: &str) -> Self { + Self { + http_url: http_url.to_string(), + authentication: HeaderMap::new(), + reference: None, + } + } + + pub fn authentication(mut self, authentication: Authentication) -> Self { + self.authentication = HeaderMap::from(authentication); + self + } + + pub fn reference(mut self, reference: &str) -> Self { + self.reference = Some(reference.to_string()); + self + } + + pub fn build(self) -> HTTPParser { + HTTPParser { + http_client: reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(10)) + .build() + .unwrap(), + http_url: self.http_url, + authentication: self.authentication, + reference: self.reference, + } + } +} + +impl HTTPParser { + fn url(&self, namespace: &str) -> String { + match &self.reference { + Some(reference) => { + format!( + "{}/internal/v1/evaluation/snapshot/namespace/{}?reference={}", + self.http_url, namespace, reference, + ) + } + None => { + format!( + "{}/internal/v1/evaluation/snapshot/namespace/{}", + self.http_url, namespace + ) + } + } + } +} + +impl Parser for HTTPParser { + fn parse(&self, namespace: &str) -> Result { + let mut headers = HeaderMap::new(); + headers.insert( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static("application/json"), + ); + headers.insert( + reqwest::header::ACCEPT, + reqwest::header::HeaderValue::from_static("application/json"), + ); + // 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"), + ); + + for (key, value) in self.authentication.iter() { + headers.insert(key, value.clone()); + } + + let response = match self + .http_client + .get(self.url(namespace)) + .headers(headers) + .send() + { + Ok(resp) => match resp.error_for_status() { + Ok(resp) => resp, + Err(e) => return Err(Error::Server(format!("response: {}", e))), + }, + Err(e) => return Err(Error::Server(format!("failed to make request: {}", e))), + }; + + let response_text = match response.text() { + Ok(t) => t, + Err(e) => return Err(Error::Server(format!("failed to get response body: {}", e))), + }; + + let document: source::Document = match serde_json::from_str(&response_text) { + Ok(doc) => doc, + Err(e) => return Err(Error::InvalidJSON(e)), + }; + + Ok(document) + } +} + +#[cfg(test)] +mod tests { + use crate::parser::http::Authentication; + + #[test] + fn test_http_parser_url() { + use super::HTTPParserBuilder; + + let parser = HTTPParserBuilder::new("http://localhost:8080") + .authentication(Authentication::with_client_token("secret".into())) + .reference("ref") + .build(); + + assert_eq!( + parser.url("default"), + "http://localhost:8080/internal/v1/evaluation/snapshot/namespace/default?reference=ref" + ); + + let parser = HTTPParserBuilder::new("http://localhost:8080") + .authentication(Authentication::with_client_token("secret".into())) + .build(); + + assert_eq!( + parser.url("default"), + "http://localhost:8080/internal/v1/evaluation/snapshot/namespace/default" + ); + } + + #[test] + fn test_deserialize_no_auth() { + let json = r#""#; + + let unwrapped_string: Authentication = serde_json::from_str(&json).unwrap_or_default(); + + assert_eq!(unwrapped_string, Authentication::None); + } + + #[test] + fn test_deserialize_client_token() { + let json = r#"{"client_token":"secret"}"#; + + let unwrapped_string: Authentication = serde_json::from_str(&json).unwrap_or_default(); + + assert_eq!( + unwrapped_string, + Authentication::ClientToken("secret".into()) + ); + } + + #[test] + fn test_deserialize_jwt_token() { + let json = r#"{"jwt_token":"secret"}"#; + + let unwrapped_string: Authentication = serde_json::from_str(&json).unwrap_or_default(); + + assert_eq!(unwrapped_string, Authentication::JwtToken("secret".into())); + } +} diff --git a/flipt-engine-ffi/src/parser/mod.rs b/flipt-engine-ffi/src/parser/mod.rs new file mode 100644 index 00000000..3883215f --- /dev/null +++ b/flipt-engine-ffi/src/parser/mod.rs @@ -0,0 +1 @@ +pub mod http; diff --git a/flipt-evaluation/Cargo.toml b/flipt-evaluation/Cargo.toml index 8ca19260..53c5d090 100644 --- a/flipt-evaluation/Cargo.toml +++ b/flipt-evaluation/Cargo.toml @@ -12,16 +12,12 @@ name = "fliptevaluation" path = "src/lib.rs" [dependencies] -reqwest = { version = "0.11.22", features = ["json", "blocking"] } -tokio = { version = "1.33.0", features = ["full"] } serde = { version = "1.0.147", features = ["derive"] } serde_json = { version = "1.0.89", features = ["raw_value"] } uuid = { version = "1.5.0", features = ["v4"] } crc32fast = "1.3.2" -chrono = { version = "0.4.31", features = ["serde", "rustc-serialize"] } -futures = "0.3" thiserror = "1.0.50" -openssl = { version = "0.10", features = ["vendored"] } +chrono = { version = "0.4.31", features = ["serde", "rustc-serialize"] } [dev-dependencies] mockall = "0.12.1" diff --git a/flipt-evaluation/src/lib.rs b/flipt-evaluation/src/lib.rs index bdf00497..5d6730f6 100644 --- a/flipt-evaluation/src/lib.rs +++ b/flipt-evaluation/src/lib.rs @@ -1,7 +1,6 @@ use chrono::{DateTime, Utc}; use serde::Serialize; use std::collections::HashMap; -use std::sync::{Arc, RwLock}; use std::time::{Duration, SystemTime, SystemTimeError}; pub mod error; @@ -12,24 +11,12 @@ pub mod store; use crate::error::Error; use crate::models::common; use crate::models::flipt; -use crate::parser::Parser; -use crate::store::{Snapshot, Store}; +use crate::store::Store; const DEFAULT_PERCENT: f32 = 100.0; const DEFAULT_TOTAL_BUCKET_NUMBER: u32 = 1000; const DEFAULT_PERCENT_MULTIPIER: f32 = DEFAULT_TOTAL_BUCKET_NUMBER as f32 / DEFAULT_PERCENT; -pub struct Evaluator -where - P: Parser + Send, - S: Store + Send, -{ - namespace: String, - parser: P, - store: S, - mtx: Arc>, -} - #[repr(C)] pub struct EvaluationRequest { pub flag_key: String, @@ -116,479 +103,387 @@ impl Default for ErrorEvaluationResponse { } } -type ListFlagsResult = std::result::Result, Error>; - -type VariantEvaluationResult = std::result::Result; +pub fn variant_evaluation( + store: &dyn Store, + namespace: &str, + request: &EvaluationRequest, +) -> Result { + let now = SystemTime::now(); + let mut last_rank = 0; + + let flag = match store.get_flag(namespace, &request.flag_key) { + Some(f) => { + if f.r#type != common::FlagType::Variant { + return Err(Error::InvalidRequest(format!( + "{} is not a variant flag", + &request.flag_key, + ))); + } + f + } + None => { + return Err(Error::InvalidRequest(format!( + "failed to get flag information {}/{}", + namespace, &request.flag_key, + ))); + } + }; -type BooleanEvaluationResult = std::result::Result; + let mut variant_evaluation_response = VariantEvaluationResponse { + flag_key: flag.key.clone(), + ..Default::default() + }; -impl

Evaluator -where - P: Parser + Send, -{ - pub fn new_snapshot_evaluator(namespace: String, parser: P) -> Result { - let snap = Snapshot::build(&namespace, &parser)?; - Ok(Evaluator::new(namespace, parser, snap)) + if !flag.enabled { + variant_evaluation_response.reason = common::EvaluationReason::FlagDisabled; + variant_evaluation_response.request_duration_millis = get_duration_millis(now.elapsed())?; + return Ok(variant_evaluation_response); } - pub fn replace_snapshot(&mut self) { - match Snapshot::build(&self.namespace, &self.parser) { - Ok(s) => { - self.replace_store(s); - } - Err(_) => { - // TODO: log::error!("error building snapshot: {}", e); - } - }; - } -} + let evaluation_rules = match store.get_evaluation_rules(namespace, &request.flag_key) { + Some(rules) => rules, + None => { + return Err(Error::Unknown(format!( + "error getting evaluation rules for namespace {} and flag {}", + namespace, + request.flag_key.clone() + ))); + } + }; -impl Evaluator -where - P: Parser + Send, - S: Store + Send, -{ - pub fn new(namespace: String, parser: P, store_impl: S) -> Self { - Self { - namespace, - parser, - store: store_impl, - mtx: Arc::new(RwLock::new(0)), + for rule in evaluation_rules { + if rule.rank < last_rank { + return Err(Error::InvalidRequest(format!( + "rule rank: {} detected out of order", + rule.rank + ))); } - } - pub fn replace_store(&mut self, store_impl: S) { - let _w_lock = self.mtx.write().unwrap(); - self.store = store_impl; - } + last_rank = rule.rank; - pub fn list_flags(&self) -> ListFlagsResult { - let _r_lock = self.mtx.read().unwrap(); - match self.store.list_flags(&self.namespace) { - Some(f) => Ok(f), - None => Err(Error::Unknown(format!( - "failed to get flags for {}", - self.namespace, - ))), - } - } + let mut segment_keys: Vec = Vec::new(); + let mut segment_matches = 0; - pub fn variant( - &self, - evaluation_request: &EvaluationRequest, - ) -> VariantEvaluationResult { - let _r_lock = self.mtx.read().unwrap(); - let flag = match self - .store - .get_flag(&self.namespace, &evaluation_request.flag_key) - { - Some(f) => { - if f.r#type != common::FlagType::Variant { - return Err(Error::InvalidRequest(format!( - "{} is not a variant flag", - &evaluation_request.flag_key, - ))); - } - f + for kv in &rule.segments { + let matched = match matches_constraints( + &request.context, + &kv.1.constraints, + &kv.1.match_type, + &request.entity_id, + ) { + Ok(b) => b, + Err(err) => return Err(err), + }; + + if matched { + segment_keys.push(kv.0.into()); + segment_matches += 1; } - None => { - return Err(Error::InvalidRequest(format!( - "failed to get flag information {}/{}", - &self.namespace, &evaluation_request.flag_key, - ))); + } + + if rule.segment_operator == common::SegmentOperator::Or { + if segment_matches < 1 { + continue; } - }; + } else if rule.segment_operator == common::SegmentOperator::And + && rule.segments.len() != segment_matches + { + continue; + } - self.variant_evaluation(&flag, evaluation_request) - } + variant_evaluation_response.segment_keys = segment_keys; - pub fn boolean( - &self, - evaluation_request: &EvaluationRequest, - ) -> BooleanEvaluationResult { - let _r_lock = self.mtx.read().unwrap(); - let flag = match self - .store - .get_flag(&self.namespace, &evaluation_request.flag_key) - { - Some(f) => { - if f.r#type != common::FlagType::Boolean { - return Err(Error::InvalidRequest(format!( - "{} is not a boolean flag", - &evaluation_request.flag_key, - ))); - } - f - } + let distributions = match store.get_evaluation_distributions(namespace, &rule.id) { + Some(evaluation_distributions) => evaluation_distributions, None => { - return Err(Error::InvalidRequest(format!( - "failed to get flag information {}/{}", - &self.namespace, &evaluation_request.flag_key, - ))); + return Err(Error::Unknown(format!( + "error getting evaluation distributions for namespace {} and rule {}", + namespace, + rule.id.clone() + ))) } }; - self.boolean_evaluation(&flag, evaluation_request) - } + let mut valid_distributions: Vec = Vec::new(); + let mut buckets: Vec = Vec::new(); - pub fn batch( - &self, - requests: Vec, - ) -> Result { - let now = SystemTime::now(); - - let mut evaluation_responses: Vec = Vec::new(); - for request in requests { - let flag = match self.store.get_flag(&self.namespace, &request.flag_key) { - Some(f) => f, - None => { - evaluation_responses.push(EvaluationResponse { - r#type: common::ResponseType::Error, - boolean_evaluation_response: None, - variant_evaluation_response: None, - error_evaluation_response: Some(ErrorEvaluationResponse { - flag_key: request.flag_key, - namespace_key: self.namespace.clone(), - reason: common::ErrorEvaluationReason::NotFound, - }), - }); - continue; - } - }; + for distribution in distributions { + if distribution.rollout > 0.0 { + valid_distributions.push(distribution.clone()); - match flag.r#type { - common::FlagType::Boolean => { - let boolean_evaluation = self.boolean_evaluation(&flag, &request)?; - evaluation_responses.push(EvaluationResponse { - r#type: common::ResponseType::Boolean, - boolean_evaluation_response: Some(boolean_evaluation), - variant_evaluation_response: None, - error_evaluation_response: None, - }); - } - common::FlagType::Variant => { - let variant_evaluation = self.variant_evaluation(&flag, &request)?; - evaluation_responses.push(EvaluationResponse { - r#type: common::ResponseType::Variant, - boolean_evaluation_response: None, - variant_evaluation_response: Some(variant_evaluation), - error_evaluation_response: None, - }); + if buckets.is_empty() { + let bucket = (distribution.rollout * DEFAULT_PERCENT_MULTIPIER) as i32; + buckets.push(bucket); + } else { + let bucket = buckets[buckets.len() - 1] + + (distribution.rollout * DEFAULT_PERCENT_MULTIPIER) as i32; + buckets.push(bucket); } } } - Ok(BatchEvaluationResponse { - responses: evaluation_responses, - request_duration_millis: get_duration_millis(now.elapsed())?, - }) - } + // no distributions for the 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.elapsed())?; + return Ok(variant_evaluation_response); + } + + let bucket = + crc32fast::hash(format!("{}{}", request.flag_key, request.entity_id).as_bytes()) + % DEFAULT_TOTAL_BUCKET_NUMBER; - fn variant_evaluation( - &self, - flag: &flipt::Flag, - evaluation_request: &EvaluationRequest, - ) -> VariantEvaluationResult { - let now = SystemTime::now(); - let mut last_rank = 0; - - let mut variant_evaluation_response = VariantEvaluationResponse { - flag_key: flag.key.clone(), - ..Default::default() + buckets.sort(); + + let index = match buckets.binary_search(&(bucket as i32 + 1)) { + Ok(idx) => idx, + Err(idx) => idx, }; - if !flag.enabled { - variant_evaluation_response.reason = common::EvaluationReason::FlagDisabled; + if index == valid_distributions.len() { + variant_evaluation_response.r#match = false; variant_evaluation_response.request_duration_millis = get_duration_millis(now.elapsed())?; return Ok(variant_evaluation_response); } - let evaluation_rules = match self - .store - .get_evaluation_rules(&self.namespace, &evaluation_request.flag_key) - { - Some(rules) => rules, - None => { - return Err(Error::Unknown(format!( - "error getting evaluation rules for namespace {} and flag {}", - self.namespace.clone(), - evaluation_request.flag_key.clone() - ))); - } - }; + let d = &valid_distributions[index]; + + variant_evaluation_response.r#match = true; + variant_evaluation_response.variant_key = d.variant_key.clone(); + variant_evaluation_response.variant_attachment = d.variant_attachment.clone(); + variant_evaluation_response.reason = common::EvaluationReason::Match; + variant_evaluation_response.request_duration_millis = get_duration_millis(now.elapsed())?; + return Ok(variant_evaluation_response); + } - for rule in evaluation_rules { - if rule.rank < last_rank { + Ok(variant_evaluation_response) +} + +pub fn boolean_evaluation( + store: &dyn Store, + namespace: &str, + request: &EvaluationRequest, +) -> Result { + let now = SystemTime::now(); + let mut last_rank = 0; + + let flag = match store.get_flag(namespace, &request.flag_key) { + Some(f) => { + if f.r#type != common::FlagType::Boolean { return Err(Error::InvalidRequest(format!( - "rule rank: {} detected out of order", - rule.rank + "{} is not a boolean flag", + &request.flag_key, ))); } + f + } + None => { + return Err(Error::InvalidRequest(format!( + "failed to get flag information {}/{}", + namespace, &request.flag_key, + ))); + } + }; - last_rank = rule.rank; + let evaluation_rollouts = match store.get_evaluation_rollouts(namespace, &request.flag_key) { + Some(rollouts) => rollouts, + None => { + return Err(Error::Unknown(format!( + "error getting evaluation rollouts for namespace {} and flag {}", + namespace, + request.flag_key.clone() + ))); + } + }; + + for rollout in evaluation_rollouts { + if rollout.rank < last_rank { + return Err(Error::InvalidRequest(format!( + "rollout rank: {} detected out of order", + rollout.rank + ))); + } - let mut segment_keys: Vec = Vec::new(); + last_rank = rollout.rank; + + if rollout.threshold.is_some() { + let threshold = rollout.threshold.unwrap(); + + let normalized_value = + (crc32fast::hash(format!("{}{}", request.entity_id, request.flag_key).as_bytes()) + % 100) as f32; + + if normalized_value < threshold.percentage { + return Ok(BooleanEvaluationResponse { + enabled: threshold.value, + flag_key: flag.key.clone(), + reason: common::EvaluationReason::Match, + request_duration_millis: get_duration_millis(now.elapsed())?, + timestamp: chrono::offset::Utc::now(), + }); + } + } else if rollout.segment.is_some() { + let segment = rollout.segment.unwrap(); let mut segment_matches = 0; - for kv in &rule.segments { - let matched = match self.matches_constraints( - &evaluation_request.context, - &kv.1.constraints, - &kv.1.match_type, - &evaluation_request.entity_id, + for s in &segment.segments { + let matched = match matches_constraints( + &request.context, + &s.1.constraints, + &s.1.match_type, + &request.entity_id, ) { - Ok(b) => b, + Ok(v) => v, Err(err) => return Err(err), }; if matched { - segment_keys.push(kv.0.into()); segment_matches += 1; } } - if rule.segment_operator == common::SegmentOperator::Or { + if segment.segment_operator == common::SegmentOperator::Or { if segment_matches < 1 { continue; } - } else if rule.segment_operator == common::SegmentOperator::And - && rule.segments.len() != segment_matches + } else if segment.segment_operator == common::SegmentOperator::And + && segment.segments.len() != segment_matches { continue; } - variant_evaluation_response.segment_keys = segment_keys; - - let distributions = match self - .store - .get_evaluation_distributions(&self.namespace, &rule.id) - { - Some(evaluation_distributions) => evaluation_distributions, - None => { - return Err(Error::Unknown(format!( - "error getting evaluation distributions for namespace {} and rule {}", - self.namespace.clone(), - rule.id.clone() - ))) - } - }; - - let mut valid_distributions: Vec = Vec::new(); - let mut buckets: Vec = Vec::new(); - - for distribution in distributions { - if distribution.rollout > 0.0 { - valid_distributions.push(distribution.clone()); - - if buckets.is_empty() { - let bucket = (distribution.rollout * DEFAULT_PERCENT_MULTIPIER) as i32; - buckets.push(bucket); - } else { - let bucket = buckets[buckets.len() - 1] - + (distribution.rollout * DEFAULT_PERCENT_MULTIPIER) as i32; - buckets.push(bucket); - } - } - } - - // no distributions for the 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.elapsed())?; - return Ok(variant_evaluation_response); - } - - let bucket = crc32fast::hash( - format!( - "{}{}", - evaluation_request.flag_key, evaluation_request.entity_id - ) - .as_bytes(), - ) % DEFAULT_TOTAL_BUCKET_NUMBER; - - buckets.sort(); - - let index = match buckets.binary_search(&(bucket as i32 + 1)) { - Ok(idx) => idx, - Err(idx) => idx, - }; - - if index == valid_distributions.len() { - variant_evaluation_response.r#match = false; - variant_evaluation_response.request_duration_millis = - get_duration_millis(now.elapsed())?; - return Ok(variant_evaluation_response); - } - - let d = &valid_distributions[index]; - - variant_evaluation_response.r#match = true; - variant_evaluation_response.variant_key = d.variant_key.clone(); - variant_evaluation_response.variant_attachment = d.variant_attachment.clone(); - variant_evaluation_response.reason = common::EvaluationReason::Match; - variant_evaluation_response.request_duration_millis = - get_duration_millis(now.elapsed())?; - return Ok(variant_evaluation_response); + return Ok(BooleanEvaluationResponse { + enabled: segment.value, + flag_key: flag.key.clone(), + reason: common::EvaluationReason::Match, + request_duration_millis: get_duration_millis(now.elapsed())?, + timestamp: chrono::offset::Utc::now(), + }); } - - Ok(variant_evaluation_response) } - fn boolean_evaluation( - &self, - flag: &flipt::Flag, - evaluation_request: &EvaluationRequest, - ) -> BooleanEvaluationResult { - let now = SystemTime::now(); - let mut last_rank = 0; - - let evaluation_rollouts = match self - .store - .get_evaluation_rollouts(&self.namespace, &evaluation_request.flag_key) - { - Some(rollouts) => rollouts, + Ok(BooleanEvaluationResponse { + enabled: flag.enabled, + flag_key: flag.key.clone(), + reason: common::EvaluationReason::Default, + request_duration_millis: get_duration_millis(now.elapsed())?, + timestamp: chrono::offset::Utc::now(), + }) +} + +pub fn batch_evalution( + store: &dyn Store, + namespace: &str, + requests: Vec, +) -> Result { + let now = SystemTime::now(); + + let mut evaluation_responses: Vec = Vec::new(); + for request in requests { + let flag = match store.get_flag(namespace, &request.flag_key) { + Some(f) => f, None => { - return Err(Error::Unknown(format!( - "error getting evaluation rollouts for namespace {} and flag {}", - self.namespace.clone(), - evaluation_request.flag_key.clone() - ))); + evaluation_responses.push(EvaluationResponse { + r#type: common::ResponseType::Error, + boolean_evaluation_response: None, + variant_evaluation_response: None, + error_evaluation_response: Some(ErrorEvaluationResponse { + flag_key: request.flag_key, + namespace_key: namespace.to_string(), + reason: common::ErrorEvaluationReason::NotFound, + }), + }); + continue; } }; - for rollout in evaluation_rollouts { - if rollout.rank < last_rank { - return Err(Error::InvalidRequest(format!( - "rollout rank: {} detected out of order", - rollout.rank - ))); + match flag.r#type { + common::FlagType::Boolean => { + let boolean_evaluation = boolean_evaluation(store, namespace, &request)?; + evaluation_responses.push(EvaluationResponse { + r#type: common::ResponseType::Boolean, + boolean_evaluation_response: Some(boolean_evaluation), + variant_evaluation_response: None, + error_evaluation_response: None, + }); } - - last_rank = rollout.rank; - - if rollout.threshold.is_some() { - let threshold = rollout.threshold.unwrap(); - - let normalized_value = (crc32fast::hash( - format!( - "{}{}", - evaluation_request.entity_id, evaluation_request.flag_key - ) - .as_bytes(), - ) % 100) as f32; - - if normalized_value < threshold.percentage { - return Ok(BooleanEvaluationResponse { - enabled: threshold.value, - flag_key: flag.key.clone(), - reason: common::EvaluationReason::Match, - request_duration_millis: get_duration_millis(now.elapsed())?, - timestamp: chrono::offset::Utc::now(), - }); - } - } else if rollout.segment.is_some() { - let segment = rollout.segment.unwrap(); - let mut segment_matches = 0; - - for s in &segment.segments { - let matched = match self.matches_constraints( - &evaluation_request.context, - &s.1.constraints, - &s.1.match_type, - &evaluation_request.entity_id, - ) { - Ok(v) => v, - Err(err) => return Err(err), - }; - - if matched { - segment_matches += 1; - } - } - - if segment.segment_operator == common::SegmentOperator::Or { - if segment_matches < 1 { - continue; - } - } else if segment.segment_operator == common::SegmentOperator::And - && segment.segments.len() != segment_matches - { - continue; - } - - return Ok(BooleanEvaluationResponse { - enabled: segment.value, - flag_key: flag.key.clone(), - reason: common::EvaluationReason::Match, - request_duration_millis: get_duration_millis(now.elapsed())?, - timestamp: chrono::offset::Utc::now(), + common::FlagType::Variant => { + let variant_evaluation = variant_evaluation(store, namespace, &request)?; + evaluation_responses.push(EvaluationResponse { + r#type: common::ResponseType::Variant, + boolean_evaluation_response: None, + variant_evaluation_response: Some(variant_evaluation), + error_evaluation_response: None, }); } } + } - Ok(BooleanEvaluationResponse { - enabled: flag.enabled, - flag_key: flag.key.clone(), - reason: common::EvaluationReason::Default, - request_duration_millis: get_duration_millis(now.elapsed())?, - timestamp: chrono::offset::Utc::now(), - }) + Ok(BatchEvaluationResponse { + responses: evaluation_responses, + request_duration_millis: get_duration_millis(now.elapsed())?, + }) +} + +fn get_duration_millis(elapsed: Result) -> Result { + match elapsed { + Ok(elapsed) => Ok(elapsed.as_secs_f64() * 1000.0), + Err(e) => Err(Error::Unknown(format!("error getting duration {}", e))), } +} - fn matches_constraints( - &self, - eval_context: &HashMap, - constraints: &Vec, - segment_match_type: &common::SegmentMatchType, - entity_id: &str, - ) -> Result { - let mut constraint_matches: usize = 0; - - for constraint in constraints { - let value = match eval_context.get(&constraint.property) { - Some(v) => v, - None => { - // If we have an entityId return dummy value which is an empty string. - "" - } - }; +fn matches_constraints( + eval_context: &HashMap, + constraints: &Vec, + segment_match_type: &common::SegmentMatchType, + entity_id: &str, +) -> Result { + let mut constraint_matches: usize = 0; - let matched = match constraint.r#type { - common::ConstraintComparisonType::String => matches_string(constraint, value), - common::ConstraintComparisonType::Number => matches_number(constraint, value)?, - common::ConstraintComparisonType::Boolean => matches_boolean(constraint, value)?, - common::ConstraintComparisonType::DateTime => matches_datetime(constraint, value)?, - common::ConstraintComparisonType::EntityId => matches_string(constraint, entity_id), - _ => { - return Ok(false); - } - }; + for constraint in constraints { + let value = match eval_context.get(&constraint.property) { + Some(v) => v, + None => { + // If we have an entityId return dummy value which is an empty string. + "" + } + }; - if matched { - constraint_matches += 1; + let matched = match constraint.r#type { + common::ConstraintComparisonType::String => matches_string(constraint, value), + common::ConstraintComparisonType::Number => matches_number(constraint, value)?, + common::ConstraintComparisonType::Boolean => matches_boolean(constraint, value)?, + common::ConstraintComparisonType::DateTime => matches_datetime(constraint, value)?, + common::ConstraintComparisonType::EntityId => matches_string(constraint, entity_id), + _ => { + return Ok(false); + } + }; - if segment_match_type == &common::SegmentMatchType::Any { - break; - } else { - continue; - } - } else if segment_match_type == &common::SegmentMatchType::All { + if matched { + constraint_matches += 1; + + if segment_match_type == &common::SegmentMatchType::Any { break; } else { continue; } + } else if segment_match_type == &common::SegmentMatchType::All { + break; + } else { + continue; } + } - let is_match = match segment_match_type { - common::SegmentMatchType::All => constraints.len() == constraint_matches, - common::SegmentMatchType::Any => constraints.is_empty() || constraint_matches != 0, - }; + let is_match = match segment_match_type { + common::SegmentMatchType::All => constraints.len() == constraint_matches, + common::SegmentMatchType::Any => constraints.is_empty() || constraint_matches != 0, + }; - Ok(is_match) - } + Ok(is_match) } fn contains_string(v: &str, values: &str) -> bool { @@ -779,18 +674,10 @@ fn matches_datetime( } } -fn get_duration_millis(elapsed: Result) -> Result { - match elapsed { - Ok(elapsed) => Ok(elapsed.as_secs_f64() * 1000.0), - Err(e) => Err(Error::Unknown(format!("error getting duration {}", e))), - } -} - #[cfg(test)] mod tests { use super::*; use crate::models::flipt::RolloutSegment; - use crate::parser::TestParser; use crate::store::MockStore; macro_rules! matches_string_tests { @@ -1103,7 +990,6 @@ mod tests { #[test] fn test_entity_id_match() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -1145,20 +1031,17 @@ mod tests { .expect_get_evaluation_distributions() .returning(|_, _| Some(vec![])); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let context: HashMap = HashMap::new(); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("user@flipt.io"), - context, - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("user@flipt.io"), + context, + }, + ); assert!(variant.is_ok()); let v = variant.unwrap(); @@ -1172,7 +1055,6 @@ mod tests { // Segment Match Type ALL #[test] fn test_evaluator_match_all_no_variants_no_distributions() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -1222,22 +1104,19 @@ mod tests { .expect_get_evaluation_distributions() .returning(|_, _| Some(vec![])); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let mut context: HashMap = HashMap::new(); context.insert(String::from("bar"), String::from("baz")); context.insert(String::from("foo"), String::from("bar")); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("entity"), - context, - }); + 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(); @@ -1250,7 +1129,6 @@ mod tests { #[test] fn test_evaluator_match_all_multiple_segments() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -1283,6 +1161,7 @@ mod tests { ], }, ); + segments.insert( String::from("segment2"), flipt::EvaluationSegment { @@ -1313,23 +1192,20 @@ mod tests { .expect_get_evaluation_distributions() .returning(|_, _| Some(vec![])); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let mut context: HashMap = HashMap::new(); context.insert(String::from("bar"), String::from("baz")); context.insert(String::from("foo"), String::from("bar")); context.insert(String::from("company"), String::from("flipt")); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("entity"), - context, - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("entity"), + context, + }, + ); assert!(variant.is_ok()); @@ -1342,11 +1218,15 @@ mod tests { let mut context: HashMap = HashMap::new(); context.insert(String::from("bar"), String::from("boz")); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("entity"), - context, - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("entity"), + context, + }, + ); assert!(variant.is_ok()); @@ -1360,7 +1240,6 @@ mod tests { #[test] fn test_evaluator_match_all_distribution_not_matched() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -1430,24 +1309,20 @@ mod tests { }]) }); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let mut context: HashMap = HashMap::new(); - context.insert(String::from("bar"), String::from("baz")); context.insert(String::from("foo"), String::from("bar")); context.insert(String::from("admin"), String::from("true")); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("123"), - context, - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("123"), + context, + }, + ); assert!(variant.is_ok()); @@ -1460,7 +1335,6 @@ mod tests { #[test] fn test_evaluator_match_all_single_variant_distribution() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -1530,24 +1404,20 @@ mod tests { }]) }); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let mut context: HashMap = HashMap::new(); - context.insert(String::from("bar"), String::from("baz")); context.insert(String::from("foo"), String::from("bar")); context.insert(String::from("admin"), String::from("true")); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("123"), - context, - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("123"), + context, + }, + ); assert!(variant.is_ok()); @@ -1570,7 +1440,6 @@ mod tests { #[test] fn test_evaluator_match_all_rollout_distribution() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -1635,23 +1504,19 @@ mod tests { ]) }); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let mut context: HashMap = HashMap::new(); - context.insert(String::from("bar"), String::from("baz")); context.insert(String::from("foo"), String::from("bar")); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("1"), - context: context.clone(), - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("1"), + context: context.clone(), + }, + ); assert!(variant.is_ok()); @@ -1663,11 +1528,15 @@ mod tests { assert_eq!(v.variant_key, String::from("variant1")); assert_eq!(v.segment_keys, vec![String::from("segment1")]); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("2"), - context, - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("2"), + context, + }, + ); assert!(variant.is_ok()); @@ -1682,7 +1551,6 @@ mod tests { #[test] fn test_evaluator_match_all_rollout_distribution_multi_rule() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -1766,23 +1634,19 @@ mod tests { ]) }); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let mut context: HashMap = HashMap::new(); - context.insert(String::from("premium_user"), String::from("true")); context.insert(String::from("foo"), String::from("bar")); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("1"), - context: context.clone(), - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("1"), + context: context.clone(), + }, + ); assert!(variant.is_ok()); @@ -1797,7 +1661,6 @@ mod tests { #[test] fn test_evaluator_match_all_no_constraints() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -1849,20 +1712,17 @@ mod tests { ]) }); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let context: HashMap = HashMap::new(); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("10"), - context: context.clone(), - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("10"), + context: context.clone(), + }, + ); assert!(variant.is_ok()); @@ -1874,11 +1734,15 @@ mod tests { assert_eq!(v.variant_key, String::from("variant1")); assert_eq!(v.segment_keys, vec![String::from("segment1")]); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("01"), - context, - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("01"), + context, + }, + ); assert!(variant.is_ok()); @@ -1894,7 +1758,6 @@ mod tests { // Segment Match Type ANY #[test] fn test_evaluator_match_any_no_variants_no_distributions() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -1944,21 +1807,19 @@ mod tests { .expect_get_evaluation_distributions() .returning(|_, _| Some(vec![])); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let mut context: HashMap = HashMap::new(); context.insert(String::from("bar"), String::from("baz")); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("entity"), - context, - }); + 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(); @@ -1971,7 +1832,6 @@ mod tests { #[test] fn test_evaluator_match_any_multiple_segments() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -2034,22 +1894,19 @@ mod tests { .expect_get_evaluation_distributions() .returning(|_, _| Some(vec![])); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let mut context: HashMap = HashMap::new(); context.insert(String::from("bar"), String::from("baz")); context.insert(String::from("company"), String::from("flipt")); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("entity"), - context, - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("entity"), + context, + }, + ); assert!(variant.is_ok()); @@ -2062,11 +1919,15 @@ mod tests { let mut context: HashMap = HashMap::new(); context.insert(String::from("bar"), String::from("boz")); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("entity"), - context, - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("entity"), + context, + }, + ); assert!(variant.is_ok()); @@ -2080,7 +1941,6 @@ mod tests { #[test] fn test_evaluator_match_any_distribution_not_matched() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -2150,23 +2010,19 @@ mod tests { }]) }); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let mut context: HashMap = HashMap::new(); - context.insert(String::from("bar"), String::from("baz")); context.insert(String::from("admin"), String::from("true")); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("123"), - context, - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("123"), + context, + }, + ); assert!(variant.is_ok()); @@ -2179,7 +2035,6 @@ mod tests { #[test] fn test_evaluator_match_any_single_variant_distribution() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -2249,23 +2104,19 @@ mod tests { }]) }); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let mut context: HashMap = HashMap::new(); - context.insert(String::from("bar"), String::from("baz")); context.insert(String::from("admin"), String::from("true")); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("123"), - context, - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("123"), + context, + }, + ); assert!(variant.is_ok()); @@ -2280,7 +2131,6 @@ mod tests { #[test] fn test_evaluator_match_any_rollout_distribution() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -2345,22 +2195,18 @@ mod tests { ]) }); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let mut context: HashMap = HashMap::new(); - context.insert(String::from("bar"), String::from("baz")); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("1"), - context: context.clone(), - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("1"), + context: context.clone(), + }, + ); assert!(variant.is_ok()); @@ -2372,11 +2218,15 @@ mod tests { assert_eq!(v.variant_key, String::from("variant1")); assert_eq!(v.segment_keys, vec![String::from("segment1")]); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("2"), - context, - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("2"), + context, + }, + ); assert!(variant.is_ok()); @@ -2391,7 +2241,6 @@ mod tests { #[test] fn test_evaluator_match_any_rollout_distribution_multi_rule() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -2475,22 +2324,18 @@ mod tests { ]) }); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let mut context: HashMap = HashMap::new(); - context.insert(String::from("premium_user"), String::from("true")); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("1"), - context: context.clone(), - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("1"), + context: context.clone(), + }, + ); assert!(variant.is_ok()); @@ -2505,7 +2350,6 @@ mod tests { #[test] fn test_evaluator_match_any_no_constraints() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -2557,20 +2401,17 @@ mod tests { ]) }); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let mut context: HashMap = HashMap::new(); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("10"), - context: context.clone(), - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("10"), + context: context.clone(), + }, + ); assert!(variant.is_ok()); @@ -2582,11 +2423,15 @@ mod tests { assert_eq!(v.variant_key, String::from("variant1")); assert_eq!(v.segment_keys, vec![String::from("segment1")]); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("01"), - context: context.clone(), - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("01"), + context: context.clone(), + }, + ); assert!(variant.is_ok()); @@ -2599,11 +2444,16 @@ mod tests { assert_eq!(v.segment_keys, vec![String::from("segment1")]); context.insert(String::from("foo"), String::from("bar")); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("01"), - context, - }); + + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("01"), + context, + }, + ); assert!(variant.is_ok()); @@ -2619,7 +2469,6 @@ mod tests { // Test cases where rollouts have a zero value #[test] fn test_evaluator_first_rollout_rule_zero() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -2676,22 +2525,18 @@ mod tests { ]) }); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let mut context: HashMap = HashMap::new(); - context.insert(String::from("bar"), String::from("baz")); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("1"), - context: context.clone(), - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("1"), + context: context.clone(), + }, + ); assert!(variant.is_ok()); @@ -2706,7 +2551,6 @@ mod tests { #[test] fn test_evaluator_multiple_zero_rollout_distributions() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -2787,22 +2631,18 @@ mod tests { ]) }); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let mut context: HashMap = HashMap::new(); - context.insert(String::from("bar"), String::from("baz")); - let variant = evaluator.variant(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("1"), - context: context.clone(), - }); + let variant = variant_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("1"), + context: context.clone(), + }, + ); assert!(variant.is_ok()); @@ -2817,7 +2657,6 @@ mod tests { #[test] fn test_boolean_notpresent() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -2858,18 +2697,15 @@ mod tests { }]) }); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - - let boolean = evaluator.boolean(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("1"), - context: HashMap::new(), - }); + let boolean = boolean_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("1"), + context: HashMap::new(), + }, + ); assert!(boolean.is_ok()); @@ -2882,7 +2718,6 @@ mod tests { #[test] fn test_boolean_present() { - let test_parser = TestParser::new(); let mut mock_store = MockStore::new(); mock_store.expect_get_flag().returning(|_, _| { @@ -2923,22 +2758,18 @@ mod tests { }]) }); - let evaluator = &Evaluator { - namespace: "default".into(), - parser: test_parser, - store: mock_store, - mtx: Arc::new(RwLock::new(0)), - }; - let mut context: HashMap = HashMap::new(); - context.insert(String::from("some"), String::from("baz")); - let boolean = evaluator.boolean(&EvaluationRequest { - flag_key: String::from("foo"), - entity_id: String::from("1"), - context: context, - }); + let boolean = boolean_evaluation( + &mock_store, + "default", + &EvaluationRequest { + flag_key: String::from("foo"), + entity_id: String::from("1"), + context, + }, + ); assert!(boolean.is_ok()); diff --git a/flipt-evaluation/src/models/mod.rs b/flipt-evaluation/src/models/mod.rs index 6777095a..c95bb0ce 100644 --- a/flipt-evaluation/src/models/mod.rs +++ b/flipt-evaluation/src/models/mod.rs @@ -1,3 +1,3 @@ -pub mod common; +pub(crate) mod common; pub mod flipt; pub mod source; diff --git a/flipt-evaluation/src/models/source.rs b/flipt-evaluation/src/models/source.rs index a7d86b1e..cbc23ce6 100644 --- a/flipt-evaluation/src/models/source.rs +++ b/flipt-evaluation/src/models/source.rs @@ -104,7 +104,7 @@ mod tests { #[test] fn test_deserialize_constraint_comparison_type_other_value() { let json = r#"{"type":"OTHER_CONSTRAINT_COMPARISON_TYPE","property":"this","operator":"eq","value":"something"}"#; - let constraint: SegmentConstraint = serde_json::from_str(&json).unwrap(); + let constraint: SegmentConstraint = serde_json::from_str(json).unwrap(); assert_eq!(ConstraintComparisonType::Unknown, constraint.r#type); } diff --git a/flipt-evaluation/src/parser/mod.rs b/flipt-evaluation/src/parser/mod.rs index f4cc8d82..d90e02a5 100644 --- a/flipt-evaluation/src/parser/mod.rs +++ b/flipt-evaluation/src/parser/mod.rs @@ -1,181 +1,15 @@ -use reqwest::header::{self, HeaderMap}; -use serde::Deserialize; - -use crate::error::Error; -use crate::models::source; - pub trait Parser { fn parse(&self, namespace: &str) -> Result; } -#[derive(Debug, Clone, Default, Deserialize)] -#[cfg_attr(test, derive(PartialEq))] -#[serde(rename_all = "snake_case")] -pub enum Authentication { - #[default] - None, - ClientToken(String), - JwtToken(String), -} - -impl Authentication { - pub fn with_client_token(token: String) -> Self { - Authentication::ClientToken(token) - } - - pub fn with_jwt_token(token: String) -> Self { - Authentication::JwtToken(token) - } - - pub fn authenticate(&self) -> Option { - match self { - Authentication::ClientToken(token) => { - let header_format: String = format!("Bearer {}", token).parse().unwrap(); - Some(header_format) - } - Authentication::JwtToken(token) => { - let header_format: String = format!("JWT {}", token).parse().unwrap(); - Some(header_format) - } - Authentication::None => None, - } - } -} - -impl From for HeaderMap { - fn from(value: Authentication) -> Self { - let mut header_map = HeaderMap::new(); - match value.authenticate() { - Some(val) => { - header_map.insert( - header::AUTHORIZATION, - header::HeaderValue::from_str(&val).unwrap(), - ); - - header_map - } - None => header_map, - } - } -} - -pub struct HTTPParser { - http_client: reqwest::blocking::Client, - http_url: String, - authentication: HeaderMap, - reference: Option, -} - -pub struct HTTPParserBuilder { - http_url: String, - authentication: HeaderMap, - reference: Option, -} - -impl HTTPParserBuilder { - pub fn new(http_url: &str) -> Self { - Self { - http_url: http_url.to_string(), - authentication: HeaderMap::new(), - reference: None, - } - } - - pub fn authentication(mut self, authentication: Authentication) -> Self { - self.authentication = HeaderMap::from(authentication); - self - } - - pub fn reference(mut self, reference: &str) -> Self { - self.reference = Some(reference.to_string()); - self - } - - pub fn build(self) -> HTTPParser { - HTTPParser { - http_client: reqwest::blocking::Client::builder() - .timeout(std::time::Duration::from_secs(10)) - .build() - .unwrap(), - http_url: self.http_url, - authentication: self.authentication, - reference: self.reference, - } - } -} - -impl HTTPParser { - fn url(&self, namespace: &str) -> String { - match &self.reference { - Some(reference) => { - format!( - "{}/internal/v1/evaluation/snapshot/namespace/{}?reference={}", - self.http_url, namespace, reference, - ) - } - None => { - format!( - "{}/internal/v1/evaluation/snapshot/namespace/{}", - self.http_url, namespace - ) - } - } - } -} - -impl Parser for HTTPParser { - fn parse(&self, namespace: &str) -> Result { - let mut headers = HeaderMap::new(); - headers.insert( - reqwest::header::CONTENT_TYPE, - reqwest::header::HeaderValue::from_static("application/json"), - ); - headers.insert( - reqwest::header::ACCEPT, - reqwest::header::HeaderValue::from_static("application/json"), - ); - // 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"), - ); - - for (key, value) in self.authentication.iter() { - headers.insert(key, value.clone()); - } - - let response = match self - .http_client - .get(self.url(namespace)) - .headers(headers) - .send() - { - Ok(resp) => match resp.error_for_status() { - Ok(resp) => resp, - Err(e) => return Err(Error::Server(format!("response: {}", e))), - }, - Err(e) => return Err(Error::Server(format!("failed to make request: {}", e))), - }; - - let response_text = match response.text() { - Ok(t) => t, - Err(e) => return Err(Error::Server(format!("failed to get response body: {}", e))), - }; - - let document: source::Document = match serde_json::from_str(&response_text) { - Ok(doc) => doc, - Err(e) => return Err(Error::InvalidJSON(e)), - }; - - Ok(document) - } -} - #[cfg(test)] use std::fs; #[cfg(test)] use std::path::PathBuf; +use crate::error::Error; +use crate::models::source; + #[cfg(test)] pub struct TestParser { path: Option, @@ -210,62 +44,3 @@ impl Parser for TestParser { Ok(document) } } - -#[cfg(test)] -mod tests { - use crate::parser::Authentication; - - #[test] - fn test_http_parser_url() { - use super::HTTPParserBuilder; - - let parser = HTTPParserBuilder::new("http://localhost:8080") - .authentication(Authentication::with_client_token("secret".into())) - .reference("ref") - .build(); - - assert_eq!( - parser.url("default"), - "http://localhost:8080/internal/v1/evaluation/snapshot/namespace/default?reference=ref" - ); - - let parser = HTTPParserBuilder::new("http://localhost:8080") - .authentication(Authentication::with_client_token("secret".into())) - .build(); - - assert_eq!( - parser.url("default"), - "http://localhost:8080/internal/v1/evaluation/snapshot/namespace/default" - ); - } - - #[test] - fn test_deserialize_no_auth() { - let json = r#""#; - - let unwrapped_string: Authentication = serde_json::from_str(&json).unwrap_or_default(); - - assert_eq!(unwrapped_string, Authentication::None); - } - - #[test] - fn test_deserialize_client_token() { - let json = r#"{"client_token":"secret"}"#; - - let unwrapped_string: Authentication = serde_json::from_str(&json).unwrap_or_default(); - - assert_eq!( - unwrapped_string, - Authentication::ClientToken("secret".into()) - ); - } - - #[test] - fn test_deserialize_jwt_token() { - let json = r#"{"jwt_token":"secret"}"#; - - let unwrapped_string: Authentication = serde_json::from_str(&json).unwrap_or_default(); - - assert_eq!(unwrapped_string, Authentication::JwtToken("secret".into())); - } -} diff --git a/flipt-evaluation/src/store/mod.rs b/flipt-evaluation/src/store/mod.rs index 278d7e5b..396740a3 100644 --- a/flipt-evaluation/src/store/mod.rs +++ b/flipt-evaluation/src/store/mod.rs @@ -1,7 +1,7 @@ use crate::error::Error; use crate::models::common; use crate::models::flipt; -use crate::parser::Parser; +use crate::models::source; #[cfg(test)] use mockall::automock; @@ -41,12 +41,7 @@ struct Namespace { } impl Snapshot { - pub fn build

(namespace: &str, parser: &P) -> Result - where - P: Parser + Send, - { - let doc = parser.parse(namespace)?; - + pub fn build(namespace: &str, doc: source::Document) -> Result { let mut flags: HashMap = HashMap::new(); let mut eval_rules: HashMap> = HashMap::new(); let mut eval_rollouts: HashMap> = HashMap::new(); @@ -269,13 +264,15 @@ mod tests { use super::{Snapshot, Store}; use crate::models::common; use crate::models::flipt; + use crate::parser::Parser; use crate::parser::TestParser; #[test] fn test_snapshot() { let tp = TestParser::new(); + let doc = tp.parse("default").unwrap(); - let snapshot = Snapshot::build("default".into(), &tp).unwrap(); + let snapshot = Snapshot::build("default", doc).unwrap(); let flag_variant = snapshot .get_flag("default", "flag1") diff --git a/release-please-config.json b/release-please-config.json index 0385a7f3..a5f62883 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -8,6 +8,10 @@ "flipt-engine-ffi": { "component": "flipt-engine-ffi", "release-type": "rust" + }, + "flipt-evaluation": { + "release-type": "rust", + "skip-github-release": true } } }