Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
.vscode
4 changes: 2 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -81,7 +81,7 @@
{
"hashed_secret": "fb34629c9af1ed4045b5d6f287426276b2be3a1e",
"is_verified": false,
"line_number": 303,
"line_number": 306,
"type": "Secret Keyword",
"verified_result": null
}
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -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.
limitations under the License.
1 change: 0 additions & 1 deletion data/data-dump-enterprise-plan-sdk-testing.json
Original file line number Diff line number Diff line change
Expand Up @@ -861,4 +861,3 @@
}
]
}

2 changes: 1 addition & 1 deletion data/data-dump-lite-plan-sdk-testing.json
Original file line number Diff line number Diff line change
Expand Up @@ -365,4 +365,4 @@
]
}
]
}
}
27 changes: 18 additions & 9 deletions src/client/app_configuration_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -107,8 +107,8 @@ impl AppConfigurationClient {
guid: &str,
collection_id: &str,
environment_id: &str,
) -> Result<ConfigurationSnapshot>{
loop{
) -> Result<ConfigurationSnapshot> {
loop {
// read() blocks until something happens.
match socket.read()? {
Message::Text(text) => match text.as_str() {
Expand All @@ -125,7 +125,7 @@ impl AppConfigurationClient {
},
Message::Close(_) => {
return Err(Error::Other("Connection closed by the server".into()));
},
}
_ => {}
}
}
Expand All @@ -151,16 +151,25 @@ impl AppConfigurationClient {
}
}

let config_snapshot = Self::wait_for_configuration_update(&mut socket, &access_token, &region, &guid, &collection_id, &environment_id);
let config_snapshot = Self::wait_for_configuration_update(
&mut socket,
&access_token,
&region,
&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
}
Expand Down
9 changes: 6 additions & 3 deletions src/client/feature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/client/feature_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 6 additions & 3 deletions src/client/property.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/client/property_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down
92 changes: 54 additions & 38 deletions src/segment_evaluation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Segment>,
Expand All @@ -33,19 +33,20 @@ pub(crate) fn find_applicable_segment_rule_for_entity(
let mut targeting_rules = segment_rules.collect::<Vec<_>>();
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(
Expand All @@ -55,9 +56,11 @@ fn targeting_rule_applies_to_entity(
) -> AnyhowResult<bool> {
// 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)
}
Expand All @@ -67,11 +70,15 @@ fn segment_applies_to_entity(
segment_ids: &[String],
entity: &impl Entity,
) -> AnyhowResult<bool> {
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)
}
Expand All @@ -80,59 +87,67 @@ fn belong_to_segment(segment: &Segment, attrs: HashMap<String, AttrValue>) -> 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<bool> {
fn check_operator(
attribute_value: &AttrValue,
operator: &str,
reference_value: &str,
) -> AnyhowResult<bool> {
match operator {
"is" => match attribute_value {
AttrValue::String(data) => Ok(*data == reference_value),
AttrValue::Boolean(data) => {
let result = *data
== reference_value
.parse::<bool>().map_err(|_| anyhow!("Entity attribute has unexpected type: Boolean."))?;
.parse::<bool>()
.map_err(|_| anyhow!("Entity attribute has unexpected type: Boolean."))?;
Ok(result)
}
AttrValue::Numeric(data) => {
let result = *data
== reference_value
.parse::<f64>().map_err(|_| anyhow!("Entity attribute has unexpected type: Number."))?;
Ok(result)
.parse::<f64>()
.map_err(|_| anyhow!("Entity attribute has unexpected type: Number."))?;
Ok(result)
}
},
"contains" => match attribute_value {
Expand Down Expand Up @@ -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")),
}
}

Expand Down Expand Up @@ -226,7 +239,7 @@ pub mod tests {
}

#[fixture]
fn segment_rules() -> Vec<TargetingRule>{
fn segment_rules() -> Vec<TargetingRule> {
vec![TargetingRule {
rules: vec![Segments {
segments: vec!["some_segment_id_1".into()],
Expand All @@ -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<String, Segment>, segment_rules: Vec<TargetingRule>) {
fn test_attribute_not_found(
segments: HashMap<String, Segment>,
segment_rules: Vec<TargetingRule>,
) {
let entity = crate::tests::GenericEntity {
id: "a2".into(),
attributes: HashMap::from([("name2".into(), AttrValue::from("heinz".to_string()))]),
Expand Down
Loading