12
12
// See the License for the specific language governing permissions and
13
13
// limitations under the License.
14
14
15
+ mod errors;
16
+
15
17
use std:: collections:: HashMap ;
16
18
17
- use crate :: errors:: { Error , Result } ;
19
+ use crate :: entity:: { AttrValue , Entity } ;
20
+ use crate :: errors:: Result ;
18
21
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 } ;
27
24
28
25
pub ( crate ) fn find_applicable_segment_rule_for_entity (
29
26
segments : & HashMap < String , Segment > ,
@@ -34,26 +31,18 @@ pub(crate) fn find_applicable_segment_rule_for_entity(
34
31
targeting_rules. sort_by ( |a, b| a. order . cmp ( & b. order ) ) ;
35
32
36
33
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
- } ) ? {
34
+ if targeting_rule_applies_to_entity ( segments, & targeting_rule, entity) ? {
46
35
return Ok ( Some ( targeting_rule) ) ;
47
36
}
48
37
}
49
- return Ok ( None ) ;
38
+ Ok ( None )
50
39
}
51
40
52
41
fn targeting_rule_applies_to_entity (
53
42
segments : & HashMap < String , Segment > ,
54
43
targeting_rule : & TargetingRule ,
55
44
entity : & impl Entity ,
56
- ) -> AnyhowResult < bool > {
45
+ ) -> std :: result :: Result < bool , SegmentEvaluationError > {
57
46
// TODO: we need to get the naming correct here to distinguish between rules, segments, segment_ids, targeting_rules etc. correctly
58
47
let rules = & targeting_rule. rules ;
59
48
for rule in rules. iter ( ) {
@@ -69,21 +58,25 @@ fn segment_applies_to_entity(
69
58
segments : & HashMap < String , Segment > ,
70
59
segment_ids : & [ String ] ,
71
60
entity : & impl Entity ,
72
- ) -> AnyhowResult < bool > {
61
+ ) -> std :: result :: Result < bool , SegmentEvaluationError > {
73
62
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 ( ) ) ?;
79
69
if applies {
80
70
return Ok ( true ) ;
81
71
}
82
72
}
83
73
Ok ( false )
84
74
}
85
75
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 > {
87
80
for rule in segment. rules . iter ( ) {
88
81
let operator = & rule. operator ;
89
82
let attr_name = & rule. attribute_name ;
@@ -94,31 +87,25 @@ fn belong_to_segment(segment: &Segment, attrs: HashMap<String, AttrValue>) -> An
94
87
let rule_result = match attr_value {
95
88
None => {
96
89
println ! ( "Warning: Operation '{attr_name}' '{operator}' '[...]' failed to evaluate: '{attr_name}' not found in entity" ) ;
97
- Ok ( false )
90
+ false
98
91
}
99
92
Some ( attr_value) => {
100
93
// FIXME: the following algorithm is too hard to read. Is it just me or do we need to simplify this?
101
94
// One of the values needs to match.
102
95
// 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 > ( ( ) ) ) ,
110
101
Ok ( false ) => None ,
111
- Err ( e) => Some ( Err ( e ) ) ,
112
- }
113
- } ) ;
102
+ Err ( e) => Some ( Err ( ( e , segment , rule , value ) . into ( ) ) ) ,
103
+ } )
104
+ . transpose ( ) ? ;
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,89 +118,52 @@ 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
- // 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 ) ,
179
146
} ,
180
147
"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 ) ,
189
150
} ,
190
151
"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 ) ,
199
154
} ,
200
155
"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 ) ,
209
158
} ,
210
- _ => Err ( anyhow ! ( "Operator not implemented" ) ) ,
159
+ _ => Err ( CheckOperatorErrorDetail :: OperatorNotImplemented ) ,
211
160
}
212
161
}
213
162
214
163
#[ cfg( test) ]
215
164
pub mod tests {
216
165
use super :: * ;
166
+ use crate :: errors:: { EntityEvaluationError , Error } ;
217
167
use crate :: {
218
168
models:: { ConfigValue , Segment , SegmentRule , Segments , TargetingRule } ,
219
169
AttrValue ,
@@ -293,11 +243,20 @@ pub mod tests {
293
243
// Failed to evaluate entity: Failed to evaluate entity 'a2' against targeting rule '0'.
294
244
// Caused by: Segment 'non_existing_segment_id' not found.
295
245
// 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"));
301
260
}
302
261
303
262
// 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 {
316
275
// Caused by: Operation 'name' 'is' 'heinz' failed to evaluate.
317
276
// Caused by: Entity attribute has unexpected type: Number.
318
277
// 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" ) ;
325
290
}
326
291
}
0 commit comments