Skip to content

Commit 8d6636e

Browse files
authored
Merge branch 'main' into token_provider/test-network
2 parents 80b2bf1 + 5573f3c commit 8d6636e

File tree

11 files changed

+240
-135
lines changed

11 files changed

+240
-135
lines changed

.github/workflows/pre-commit.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
runs-on: ubuntu-latest
1111
steps:
1212
- uses: actions/checkout@v5
13-
- uses: actions/setup-python@v5
13+
- uses: actions/setup-python@v6
1414
- name: Run pre-commit
1515
run: |
1616
pip install pre-commit

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ repos:
3838
# Add options for detect-secrets-hook binary. You can run `detect-secrets-hook --help` to list out all possible options.
3939
# add "--fail-on-unaudited" to fail pre-commit for unaudited potential secrets
4040
args: [--baseline, .secrets.baseline, --use-all-plugins]
41+
additional_dependencies: [boxsdk<4]

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ reqwest = { version = "0.12.9", features = ["json", "blocking"] }
2626
serde = { version = "1.0.216", features = ["derive"] }
2727
serde_json = { version = "1.0.133" }
2828
murmur3 = "0.5.2"
29-
tungstenite = { version = "0.27.0", features = ["native-tls"] }
29+
tungstenite = { version = "0.28.0", features = ["native-tls"] }
3030
url = "2.5.4"
3131
thiserror = "2.0.7"
3232
chrono = { version = "0.4", features = ["serde"] }
@@ -36,7 +36,7 @@ log = "0.4.27"
3636
appconfiguration = {path = ".", features = ["test_utils"]}
3737
dotenvy = "0.15.7"
3838
rstest = "0.26.0"
39-
httpmock = "0.7.0"
39+
httpmock = "0.8.0"
4040

4141
[badges]
4242
github = { repository = "IBM/appconfiguration-rust-sdk" }

examples/demo.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,11 @@ fn main() -> std::result::Result<(), Box<dyn Error>> {
6060
city: "Bangalore".to_string(),
6161
radius: 60,
6262
};
63-
63+
thread::sleep(Duration::from_secs(5));
6464
println!("The information is displayed every 5 seconds.");
6565
println!("Try changing the configuraiton in the App Configuration instances.");
6666

6767
loop {
68-
println!("\n\nFEATURE FLAG OPERATIONS\n");
69-
7068
match client.get_feature_proxy(&feature_id) {
7169
Ok(feature) => {
7270
println!("Feature name: {}", feature.get_name()?);
@@ -78,8 +76,6 @@ fn main() -> std::result::Result<(), Box<dyn Error>> {
7876
println!("There was an error getting the Feature Flag. Error {error}",);
7977
}
8078
}
81-
82-
println!("\n\nPROPERTY OPERATIONS\n");
8379
match client.get_property_proxy(&property_id) {
8480
Ok(property) => {
8581
println!("Property name: {}", property.get_name()?);

src/metering/client_http.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ impl MeteringClientHttp {
2929
impl MeteringClient for MeteringClientHttp {
3030
fn push_metering_data(&self, guid: &str, data: &MeteringDataJson) -> MeteringResult<()> {
3131
let url = format!(
32-
"{}/apprapp/events/v1/instances/{}/usage",
32+
"{}/events/v1/instances/{}/usage",
3333
self.service_address.base_url(ServiceAddressProtocol::Http),
3434
guid
3535
);
@@ -82,7 +82,7 @@ pub(crate) mod tests {
8282
let server = MockServer::start();
8383
let mock = server.mock(|when, then| {
8484
when.method(POST)
85-
.path("/apprapp/events/v1/instances/example_guid/usage")
85+
.path("/events/v1/instances/example_guid/usage")
8686
.header("content-type", "application/json")
8787
.header("Authorization", "Bearer mocked_token")
8888
.json_body(json!(

src/metering/metering.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
use log::debug;
1516
use log::warn;
1617

1718
use crate::metering::models::{
@@ -46,6 +47,7 @@ pub(crate) fn start_metering<T: MeteringClient>(
4647
let thread = ThreadHandle::new(move |_terminator: mpsc::Receiver<()>| {
4748
let mut batcher = MeteringBatcher::new(client, config_id);
4849
let mut last_flush = std::time::Instant::now();
50+
debug!("Starting Metering transmitting thread");
4951
loop {
5052
let recv_result = receiver.recv_timeout(std::time::Duration::from_millis(100));
5153
match recv_result {
@@ -196,9 +198,19 @@ impl<T: MeteringClient> MeteringBatcher<T> {
196198
json_data.add_usage(evaluation.0, evaluation.1);
197199
}
198200

199-
let _ = self
201+
debug!(
202+
"Sending metering data for {} usages.",
203+
json_data.usages.len()
204+
);
205+
let result = self
200206
.client
201207
.push_metering_data(&self.config_id.guid, &json_data);
208+
match result {
209+
Err(err) => {
210+
warn!("Sending metering data failed: {}", err);
211+
}
212+
_ => {}
213+
}
202214
self.evaluations.clear();
203215
}
204216
}

src/models/feature_snapshot.rs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,14 @@ impl FeatureSnapshot {
9999
}
100100
}
101101

102+
fn normalized_hash(data: &str) -> u32 {
103+
let hash = murmur3_32(&mut Cursor::new(data), 0).expect("Cannot hash the value.");
104+
(f64::from(hash) / f64::from(u32::MAX) * 100.0) as u32
105+
}
106+
102107
fn should_rollout(rollout_percentage: u32, entity: &impl Entity, feature_id: &str) -> bool {
103108
let tag = format!("{}:{}", entity.get_id(), feature_id);
104-
rollout_percentage == 100 || random_value(&tag) < rollout_percentage
109+
rollout_percentage == 100 || Self::normalized_hash(&tag) < rollout_percentage
105110
}
106111

107112
fn use_rollout_percentage_to_get_value_from_feature_directly(
@@ -139,15 +144,6 @@ impl Feature for FeatureSnapshot {
139144
}
140145
}
141146

142-
pub(crate) fn random_value(v: &str) -> u32 {
143-
let max_hash = u32::MAX;
144-
(f64::from(hash(v)) / f64::from(max_hash) * 100.0) as u32
145-
}
146-
147-
fn hash(v: &str) -> u32 {
148-
murmur3_32(&mut Cursor::new(v), 0).expect("Cannot hash the value.")
149-
}
150-
151147
#[cfg(test)]
152148
pub mod tests {
153149

@@ -213,7 +209,9 @@ pub mod tests {
213209
attributes: entity_attributes.clone(),
214210
};
215211
assert_eq!(
216-
random_value(format!("{}:{}", entity.id, feature.feature_id).as_str()),
212+
FeatureSnapshot::normalized_hash(
213+
format!("{}:{}", entity.id, feature.feature_id).as_str()
214+
),
217215
68
218216
);
219217
let value = feature.get_value(&entity).unwrap();
@@ -225,7 +223,9 @@ pub mod tests {
225223
attributes: entity_attributes,
226224
};
227225
assert_eq!(
228-
random_value(format!("{}:{}", entity.id, feature.feature_id).as_str()),
226+
FeatureSnapshot::normalized_hash(
227+
format!("{}:{}", entity.id, feature.feature_id).as_str()
228+
),
229229
29
230230
);
231231
let value = feature.get_value(&entity).unwrap();
@@ -407,4 +407,12 @@ pub mod tests {
407407
let value = feature.get_value(&entity).unwrap();
408408
assert!(matches!(value, Value::Int64(ref v) if v == &2));
409409
}
410+
411+
/// This test ensures that the rust client is using the same hashing algorithm as to other clients.
412+
/// See same test for Node client:
413+
/// https://github.com/IBM/appconfiguration-node-sdk/blob/master/test/unit/configurations/internal/utils.test.js#L25
414+
#[test]
415+
fn test_normalized_hash() {
416+
assert_eq!(FeatureSnapshot::normalized_hash("entityId:featureId"), 41)
417+
}
410418
}

src/segment_evaluation/errors.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,14 @@ pub(crate) struct SegmentEvaluationErrorKind {
4141
pub(crate) source: CheckOperatorErrorDetail,
4242
}
4343

44-
impl From<(CheckOperatorErrorDetail, &Segment, &Rule, &String)> for SegmentEvaluationError {
45-
fn from(value: (CheckOperatorErrorDetail, &Segment, &Rule, &String)) -> Self {
44+
impl From<(CheckOperatorErrorDetail, &Segment, &Rule, String)> for SegmentEvaluationError {
45+
fn from(value: (CheckOperatorErrorDetail, &Segment, &Rule, String)) -> Self {
4646
let (source, segment, segment_rule, value) = value;
4747
Self::SegmentEvaluationFailed(SegmentEvaluationErrorKind {
4848
segment_id: segment.segment_id.clone(),
4949
segment_rule_attribute_name: segment_rule.attribute_name.clone(),
5050
segment_rule_operator: segment_rule.operator.clone(),
51-
value: value.clone(),
51+
value: value,
5252
source,
5353
})
5454
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// (C) Copyright IBM Corp. 2025.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use std::collections::HashMap;
16+
17+
use super::errors::CheckOperatorErrorDetail;
18+
use crate::network::serialization::{Rule, Segment};
19+
use crate::segment_evaluation::errors::SegmentEvaluationError;
20+
use crate::segment_evaluation::rule_operator::RuleOperator;
21+
use crate::Value;
22+
23+
pub(crate) trait MatchesAttributes {
24+
type Error;
25+
26+
fn matches_attributes(
27+
&self,
28+
attributes: &HashMap<String, Value>,
29+
) -> std::result::Result<bool, Self::Error>;
30+
}
31+
32+
impl MatchesAttributes for Segment {
33+
type Error = SegmentEvaluationError;
34+
35+
/// A [`Segment`] matches attributes iif:
36+
/// * ALL the rules match the attributes
37+
fn matches_attributes(
38+
&self,
39+
attributes: &HashMap<String, Value>,
40+
) -> std::result::Result<bool, Self::Error> {
41+
self.rules
42+
.iter()
43+
.map(|rule| {
44+
rule.matches_attributes(attributes)
45+
.map_err(|(e, rule_value)| (e, self, rule, rule_value).into())
46+
})
47+
.collect::<std::result::Result<Vec<bool>, _>>()
48+
.map(|v| v.iter().all(|&x| x))
49+
}
50+
}
51+
52+
impl MatchesAttributes for Rule {
53+
type Error = (CheckOperatorErrorDetail, String);
54+
55+
/// A [`Rule`] matches attributes iif:
56+
/// * the attributes contain the requested attribute, AND
57+
/// * the attribute satisfies ANY of the rule values.
58+
///
59+
/// TODO: What if rules.values is empty? Now it returns false
60+
fn matches_attributes(
61+
&self,
62+
attributes: &HashMap<String, Value>,
63+
) -> std::result::Result<bool, Self::Error> {
64+
attributes
65+
.get(&self.attribute_name)
66+
.map_or(Ok(false), |attr_value| {
67+
self.values
68+
.iter()
69+
.map(|value| {
70+
attr_value
71+
.operate(&self.operator, value)
72+
.map_err(|e| (e, value.to_owned()))
73+
})
74+
.collect::<std::result::Result<Vec<bool>, _>>()
75+
.map(|v| v.iter().any(|&x| x))
76+
})
77+
}
78+
}

0 commit comments

Comments
 (0)