File Coverage

blib/lib/WebService/GrowthBook/Eval.pm
Criterion Covered Total %
statement 177 186 95.1
branch 135 144 93.7
condition 22 36 61.1
subroutine 20 20 100.0
pod 0 12 0.0
total 354 398 88.9


line stmt bran cond sub pod time code
1             package WebService::GrowthBook::Eval;
2 4     4   256030 use strict;
  4         11  
  4         213  
3 4     4   28 use warnings;
  4         11  
  4         299  
4 4     4   607 no indirect;
  4         1459  
  4         62  
5 4     4   321 use Exporter 'import';
  4         9  
  4         273  
6 4     4   34 use Scalar::Util qw(looks_like_number);
  4         8  
  4         316  
7 4     4   3264 use Data::Dumper;
  4         67294  
  4         591  
8 4     4   689 use Syntax::Keyword::Try;
  4         2956  
  4         42  
9 4     4   1183 use JSON::MaybeXS qw(is_bool);
  4         11728  
  4         27525  
10              
11             our $VERSION = '0.003'; ## VERSION
12              
13             our @EXPORT_OK = qw(eval_condition);
14              
15             sub eval_condition {
16 179     179 0 465999 my ($attributes, $condition) = @_;
17 179 100       660 if (exists $condition->{"\$or"}) {
18 5         18 my $r = eval_or($attributes, $condition->{"\$or"});
19 5         19 return $r;
20             }
21 174 100       499 if (exists $condition->{"\$nor"}) {
22 4         20 return !eval_or($attributes, $condition->{"\$nor"});
23             }
24 170 100       416 if (exists $condition->{"\$and"}) {
25 7         40 my $r = eval_and($attributes, $condition->{"\$and"});
26 7         29 return $r;
27             }
28 163 100       429 if (exists $condition->{"\$not"}) {
29 2         12 return !eval_condition($attributes, $condition->{"\$not"});
30             }
31              
32 161         570 for my $key (keys %$condition){
33 163         390 my $value = $condition->{$key};
34 163 100       417 if (!eval_condition_value($value, get_path($attributes, $key))) {
35 91         471 return 0;
36             }
37             }
38              
39 70         322 return 1;
40             }
41              
42             sub get_path {
43 163     163 0 375 my ($attributes, $path) = @_;
44 163         303 my $current = $attributes;
45              
46 163         536 foreach my $segment (split /\./, $path) {
47 175 100 100     1079 if (ref($current) eq 'HASH' && exists $current->{$segment}) {
48 168         447 $current = $current->{$segment};
49             } else {
50 7         37 return undef;
51             }
52             }
53 156         586 return $current;
54             }
55              
56             sub eval_or {
57 9     9 0 23 my ($attributes, $conditions) = @_;
58              
59 9 100       54 if (scalar @$conditions == 0) {
60 1         7 return 1; # True
61             }
62              
63 8         23 foreach my $condition (@$conditions) {
64 12 100       34 if (eval_condition($attributes, $condition)) {
65 6         28 return 1; # True
66             }
67             }
68 2         9 return 0; # False
69             }
70             sub eval_and {
71 7     7 0 18 my ($attributes, $conditions) = @_;
72              
73 7         20 foreach my $condition (@$conditions) {
74 10 100       28 if (!eval_condition($attributes, $condition)) {
75 3         10 return 0; # False
76             }
77             }
78 4         13 return 1; # True
79             }
80              
81              
82             sub eval_condition_value {
83 209     209 0 551 my ($condition_value, $attribute_value) = @_;
84 209 100 100     721 if (ref($condition_value) eq 'HASH' && is_operator_object($condition_value)) {
85 152         401 for my $key (keys %$condition_value){
86 177         397 my $value = $condition_value->{$key};
87 177 100       464 if (!eval_operator_condition($key, $attribute_value, $value)) {
88 89         337 return 0; # False
89             }
90             }
91 63         293 return 1; # True
92             }
93 57 100       174 if (ref($condition_value) eq 'ARRAY') {
94 5 100       17 if(ref($attribute_value) ne 'ARRAY'){
95 1         4 return 0;
96             }
97 4 100       15 if(scalar @$condition_value != scalar @$attribute_value){
98 2         10 return 0;
99             }
100 2         8 for my $i (0..$#$condition_value){
101 3 100       9 if(!eval_condition_value($condition_value->[$i], $attribute_value->[$i])){
102 1         6 return 0;
103             }
104             }
105 1         5 return 1;
106             }
107              
108 52 100       140 if(ref($condition_value) eq 'HASH'){
109 4 100       13 if(ref($attribute_value) ne 'HASH'){
110 1         5 return 0;
111             }
112 3 100       12 if(scalar keys %$condition_value != scalar keys %$attribute_value){
113 2         8 return 0;
114             }
115 1         4 for my $key (keys %$condition_value){
116 1 50       5 if(!exists $attribute_value->{$key}){
117 0         0 return 0;
118             }
119 1 50       7 if(!eval_condition_value($condition_value->{$key}, $attribute_value->{$key})){
120 0         0 return 0;
121             }
122             }
123 1         5 return 1;
124              
125             }
126 48 100 100     319 if(!defined($condition_value) && !defined($attribute_value)){
    100 100        
127 2         10 return 1;
128             }
129             elsif(!defined($condition_value) || !defined($attribute_value)){
130 5         18 return 0;
131             }
132 41         150 return $condition_value eq $attribute_value;
133             }
134              
135             sub is_operator_object {
136 182     182 0 420 my ($obj) = @_;
137              
138 182         514 foreach my $key (keys %$obj) {
139 217 100       686 if (substr($key, 0, 1) ne '$') {
140 8         32 return 0; # False
141             }
142             }
143 174         662 return 1; # True
144             }
145              
146             sub compare {
147 52     52 0 117 my ($va, $vb) = @_;
148 52 50 66     256 if(looks_like_number($va) && ! defined($vb)){
149 0         0 $vb = 0;
150             }
151 52 50 66     183 if(looks_like_number($vb) && ! defined($va)){
152 0         0 $va = 0;
153             }
154 52 100 66     181 if(looks_like_number($va) && looks_like_number($vb)){
155 34         121 return $va <=> $vb;
156             }
157             else {
158 18         80 return $va cmp $vb;
159             }
160             }
161             sub eval_operator_condition {
162 177     177 0 515 my ($operator, $attribute_value, $condition_value) = @_;
163 177 100       1545 if ($operator eq '$eq') {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
164             try {
165             return compare($attribute_value, $condition_value) == 0;
166             }
167 10         27 catch {
168             return 0;
169             }
170             } elsif ($operator eq '$ne') {
171             try {
172             return compare($attribute_value, $condition_value) != 0;
173             }
174 2         7 catch {
175             return 0;
176             }
177             } elsif ($operator eq '$lt') {
178             try {
179             return compare($attribute_value, $condition_value) < 0;
180             }
181 7         17 catch {
182             return 0;
183             }
184             } elsif ($operator eq '$lte') {
185             try {
186             return compare($attribute_value, $condition_value) <= 0;
187             }
188 4         9 catch {
189             return 0;
190             }
191             } elsif ($operator eq '$gt') {
192             try {
193             my $r = compare($attribute_value, $condition_value);
194             return $r > 0;
195             }
196 25         61 catch {
197             return 0;
198             }
199             } elsif ($operator eq '$gte') {
200             try {
201             return compare($attribute_value, $condition_value) >= 0;
202             }
203 4         12 catch {
204             return 0;
205             }
206             } elsif ($operator eq '$veq') {
207 2         6 return padded_version_string($attribute_value) eq padded_version_string($condition_value);
208             } elsif ($operator eq '$vne') {
209 2         8 return padded_version_string($attribute_value) ne padded_version_string($condition_value);
210             } elsif ($operator eq '$vlt') {
211 10         26 return padded_version_string($attribute_value) lt padded_version_string($condition_value);
212             } elsif ($operator eq '$vlte') {
213 8         20 return padded_version_string($attribute_value) le padded_version_string($condition_value);
214             } elsif ($operator eq '$vgt') {
215 13         37 return padded_version_string($attribute_value) gt padded_version_string($condition_value);
216             } elsif ($operator eq '$vgte') {
217 8         22 return padded_version_string($attribute_value) ge padded_version_string($condition_value);
218             } elsif ($operator eq '$regex') {
219             try {
220             my $r = qr/$condition_value/;
221             return $attribute_value =~ $r;
222             }
223 14         34 catch {
224             return 0;
225             }
226             } elsif ($operator eq '$in') {
227 16 100       52 return 0 unless ref($condition_value) eq 'ARRAY';
228 15         44 return is_in($condition_value, $attribute_value);
229             } elsif ($operator eq '$nin') {
230 8 100       30 return 0 unless ref($condition_value) eq 'ARRAY';
231 7         23 return !is_in($condition_value, $attribute_value);
232             } elsif ($operator eq '$elemMatch') {
233 11         36 return elem_match($condition_value, $attribute_value);
234             } elsif ($operator eq '$size') {
235 9 100       53 return 0 unless ref($attribute_value) eq 'ARRAY';
236 8         29 return eval_condition_value($condition_value, scalar @$attribute_value);
237             } elsif ($operator eq '$all') {
238 3 100       16 return 0 unless ref($attribute_value) eq 'ARRAY';
239 2         8 foreach my $cond (@$condition_value) {
240 4         9 my $passing = 0;
241 4         9 foreach my $attr (@$attribute_value) {
242 8 100       19 if (eval_condition_value($cond, $attr)) {
243 3         6 $passing = 1;
244 3         8 last;
245             }
246             }
247 4 100       24 return 0 unless $passing;
248             }
249 1         6 return 1;
250             } elsif ($operator eq '$exists') {
251 4 100       24 return !$condition_value ? !defined $attribute_value : defined $attribute_value;
252             } elsif ($operator eq '$type') {
253 12         35 my $r = get_type($attribute_value);
254 12         70 return $r eq $condition_value;
255             } elsif ($operator eq '$not') {
256 4         13 return !eval_condition_value($condition_value, $attribute_value);
257             }
258 1         5 return 0;
259             }
260              
261              
262             sub padded_version_string {
263 212     212 0 77533 my ($input) = @_;
264              
265             # If input is a number, convert to a string
266 212 50       871 if (looks_like_number($input)) {
267 0         0 $input = "$input";
268             }
269              
270 212 50 33     1224 if (!defined $input || ref($input) || $input eq '') {
      33        
271 0         0 $input = "0";
272             }
273              
274             # Remove build info and leading `v` if any
275 212         1058 $input =~ s/^v|\+.*$//g;
276              
277             # Split version into parts (both core version numbers and pre-release tags)
278 212         883 my @parts = split(/[-.]/, $input);
279              
280             # If it's SemVer without a pre-release, add `~` to the end
281 212 100       538 if (scalar(@parts) == 3) {
282 130         284 push @parts, "~";
283             }
284              
285             # Left pad each numeric part with spaces so string comparisons will work ("9">"10", but " 9"<"10")
286 212 100       442 @parts = map { /^\d+$/ ? sprintf("%5s", $_) : $_ } @parts;
  895         3260  
287              
288             # Join back together into a single string
289 212         2194 return join("-", @parts);
290             }
291             sub is_in {
292 22     22 0 48 my ($condition_value, $attribute_value) = @_;
293 22 100       56 return 0 unless defined($attribute_value);
294 21 100       60 if (ref($attribute_value) eq 'ARRAY') {
295 10         25 my %condition_hash = map { $_ => 1 } @$condition_value;
  20         77  
296 10         29 foreach my $item (@$attribute_value) {
297 20 100       75 return 1 if exists $condition_hash{$item};
298             }
299 4         22 return 0;
300             }
301 11         28 return grep { $_ eq $attribute_value } @$condition_value;
  26         102  
302             }
303              
304             sub elem_match {
305 11     11 0 27 my ($condition, $attribute_value) = @_;
306              
307             # Check if $attribute_value is an array reference
308 11 100       42 return 0 unless ref($attribute_value) eq 'ARRAY';
309              
310 10         25 foreach my $item (@$attribute_value) {
311 26 100       52 if (is_operator_object($condition)) {
312 22 100       55 if (eval_condition_value($condition, $item)) {
313 4         18 return 1;
314             }
315             } else {
316 4 100       11 if (eval_condition($item, $condition)) {
317 1         5 return 1;
318             }
319             }
320             }
321              
322 5         19 return 0;
323             }
324              
325             sub get_type {
326 12     12 0 32 my ($attribute_value) = @_;
327 12 100       32 if (!defined $attribute_value) {
328 1         5 return "null";
329             }
330 11 100       36 if (ref($attribute_value) eq '') {
331 8 100 66     60 if ($attribute_value =~ /^[+-]?\d+$/ || $attribute_value =~ /^[+-]?\d*\.\d+$/) {
332 6         18 return "number";
333             }
334 2         8 return "string";
335             }
336 3 100       17 if (is_bool($attribute_value)) {
337 1         23 return "boolean";
338             }
339 2 100       24 if (ref($attribute_value) eq 'ARRAY') {
340 1         5 return "array";
341             }
342 1 50       6 if (ref($attribute_value) eq 'HASH') {
343 1         3 return "object";
344             }
345 0 0 0       if (ref($attribute_value) eq 'SCALAR' && ($$attribute_value eq '0' || $$attribute_value eq '1')) {
      0        
346 0           return "boolean";
347             }
348 0           return "unknown";
349             }
350              
351             1;