Skip to content

Commit 1164943

Browse files
jgsogorainerschoe
andauthored
Use detailed error - Remove anyhow (#25)
Signed-off-by: Javier G. Sogo <[email protected]> Co-authored-by: Rainer Schoenberger <[email protected]>
1 parent b2afcf8 commit 1164943

File tree

4 files changed

+151
-110
lines changed

4 files changed

+151
-110
lines changed

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ tungstenite = { version = "0.24.0", features = ["native-tls"] }
1515
url = "2.5.2"
1616
http = "1.1.0"
1717
thiserror = "2.0.4"
18-
anyhow = "1.0.94"
1918

2019
[dev-dependencies]
2120
dotenvy = "0.15.7"

src/errors.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use std::sync::PoisonError;
22

33
use thiserror::Error;
44

5+
use crate::segment_evaluation::SegmentEvaluationError;
6+
57
pub type Result<T> = std::result::Result<T, Error>;
68

79
#[derive(Debug, Error)]
@@ -42,12 +44,22 @@ pub enum Error {
4244
ConfigurationAccessError(#[from] ConfigurationAccessError),
4345

4446
#[error("Failed to evaluate entity: {0}")]
45-
EntityEvaluationError(String),
47+
EntityEvaluationError(EntityEvaluationError),
4648

4749
#[error("{0}")]
4850
Other(String),
4951
}
5052

53+
#[derive(Debug, Error)]
54+
#[error(transparent)]
55+
pub struct EntityEvaluationError(pub(crate) SegmentEvaluationError);
56+
57+
impl From<SegmentEvaluationError> for Error {
58+
fn from(value: SegmentEvaluationError) -> Self {
59+
Self::EntityEvaluationError(EntityEvaluationError(value))
60+
}
61+
}
62+
5163
impl<T> From<PoisonError<T>> for Error {
5264
fn from(_value: PoisonError<T>) -> Self {
5365
Error::CannotAcquireLock

src/segment_evaluation/errors.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// (C) Copyright IBM Corp. 2024.
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 thiserror::Error;
16+
17+
use crate::models::{Segment, SegmentRule};
18+
19+
#[derive(Debug, Error)]
20+
pub(crate) enum SegmentEvaluationError {
21+
#[error(transparent)]
22+
SegmentEvaluationFailed(#[from] SegmentEvaluationErrorKind),
23+
24+
#[error("Segment ID '{0}' not found")]
25+
SegmentIdNotFound(String),
26+
}
27+
28+
#[derive(Debug, Error)]
29+
#[error("Operation '{}' '{}' '{}' failed to evaluate: {}", segment_rule.attribute_name, segment_rule.operator, value, source)]
30+
pub(crate) struct SegmentEvaluationErrorKind {
31+
pub(crate) segment: Segment,
32+
pub(crate) segment_rule: SegmentRule,
33+
pub(crate) value: String,
34+
pub(crate) source: CheckOperatorErrorDetail,
35+
}
36+
37+
impl From<(CheckOperatorErrorDetail, &Segment, &SegmentRule, &String)> for SegmentEvaluationError {
38+
fn from(value: (CheckOperatorErrorDetail, &Segment, &SegmentRule, &String)) -> Self {
39+
let (source, segment, segment_rule, value) = value;
40+
Self::SegmentEvaluationFailed(SegmentEvaluationErrorKind {
41+
segment: segment.clone(),
42+
segment_rule: segment_rule.clone(),
43+
value: value.clone(),
44+
source,
45+
})
46+
}
47+
}
48+
49+
#[derive(Debug, Error)]
50+
pub(crate) enum CheckOperatorErrorDetail {
51+
#[error("Entity attribute is not a string.")]
52+
StringExpected,
53+
54+
#[error("Entity attribute has unexpected type: Boolean.")]
55+
BooleanExpected(#[from] std::str::ParseBoolError),
56+
57+
#[error("Entity attribute has unexpected type: Number.")]
58+
NumberExpected(#[from] std::num::ParseFloatError),
59+
60+
#[error("Entity attribute is not a number.")]
61+
EntityAttrNotANumber,
62+
63+
#[error("Operator not implemented.")]
64+
OperatorNotImplemented,
65+
}

src/segment_evaluation.rs renamed to src/segment_evaluation/mod.rs

Lines changed: 73 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,15 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
mod errors;
16+
1517
use std::collections::HashMap;
1618

17-
use crate::errors::{Error, Result};
19+
use crate::entity::{AttrValue, Entity};
20+
use crate::errors::Result;
1821
use crate::models::Segment;
19-
use crate::{
20-
entity::{AttrValue, Entity},
21-
models::TargetingRule,
22-
};
23-
24-
// For chaining errors creating useful error messages:
25-
use anyhow::anyhow;
26-
use anyhow::{Context, Result as AnyhowResult};
22+
use crate::models::TargetingRule;
23+
pub(crate) use errors::{CheckOperatorErrorDetail, SegmentEvaluationError};
2724

2825
pub(crate) fn find_applicable_segment_rule_for_entity(
2926
segments: &HashMap<String, Segment>,
@@ -34,26 +31,18 @@ pub(crate) fn find_applicable_segment_rule_for_entity(
3431
targeting_rules.sort_by(|a, b| a.order.cmp(&b.order));
3532

3633
for targeting_rule in targeting_rules.into_iter() {
37-
if targeting_rule_applies_to_entity(segments, &targeting_rule, entity).map_err(|e| {
38-
// This terminates the use of anyhow in this module, converting all errors:
39-
let cause: String = e.chain().map(|c| format!("\nCaused by: {c}")).collect();
40-
Error::EntityEvaluationError(format!(
41-
"Failed to evaluate entity '{}' against targeting rule '{}'.{cause}",
42-
entity.get_id(),
43-
targeting_rule.order
44-
))
45-
})? {
34+
if targeting_rule_applies_to_entity(segments, &targeting_rule, entity)? {
4635
return Ok(Some(targeting_rule));
4736
}
4837
}
49-
return Ok(None);
38+
Ok(None)
5039
}
5140

5241
fn targeting_rule_applies_to_entity(
5342
segments: &HashMap<String, Segment>,
5443
targeting_rule: &TargetingRule,
5544
entity: &impl Entity,
56-
) -> AnyhowResult<bool> {
45+
) -> std::result::Result<bool, SegmentEvaluationError> {
5746
// TODO: we need to get the naming correct here to distinguish between rules, segments, segment_ids, targeting_rules etc. correctly
5847
let rules = &targeting_rule.rules;
5948
for rule in rules.iter() {
@@ -69,21 +58,25 @@ fn segment_applies_to_entity(
6958
segments: &HashMap<String, Segment>,
7059
segment_ids: &[String],
7160
entity: &impl Entity,
72-
) -> AnyhowResult<bool> {
61+
) -> std::result::Result<bool, SegmentEvaluationError> {
7362
for segment_id in segment_ids.iter() {
74-
let segment = segments.get(segment_id).ok_or(Error::Other(
75-
format!("Segment '{segment_id}' not found.").into(),
76-
))?;
77-
let applies = belong_to_segment(segment, entity.get_attributes())
78-
.context(format!("Failed to evaluate segment '{segment_id}'"))?;
63+
let segment = segments
64+
.get(segment_id)
65+
.ok_or(SegmentEvaluationError::SegmentIdNotFound(
66+
segment_id.clone(),
67+
))?;
68+
let applies = belong_to_segment(segment, entity.get_attributes())?;
7969
if applies {
8070
return Ok(true);
8171
}
8272
}
8373
Ok(false)
8474
}
8575

86-
fn belong_to_segment(segment: &Segment, attrs: HashMap<String, AttrValue>) -> AnyhowResult<bool> {
76+
fn belong_to_segment(
77+
segment: &Segment,
78+
attrs: HashMap<String, AttrValue>,
79+
) -> std::result::Result<bool, SegmentEvaluationError> {
8780
for rule in segment.rules.iter() {
8881
let operator = &rule.operator;
8982
let attr_name = &rule.attribute_name;
@@ -94,31 +87,25 @@ fn belong_to_segment(segment: &Segment, attrs: HashMap<String, AttrValue>) -> An
9487
let rule_result = match attr_value {
9588
None => {
9689
println!("Warning: Operation '{attr_name}' '{operator}' '[...]' failed to evaluate: '{attr_name}' not found in entity");
97-
Ok(false)
90+
false
9891
}
9992
Some(attr_value) => {
10093
// FIXME: the following algorithm is too hard to read. Is it just me or do we need to simplify this?
10194
// One of the values needs to match.
10295
// Find a candidate (a candidate corresponds to a value which matches or which might match but the operator failed):
103-
let candidate = rule.values.iter().find_map(|value| {
104-
let result_for_value =
105-
check_operator(attr_value, operator, value).context(format!(
106-
"Operation '{attr_name}' '{operator}' '{value}' failed to evaluate."
107-
));
108-
match result_for_value {
109-
Ok(true) => Some(Ok(())),
96+
let candidate = rule
97+
.values
98+
.iter()
99+
.find_map(|value| match check_operator(attr_value, operator, value) {
100+
Ok(true) => Some(Ok::<_, SegmentEvaluationError>(())),
110101
Ok(false) => None,
111-
Err(e) => Some(Err(e)),
112-
}
113-
});
102+
Err(e) => Some(Err((e, segment, rule, value).into())),
103+
})
104+
.transpose()?;
114105
// check if the candidate is good, or if the operator failed:
115-
match candidate {
116-
None => Ok(false),
117-
Some(Ok(())) => Ok(true),
118-
Some(Err(e)) => Err(e),
119-
}
106+
candidate.is_some()
120107
}
121-
}?;
108+
};
122109
// All rules must match:
123110
if !rule_result {
124111
return Ok(false);
@@ -131,89 +118,52 @@ fn check_operator(
131118
attribute_value: &AttrValue,
132119
operator: &str,
133120
reference_value: &str,
134-
) -> AnyhowResult<bool> {
121+
) -> std::result::Result<bool, CheckOperatorErrorDetail> {
135122
match operator {
136123
"is" => match attribute_value {
137124
AttrValue::String(data) => Ok(*data == reference_value),
138-
AttrValue::Boolean(data) => {
139-
let result = *data
140-
== reference_value
141-
.parse::<bool>()
142-
.map_err(|_| anyhow!("Entity attribute has unexpected type: Boolean."))?;
143-
Ok(result)
144-
}
145-
AttrValue::Numeric(data) => {
146-
let result = *data
147-
== reference_value
148-
.parse::<f64>()
149-
.map_err(|_| anyhow!("Entity attribute has unexpected type: Number."))?;
150-
Ok(result)
151-
}
125+
AttrValue::Boolean(data) => Ok(*data == reference_value.parse::<bool>()?),
126+
AttrValue::Numeric(data) => Ok(*data == reference_value.parse::<f64>()?),
152127
},
153128
"contains" => match attribute_value {
154129
AttrValue::String(data) => Ok(data.contains(reference_value)),
155-
_ => Err(anyhow!("Entity attribute is not a string.")),
130+
_ => Err(CheckOperatorErrorDetail::StringExpected),
156131
},
157132
"startsWith" => match attribute_value {
158133
AttrValue::String(data) => Ok(data.starts_with(reference_value)),
159-
_ => Err(anyhow!("Entity attribute is not a string.")),
134+
_ => Err(CheckOperatorErrorDetail::StringExpected),
160135
},
161136
"endsWith" => match attribute_value {
162137
AttrValue::String(data) => Ok(data.ends_with(reference_value)),
163-
_ => Err(anyhow!("Entity attribute is not a string.")),
138+
_ => Err(CheckOperatorErrorDetail::StringExpected),
164139
},
165140
"greaterThan" => match attribute_value {
166141
// TODO: Go implementation also compares strings (by parsing them as floats). Do we need this?
167142
// https://github.com/IBM/appconfiguration-go-sdk/blob/master/lib/internal/models/Rule.go#L82
168143
// TODO: we could have numbers not representable as f64, maybe we should try to parse it to i64 and u64 too?
169-
// TODO: we should have a different nesting style here: match the reference_value first and error out when given
170-
// entity attr does not match. This would yield more natural error messages
171-
AttrValue::Numeric(data) => {
172-
let result = *data
173-
> reference_value
174-
.parse()
175-
.map_err(|_| Error::Other("Value cannot convert into f64.".into()))?;
176-
Ok(result)
177-
}
178-
_ => Err(anyhow!("Entity attribute is not a number.")),
144+
AttrValue::Numeric(data) => Ok(*data > reference_value.parse()?),
145+
_ => Err(CheckOperatorErrorDetail::EntityAttrNotANumber),
179146
},
180147
"lesserThan" => match attribute_value {
181-
AttrValue::Numeric(data) => {
182-
let result = *data
183-
< reference_value
184-
.parse()
185-
.map_err(|_| Error::Other("Value cannot convert into f64.".into()))?;
186-
Ok(result)
187-
}
188-
_ => Err(anyhow!("Entity attribute is not a number.")),
148+
AttrValue::Numeric(data) => Ok(*data < reference_value.parse()?),
149+
_ => Err(CheckOperatorErrorDetail::EntityAttrNotANumber),
189150
},
190151
"greaterThanEquals" => match attribute_value {
191-
AttrValue::Numeric(data) => {
192-
let result = *data
193-
>= reference_value
194-
.parse()
195-
.map_err(|_| Error::Other("Value cannot convert into f64.".into()))?;
196-
Ok(result)
197-
}
198-
_ => Err(anyhow!("Entity attribute is not a number.")),
152+
AttrValue::Numeric(data) => Ok(*data >= reference_value.parse()?),
153+
_ => Err(CheckOperatorErrorDetail::EntityAttrNotANumber),
199154
},
200155
"lesserThanEquals" => match attribute_value {
201-
AttrValue::Numeric(data) => {
202-
let result = *data
203-
<= reference_value
204-
.parse()
205-
.map_err(|_| Error::Other("Value cannot convert into f64.".into()))?;
206-
Ok(result)
207-
}
208-
_ => Err(anyhow!("Entity attribute is not a number.")),
156+
AttrValue::Numeric(data) => Ok(*data <= reference_value.parse()?),
157+
_ => Err(CheckOperatorErrorDetail::EntityAttrNotANumber),
209158
},
210-
_ => Err(anyhow!("Operator not implemented")),
159+
_ => Err(CheckOperatorErrorDetail::OperatorNotImplemented),
211160
}
212161
}
213162

214163
#[cfg(test)]
215164
pub mod tests {
216165
use super::*;
166+
use crate::errors::{EntityEvaluationError, Error};
217167
use crate::{
218168
models::{ConfigValue, Segment, SegmentRule, Segments, TargetingRule},
219169
AttrValue,
@@ -293,11 +243,20 @@ pub mod tests {
293243
// Failed to evaluate entity: Failed to evaluate entity 'a2' against targeting rule '0'.
294244
// Caused by: Segment 'non_existing_segment_id' not found.
295245
// We are checking here that the parts are present to allow debugging of config by the user:
296-
let msg = rule.unwrap_err().to_string();
297-
assert!(msg.contains("'a2'"));
298-
assert!(msg.contains("'0'"));
299-
assert!(msg.contains("'non_existing_segment_id'"));
300-
assert!(msg.contains("not found"));
246+
let e = rule.unwrap_err();
247+
assert!(matches!(e, Error::EntityEvaluationError(_)));
248+
let Error::EntityEvaluationError(EntityEvaluationError(
249+
SegmentEvaluationError::SegmentIdNotFound(ref segment_id),
250+
)) = e
251+
else {
252+
panic!("Error type mismatch!");
253+
};
254+
assert_eq!(segment_id, "non_existing_segment_id");
255+
// let msg = rule.unwrap_err().to_string();
256+
// assert!(msg.contains("'a2'"));
257+
// assert!(msg.contains("'0'"));
258+
// assert!(msg.contains("'non_existing_segment_id'"));
259+
// assert!(msg.contains("not found"));
301260
}
302261

303262
// SCENARIO - evaluating an operator fails. Meaning, [for example] user has added a numeric value(int/float) in appconfig segment attribute, but in their application they pass the attribute with a boolean value.
@@ -316,11 +275,17 @@ pub mod tests {
316275
// Caused by: Operation 'name' 'is' 'heinz' failed to evaluate.
317276
// Caused by: Entity attribute has unexpected type: Number.
318277
// We are checking here that the parts are present to allow debugging of config by the user:
319-
let msg = rule.unwrap_err().to_string();
320-
assert!(msg.contains("'a2'"));
321-
assert!(msg.contains("'0'"));
322-
assert!(msg.contains("'some_segment_id_1'"));
323-
assert!(msg.contains("'name' 'is' 'heinz'"));
324-
assert!(msg.contains("Entity attribute has unexpected type: Number"));
278+
279+
let e = rule.unwrap_err();
280+
assert!(matches!(e, Error::EntityEvaluationError(_)));
281+
let Error::EntityEvaluationError(EntityEvaluationError(
282+
SegmentEvaluationError::SegmentEvaluationFailed(ref error),
283+
)) = e
284+
else {
285+
panic!("Error type mismatch!");
286+
};
287+
assert_eq!(error.segment.name, "");
288+
assert_eq!(error.segment_rule.attribute_name, "name");
289+
assert_eq!(error.value, "heinz");
325290
}
326291
}

0 commit comments

Comments
 (0)