14
14
15
15
use std:: collections:: HashMap ;
16
16
17
- use crate :: errors:: { Error , Result } ;
17
+ use crate :: errors:: { CheckOperatorErrorDetail , Result , SegmentEvaluationError } ;
18
18
use crate :: models:: Segment ;
19
19
use crate :: {
20
20
entity:: { AttrValue , Entity } ,
21
21
models:: TargetingRule ,
22
22
} ;
23
23
24
- // For chaining errors creating useful error messages:
25
- use anyhow:: anyhow;
26
- use anyhow:: { Context , Result as AnyhowResult } ;
27
-
28
24
pub ( crate ) fn find_applicable_segment_rule_for_entity (
29
25
segments : & HashMap < String , Segment > ,
30
26
segment_rules : impl Iterator < Item = TargetingRule > ,
@@ -34,26 +30,18 @@ pub(crate) fn find_applicable_segment_rule_for_entity(
34
30
targeting_rules. sort_by ( |a, b| a. order . cmp ( & b. order ) ) ;
35
31
36
32
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 ! ( "\n Caused 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
- } ) ? {
33
+ if targeting_rule_applies_to_entity ( segments, & targeting_rule, entity) ? {
46
34
return Ok ( Some ( targeting_rule) ) ;
47
35
}
48
36
}
49
- return Ok ( None ) ;
37
+ Ok ( None )
50
38
}
51
39
52
40
fn targeting_rule_applies_to_entity (
53
41
segments : & HashMap < String , Segment > ,
54
42
targeting_rule : & TargetingRule ,
55
43
entity : & impl Entity ,
56
- ) -> AnyhowResult < bool > {
44
+ ) -> std :: result :: Result < bool , SegmentEvaluationError > {
57
45
// TODO: we need to get the naming correct here to distinguish between rules, segments, segment_ids, targeting_rules etc. correctly
58
46
let rules = & targeting_rule. rules ;
59
47
for rule in rules. iter ( ) {
@@ -69,21 +57,25 @@ fn segment_applies_to_entity(
69
57
segments : & HashMap < String , Segment > ,
70
58
segment_ids : & [ String ] ,
71
59
entity : & impl Entity ,
72
- ) -> AnyhowResult < bool > {
60
+ ) -> std :: result :: Result < bool , SegmentEvaluationError > {
73
61
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}'" ) ) ?;
62
+ let segment = segments
63
+ . get ( segment_id)
64
+ . ok_or ( SegmentEvaluationError :: SegmentIdNotFound (
65
+ segment_id. clone ( ) ,
66
+ ) ) ?;
67
+ let applies = belong_to_segment ( segment, entity. get_attributes ( ) ) ?;
79
68
if applies {
80
69
return Ok ( true ) ;
81
70
}
82
71
}
83
72
Ok ( false )
84
73
}
85
74
86
- fn belong_to_segment ( segment : & Segment , attrs : HashMap < String , AttrValue > ) -> AnyhowResult < bool > {
75
+ fn belong_to_segment (
76
+ segment : & Segment ,
77
+ attrs : HashMap < String , AttrValue > ,
78
+ ) -> std:: result:: Result < bool , SegmentEvaluationError > {
87
79
for rule in segment. rules . iter ( ) {
88
80
let operator = & rule. operator ;
89
81
let attr_name = & rule. attribute_name ;
@@ -94,31 +86,26 @@ fn belong_to_segment(segment: &Segment, attrs: HashMap<String, AttrValue>) -> An
94
86
let rule_result = match attr_value {
95
87
None => {
96
88
println ! ( "Warning: Operation '{attr_name}' '{operator}' '[...]' failed to evaluate: '{attr_name}' not found in entity" ) ;
97
- Ok ( false )
89
+ false
98
90
}
99
91
Some ( attr_value) => {
100
92
// FIXME: the following algorithm is too hard to read. Is it just me or do we need to simplify this?
101
93
// One of the values needs to match.
102
94
// 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 {
95
+ let candidate = rule
96
+ . values
97
+ . iter ( )
98
+ . find_map ( |value| match check_operator ( attr_value, operator, value) {
109
99
Ok ( true ) => Some ( Ok ( ( ) ) ) ,
110
100
Ok ( false ) => None ,
111
101
Err ( e) => Some ( Err ( e) ) ,
112
- }
113
- } ) ;
102
+ } )
103
+ . transpose ( )
104
+ . map_err ( |e| ( e, segment, rule, attr_value) ) ?;
114
105
// 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 ( )
120
107
}
121
- } ? ;
108
+ } ;
122
109
// All rules must match:
123
110
if !rule_result {
124
111
return Ok ( false ) ;
@@ -131,83 +118,47 @@ fn check_operator(
131
118
attribute_value : & AttrValue ,
132
119
operator : & str ,
133
120
reference_value : & str ,
134
- ) -> AnyhowResult < bool > {
121
+ ) -> std :: result :: Result < bool , CheckOperatorErrorDetail > {
135
122
match operator {
136
123
"is" => match attribute_value {
137
124
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 > ( ) ?) ,
152
127
} ,
153
128
"contains" => match attribute_value {
154
129
AttrValue :: String ( data) => Ok ( data. contains ( reference_value) ) ,
155
- _ => Err ( anyhow ! ( "Entity attribute is not a string." ) ) ,
130
+ _ => Err ( CheckOperatorErrorDetail :: StringExpected ) ,
156
131
} ,
157
132
"startsWith" => match attribute_value {
158
133
AttrValue :: String ( data) => Ok ( data. starts_with ( reference_value) ) ,
159
- _ => Err ( anyhow ! ( "Entity attribute is not a string." ) ) ,
134
+ _ => Err ( CheckOperatorErrorDetail :: StringExpected ) ,
160
135
} ,
161
136
"endsWith" => match attribute_value {
162
137
AttrValue :: String ( data) => Ok ( data. ends_with ( reference_value) ) ,
163
- _ => Err ( anyhow ! ( "Entity attribute is not a string." ) ) ,
138
+ _ => Err ( CheckOperatorErrorDetail :: StringExpected ) ,
164
139
} ,
165
140
"greaterThan" => match attribute_value {
166
141
// TODO: Go implementation also compares strings (by parsing them as floats). Do we need this?
167
142
// https://github.com/IBM/appconfiguration-go-sdk/blob/master/lib/internal/models/Rule.go#L82
168
143
// TODO: we could have numbers not representable as f64, maybe we should try to parse it to i64 and u64 too?
169
144
// TODO: we should have a different nesting style here: match the reference_value first and error out when given
170
145
// 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." ) ) ,
146
+ AttrValue :: Numeric ( data) => Ok ( * data > reference_value. parse ( ) ?) ,
147
+ _ => Err ( CheckOperatorErrorDetail :: EntityAttrNotANumber ) ,
179
148
} ,
180
149
"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." ) ) ,
150
+ AttrValue :: Numeric ( data) => Ok ( * data < reference_value. parse ( ) ?) ,
151
+ _ => Err ( CheckOperatorErrorDetail :: EntityAttrNotANumber ) ,
189
152
} ,
190
153
"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." ) ) ,
154
+ AttrValue :: Numeric ( data) => Ok ( * data >= reference_value. parse ( ) ?) ,
155
+ _ => Err ( CheckOperatorErrorDetail :: EntityAttrNotANumber ) ,
199
156
} ,
200
157
"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." ) ) ,
158
+ AttrValue :: Numeric ( data) => Ok ( * data <= reference_value. parse ( ) ?) ,
159
+ _ => Err ( CheckOperatorErrorDetail :: EntityAttrNotANumber ) ,
209
160
} ,
210
- _ => Err ( anyhow ! ( "Operator not implemented" ) ) ,
161
+ _ => Err ( CheckOperatorErrorDetail :: OperatorNotImplemented ) ,
211
162
}
212
163
}
213
164
@@ -218,6 +169,7 @@ pub mod tests {
218
169
models:: { ConfigValue , Segment , SegmentRule , Segments , TargetingRule } ,
219
170
AttrValue ,
220
171
} ;
172
+ use crate :: errors:: Error ;
221
173
use rstest:: * ;
222
174
223
175
#[ fixture]
@@ -316,7 +268,15 @@ pub mod tests {
316
268
// Caused by: Operation 'name' 'is' 'heinz' failed to evaluate.
317
269
// Caused by: Entity attribute has unexpected type: Number.
318
270
// 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 ( ) ;
271
+
272
+ let e = rule. unwrap_err ( ) ;
273
+ assert ! ( matches!(
274
+ e,
275
+ Error :: EntityEvaluationError ( ref v)
276
+ ) ) ;
277
+
278
+ let msg = e. to_string ( ) ;
279
+ assert_eq ! ( msg, "lol" ) ;
320
280
assert ! ( msg. contains( "'a2'" ) ) ;
321
281
assert ! ( msg. contains( "'0'" ) ) ;
322
282
assert ! ( msg. contains( "'some_segment_id_1'" ) ) ;
0 commit comments