diff --git a/.gitignore b/.gitignore index ad0389a..370dbc8 100644 --- a/.gitignore +++ b/.gitignore @@ -18,11 +18,11 @@ Cargo.lock *.pdb ### rust-analyzer ### -# Can be generated by other build systems other than cargo (ex: bazelbuild/rust_rules) +# Can be generated by other build systems other than cargo (ex: bazelbuild/rust_rules) rust-project.json # End of https://www.toptal.com/developers/gitignore/api/rust,rust-analyzer .env -.vscode \ No newline at end of file +.vscode diff --git a/.secrets.baseline b/.secrets.baseline index f94024d..2ffdc24 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$", "lines": null }, - "generated_at": "2024-12-09T10:13:16Z", + "generated_at": "2024-12-10T11:33:54Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -81,7 +81,7 @@ { "hashed_secret": "fb34629c9af1ed4045b5d6f287426276b2be3a1e", "is_verified": false, - "line_number": 303, + "line_number": 306, "type": "Secret Keyword", "verified_result": null } diff --git a/LICENSE b/LICENSE index bf4c474..fc81e23 100644 --- a/LICENSE +++ b/LICENSE @@ -198,4 +198,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. diff --git a/data/data-dump-enterprise-plan-sdk-testing.json b/data/data-dump-enterprise-plan-sdk-testing.json index fb62cfb..9bfaa53 100644 --- a/data/data-dump-enterprise-plan-sdk-testing.json +++ b/data/data-dump-enterprise-plan-sdk-testing.json @@ -861,4 +861,3 @@ } ] } - \ No newline at end of file diff --git a/data/data-dump-lite-plan-sdk-testing.json b/data/data-dump-lite-plan-sdk-testing.json index 9ec2e1b..e98ca0f 100644 --- a/data/data-dump-lite-plan-sdk-testing.json +++ b/data/data-dump-lite-plan-sdk-testing.json @@ -365,4 +365,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/client/app_configuration_client.rs b/src/client/app_configuration_client.rs index 9b22f8b..82877b2 100644 --- a/src/client/app_configuration_client.rs +++ b/src/client/app_configuration_client.rs @@ -18,7 +18,7 @@ pub use crate::client::feature_proxy::FeatureProxy; use crate::client::http; use crate::client::property::Property; pub use crate::client::property_proxy::PropertyProxy; -use crate::errors::{ConfigurationAccessError, Result, Error}; +use crate::errors::{ConfigurationAccessError, Error, Result}; use crate::models::Segment; use std::collections::{HashMap, HashSet}; use std::net::TcpStream; @@ -107,8 +107,8 @@ impl AppConfigurationClient { guid: &str, collection_id: &str, environment_id: &str, - ) -> Result{ - loop{ + ) -> Result { + loop { // read() blocks until something happens. match socket.read()? { Message::Text(text) => match text.as_str() { @@ -125,7 +125,7 @@ impl AppConfigurationClient { }, Message::Close(_) => { return Err(Error::Other("Connection closed by the server".into())); - }, + } _ => {} } } @@ -151,16 +151,25 @@ impl AppConfigurationClient { } } - let config_snapshot = Self::wait_for_configuration_update(&mut socket, &access_token, ®ion, &guid, &collection_id, &environment_id); + let config_snapshot = Self::wait_for_configuration_update( + &mut socket, + &access_token, + ®ion, + &guid, + &collection_id, + &environment_id, + ); - match config_snapshot{ + match config_snapshot { Ok(config_snapshot) => *latest_config_snapshot.lock()? = config_snapshot, - Err(e) => {println!("Waiting for configuration update failed. Stopping to monitor for changes.: {e}"); break;} + Err(e) => { + println!("Waiting for configuration update failed. Stopping to monitor for changes.: {e}"); + break; + } } } Ok::<(), Error>(()) - } - ); + }); sender } diff --git a/src/client/feature.rs b/src/client/feature.rs index 71ec764..e2b96fd 100644 --- a/src/client/feature.rs +++ b/src/client/feature.rs @@ -42,9 +42,12 @@ impl Feature { crate::models::ValueKind::Numeric => { Value::Numeric(NumericValue(model_value.0.clone())) } - crate::models::ValueKind::Boolean => { - Value::Boolean(model_value.0.as_bool().ok_or(Error::ProtocolError("Expected Boolean".into()))?) - } + crate::models::ValueKind::Boolean => Value::Boolean( + model_value + .0 + .as_bool() + .ok_or(Error::ProtocolError("Expected Boolean".into()))?, + ), crate::models::ValueKind::String => Value::String( model_value .0 diff --git a/src/client/feature_proxy.rs b/src/client/feature_proxy.rs index 97d0770..e7a1a7e 100644 --- a/src/client/feature_proxy.rs +++ b/src/client/feature_proxy.rs @@ -171,7 +171,8 @@ impl FeatureProxy { .segments, self.get_targeting_rules().into_iter(), entity, - ).unwrap_or_else(|e| panic!("Failed to evaluate segment rules: {e}")); + ) + .unwrap_or_else(|e| panic!("Failed to evaluate segment rules: {e}")); if let Some(segment_rule) = segment_rule { let rollout_percentage = self.resolve_rollout_percentage(&segment_rule); if rollout_percentage == 100 || random_value(&tag) < rollout_percentage { diff --git a/src/client/property.rs b/src/client/property.rs index 579b595..1dfae87 100644 --- a/src/client/property.rs +++ b/src/client/property.rs @@ -40,9 +40,12 @@ impl Property { crate::models::ValueKind::Numeric => { Value::Numeric(NumericValue(model_value.0.clone())) } - crate::models::ValueKind::Boolean => { - Value::Boolean(model_value.0.as_bool().ok_or(Error::ProtocolError("Expected Boolean".into()))?) - } + crate::models::ValueKind::Boolean => Value::Boolean( + model_value + .0 + .as_bool() + .ok_or(Error::ProtocolError("Expected Boolean".into()))?, + ), crate::models::ValueKind::String => Value::String( model_value .0 diff --git a/src/client/property_proxy.rs b/src/client/property_proxy.rs index f015e91..c6af27b 100644 --- a/src/client/property_proxy.rs +++ b/src/client/property_proxy.rs @@ -126,7 +126,8 @@ impl PropertyProxy { .segments, self.get_targeting_rules().into_iter(), entity, - ).unwrap_or_else(|e| panic!("Failed to evaluate segment rules: {e}")); + ) + .unwrap_or_else(|e| panic!("Failed to evaluate segment rules: {e}")); if let Some(segment_rule) = segment_rule { self.resolve_value(&segment_rule) } else { diff --git a/src/lib.rs b/src/lib.rs index deec868..aa8f211 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,9 +14,9 @@ pub mod client; pub mod entity; +pub mod errors; pub mod models; mod segment_evaluation; -pub mod errors; pub use entity::{AttrValue, Entity}; diff --git a/src/segment_evaluation.rs b/src/segment_evaluation.rs index b465d90..cdaa5ff 100644 --- a/src/segment_evaluation.rs +++ b/src/segment_evaluation.rs @@ -14,16 +14,16 @@ use std::collections::HashMap; +use crate::errors::{Error, Result}; use crate::models::Segment; use crate::{ entity::{AttrValue, Entity}, models::TargetingRule, }; -use crate::errors::{Result, Error}; // For chaining errors creating useful error messages: -use anyhow::{Context, Result as AnyhowResult}; use anyhow::anyhow; +use anyhow::{Context, Result as AnyhowResult}; pub(crate) fn find_applicable_segment_rule_for_entity( segments: &HashMap, @@ -33,19 +33,20 @@ pub(crate) fn find_applicable_segment_rule_for_entity( let mut targeting_rules = segment_rules.collect::>(); targeting_rules.sort_by(|a, b| a.order.cmp(&b.order)); - for targeting_rule in targeting_rules.into_iter(){ - if targeting_rule_applies_to_entity(segments, &targeting_rule, entity) - .map_err( - |e|{ + for targeting_rule in targeting_rules.into_iter() { + if targeting_rule_applies_to_entity(segments, &targeting_rule, entity).map_err(|e| { // This terminates the use of anyhow in this module, converting all errors: let cause: String = e.chain().map(|c| format!("\nCaused by: {c}")).collect(); - Error::EntityEvaluationError(format!("Failed to evaluate entity '{}' against targeting rule '{}'.{cause}", entity.get_id(), targeting_rule.order)) - })? - { + Error::EntityEvaluationError(format!( + "Failed to evaluate entity '{}' against targeting rule '{}'.{cause}", + entity.get_id(), + targeting_rule.order + )) + })? { return Ok(Some(targeting_rule)); } } - return Ok(None) + return Ok(None); } fn targeting_rule_applies_to_entity( @@ -55,9 +56,11 @@ fn targeting_rule_applies_to_entity( ) -> AnyhowResult { // TODO: we need to get the naming correct here to distinguish between rules, segments, segment_ids, targeting_rules etc. correctly let rules = &targeting_rule.rules; - for rule in rules.iter(){ + for rule in rules.iter() { let rule_applies = segment_applies_to_entity(segments, &rule.segments, entity)?; - if rule_applies {return Ok(true)} + if rule_applies { + return Ok(true); + } } Ok(false) } @@ -67,11 +70,15 @@ fn segment_applies_to_entity( segment_ids: &[String], entity: &impl Entity, ) -> AnyhowResult { - for segment_id in segment_ids.iter(){ - let segment = segments.get(segment_id) - .ok_or(Error::Other(format!("Segment '{segment_id}' not found.").into()))?; - let applies = belong_to_segment(segment, entity.get_attributes()).context(format!("Failed to evaluate segment '{segment_id}'"))?; - if applies {return Ok(true)} + for segment_id in segment_ids.iter() { + let segment = segments.get(segment_id).ok_or(Error::Other( + format!("Segment '{segment_id}' not found.").into(), + ))?; + let applies = belong_to_segment(segment, entity.get_attributes()) + .context(format!("Failed to evaluate segment '{segment_id}'"))?; + if applies { + return Ok(true); + } } Ok(false) } @@ -80,59 +87,67 @@ fn belong_to_segment(segment: &Segment, attrs: HashMap) -> An for rule in segment.rules.iter() { let operator = &rule.operator; let attr_name = &rule.attribute_name; - let attr_value = attrs - .get(attr_name); - if attr_value.is_none(){ - return Ok(false) + let attr_value = attrs.get(attr_name); + if attr_value.is_none() { + return Ok(false); } - let rule_result = match attr_value{ + let rule_result = match attr_value { None => { println!("Warning: Operation '{attr_name}' '{operator}' '[...]' failed to evaluate: '{attr_name}' not found in entity"); Ok(false) - }, + } Some(attr_value) => { // FIXME: the following algorithm is too hard to read. Is it just me or do we need to simplify this? // One of the values needs to match. // Find a candidate (a candidate corresponds to a value which matches or which might match but the operator failed): let candidate = rule.values.iter().find_map(|value| { - let result_for_value = check_operator(attr_value, operator, value).context(format!("Operation '{attr_name}' '{operator}' '{value}' failed to evaluate.")); - match result_for_value{ + let result_for_value = + check_operator(attr_value, operator, value).context(format!( + "Operation '{attr_name}' '{operator}' '{value}' failed to evaluate." + )); + match result_for_value { Ok(true) => Some(Ok(())), Ok(false) => None, - Err(e) => Some(Err(e)) + Err(e) => Some(Err(e)), } }); // check if the candidate is good, or if the operator failed: - match candidate{ + match candidate { None => Ok(false), Some(Ok(())) => Ok(true), - Some(Err(e)) => Err(e) + Some(Err(e)) => Err(e), } } }?; // All rules must match: if !rule_result { - return Ok(false) + return Ok(false); } } Ok(true) } -fn check_operator(attribute_value: &AttrValue, operator: &str, reference_value: &str) -> AnyhowResult { +fn check_operator( + attribute_value: &AttrValue, + operator: &str, + reference_value: &str, +) -> AnyhowResult { match operator { "is" => match attribute_value { AttrValue::String(data) => Ok(*data == reference_value), AttrValue::Boolean(data) => { let result = *data == reference_value - .parse::().map_err(|_| anyhow!("Entity attribute has unexpected type: Boolean."))?; + .parse::() + .map_err(|_| anyhow!("Entity attribute has unexpected type: Boolean."))?; Ok(result) } AttrValue::Numeric(data) => { let result = *data == reference_value - .parse::().map_err(|_| anyhow!("Entity attribute has unexpected type: Number."))?; - Ok(result) + .parse::() + .map_err(|_| anyhow!("Entity attribute has unexpected type: Number."))?; + Ok(result) } }, "contains" => match attribute_value { @@ -192,9 +207,7 @@ fn check_operator(attribute_value: &AttrValue, operator: &str, reference_value: } _ => Err(anyhow!("Entity attribute is not a number.")), }, - _ => { - Err(anyhow!("Operator not implemented")) - } + _ => Err(anyhow!("Operator not implemented")), } } @@ -226,7 +239,7 @@ pub mod tests { } #[fixture] - fn segment_rules() -> Vec{ + fn segment_rules() -> Vec { vec![TargetingRule { rules: vec![Segments { segments: vec!["some_segment_id_1".into()], @@ -241,7 +254,10 @@ pub mod tests { // EXAMPLE - Assume two teams are using same featureflag. One team is interested only in enabled_value & disabled_value. This team doesn’t pass attributes for their evaluation. Other team wants to have overridden_value, as a result they update the featureflag by adding segment rules to it. This team passes attributes in their evaluation to get the overridden_value for matching segment, and enabled_value for non-matching segment. // We should not fail the evaluation. #[rstest] - fn test_attribute_not_found(segments: HashMap, segment_rules: Vec) { + fn test_attribute_not_found( + segments: HashMap, + segment_rules: Vec, + ) { let entity = crate::tests::GenericEntity { id: "a2".into(), attributes: HashMap::from([("name2".into(), AttrValue::from("heinz".to_string()))]),