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
}
}
}