File Coverage

blib/lib/URI/VersionRange.pm
Criterion Covered Total %
statement 146 147 99.3
branch 44 54 81.4
condition 23 27 85.1
subroutine 37 37 100.0
pod 12 12 100.0
total 262 277 94.5


line stmt bran cond sub pod time code
1             package URI::VersionRange;
2              
3 4     4   420316 use feature ':5.10';
  4         11  
  4         714  
4 4     4   26 use strict;
  4         6  
  4         166  
5 4     4   1171 use utf8;
  4         721  
  4         27  
6 4     4   139 use warnings;
  4         9  
  4         257  
7              
8 4     4   24 use Carp ();
  4         9  
  4         106  
9 4     4   20 use List::Util qw(first);
  4         8  
  4         350  
10 4     4   23 use Exporter qw(import);
  4         6  
  4         152  
11              
12 4     4   2829 use URI::VersionRange::Constraint;
  4         16  
  4         322  
13 4     4   29 use URI::VersionRange::Util qw(native_range_to_vers version_compare);
  4         7  
  4         220  
14 4     4   22 use URI::VersionRange::Version;
  4         8  
  4         126  
15              
16 4     4   19 use constant DEBUG => $ENV{VERS_DEBUG};
  4         7  
  4         359  
17 4     4   23 use constant TRUE => !!1;
  4         9  
  4         204  
18 4     4   24 use constant FALSE => !!0;
  4         9  
  4         187  
19              
20 4     4   20 use overload '""' => 'to_string', fallback => 1;
  4         7  
  4         20  
21              
22 4     4   13728 BEGIN { *VERS:: = *URI::VersionRange:: }
23              
24             our $VERSION = '2.25';
25             our @EXPORT = qw(encode_vers decode_vers);
26              
27             my $VERS_REGEXP = qr{^vers:[a-z\\.\\-\\+][a-z0-9\\.\\-\\+]*/.+};
28              
29             sub new {
30              
31 791     791 1 2262 my ($class, %params) = @_;
32              
33 791 50       2313 my $scheme = delete $params{scheme} or Carp::croak "Invalid Version Range: 'scheme' is required";
34 791 50       1912 my $constraints = delete $params{constraints} or Carp::croak "Invalid Version Range: 'constraints' is required";
35              
36 791         1365 my @constraints = ();
37              
38 791         1098 foreach my $constraint (@{$constraints}) {
  791         1585  
39              
40 1156 100       2669 if (ref($constraint) ne 'URI::VersionRange::Constraint') {
41 6         18 $constraint = URI::VersionRange::Constraint->from_string($constraint);
42             }
43              
44 1156         2180 push @constraints, $constraint;
45              
46             }
47              
48 791         1446 $scheme = lc $scheme;
49              
50 791         2866 my $scheme_class = URI::VersionRange::Version->load($scheme);
51              
52 791         3497 my $self = {scheme => $scheme, constraints => \@constraints, scheme_class => $scheme_class};
53              
54 791         15079 return bless $self, $class;
55              
56             }
57              
58 2609     2609 1 8534 sub scheme { shift->{scheme} }
59 1609     1609 1 5289 sub constraints { shift->{constraints} }
60              
61 1     1 1 9 sub encode_vers { __PACKAGE__->new(@_)->to_string }
62 23     23 1 20524 sub decode_vers { __PACKAGE__->from_string(shift) }
63              
64             sub from_native {
65              
66 737     737 1 7887909 my ($class, %params) = @_;
67              
68 737 50       3081 my $scheme = delete $params{scheme} or Carp::croak "Invalid Version Range: 'scheme' is required";
69 737 100       3297 my $range = delete $params{range} or Carp::croak "Invalid Version Range: 'range' is required";
70              
71 736         3441 my $vers = native_range_to_vers(lc $scheme, $range);
72 736         2917 return $class->from_string($vers);
73              
74             }
75              
76             sub from_string {
77              
78 791     791 1 330311 my ($class, $string) = @_;
79              
80 791 100       6001 if ($string !~ /$VERS_REGEXP/) {
81 1         178 Carp::croak 'Malformed Version Range string';
82             }
83              
84 790 100 100     3204 if ($string =~ /^vers\:(none|all)\// && $string !~ /^vers\:(none|all)\/\*$/) {
85 2         312 Carp::croak 'Malformed Version Range string';
86             }
87              
88 788         1498 my %params = ();
89              
90             # - Remove all spaces and tabs.
91             # - Start from left, and split once on colon ":".
92             # - The left hand side is the URI-scheme that must be lowercase.
93             # Tools must validate that the URI-scheme value is vers.
94             # - The right hand side is the specifier.
95              
96 788         5151 $string =~ s/(\s|\t)+//g;
97              
98 788         2064 my @s1 = split(':', $string);
99              
100             # - Split the specifier from left once on a slash "/".
101             # - The left hand side is the that must be lowercase. Tools
102             # should validate that the is a known scheme.
103             # - The right hand side is a list of one or more constraints. Tools must validate
104             # that this constraints string is not empty ignoring spaces.
105              
106 788         1729 my @s2 = split('/', $s1[1]);
107 788         2208 $params{scheme} = lc $s2[0];
108              
109             # - If the constraints string is equal to "", the ````
110             # is "". Parsing is done and no further processing is needed for this vers.
111             # A tool should report an error if there are extra characters beyond "*".
112             # - Strip leading and trailing pipes "|" from the constraints string.
113             # - Split the constraints on pipe "|". The result is a list of .
114             # Consecutive pipes must be treated as one and leading and trailing pipes ignored.
115              
116 788         3485 $s2[1] =~ s/(^\|)|(\|$)//g;
117              
118 788         1666 my @s3 = split(/\|/, $s2[1]);
119 788         2003 $params{constraints} = [];
120              
121             # - For each :
122             # - Determine if the starts with one of the two comparators:
123             # - If it starts with ">=", then the comparator is ">=".
124             # - If it starts with "<=", then the comparator is "<=".
125             # - If it starts with "!=", then the comparator is "!=".
126             # - If it starts with "<", then the comparator is "<".
127             # - If it starts with ">", then the comparator is ">".
128             # - Remove the comparator from string start. The remaining string is the version.
129             # - Otherwise the version is the full string (which implies an equality comparator of "=")
130             # - Tools should validate and report an error if the version is empty.
131             # - If the version contains a percent "%" character, apply URL quoting rules to unquote this string.
132             # - Append the parsed (comparator, version) to the constraints list.
133              
134 788         2118 foreach (@s3) {
135 1147         1677 push @{$params{constraints}}, URI::VersionRange::Constraint->from_string($_);
  1147         4194  
136             }
137              
138 788         1107 if (DEBUG) {
139             say STDERR "-- S1: @s1";
140             say STDERR "-- S2: @s2";
141             say STDERR "-- S3: @s3";
142             }
143              
144 788         2201 return $class->new(%params);
145              
146             }
147              
148             sub to_string {
149 1538     1538 1 12556 my $self = shift;
150 1538         2222 my @constraints = sort { version_compare($self->scheme, $a->version, $b->version) } @{$self->constraints};
  1070         2582  
  1538         3355  
151 1538         3479 return join '', 'vers:', $self->scheme, '/', join('|', @constraints);
152             }
153              
154             sub constraint_contains {
155              
156 23     23 1 51 my ($self, $constraint, $version) = @_;
157              
158 23 100       83 return TRUE if $constraint->comparator eq '*';
159              
160 7         17 my $version_class = $self->{scheme_class};
161              
162 7         20 my $v1 = $version_class->new($version);
163 7         20 my $v2 = $version_class->new($constraint->version);
164              
165 7 50       16 return ($v1 == $v2) if ($constraint->comparator eq '=');
166 7 50       15 return ($v1 != $v2) if ($constraint->comparator eq '!=');
167 7 100       18 return ($v1 <= $v2) if ($constraint->comparator eq '<=');
168 6 100       13 return ($v1 >= $v2) if ($constraint->comparator eq '>=');
169 5 100       12 return ($v1 < $v2) if ($constraint->comparator eq '<');
170 1 50       2 return ($v1 > $v2) if ($constraint->comparator eq '>');
171              
172 0         0 return FALSE;
173              
174             }
175              
176             sub contains {
177              
178 35     35 1 5746 my ($self, $version) = @_;
179              
180 35         67 my @first = ();
181 35         82 my @second = ();
182              
183 35         79 my $version_class = $self->{scheme_class};
184              
185 35 100       50 if (scalar @{$self->constraints} == 1) {
  35         89  
186 21         55 return $self->constraint_contains($self->constraints->[0], $version);
187             }
188              
189 14         23 foreach my $constraint (@{$self->constraints}) {
  14         25  
190              
191             # If the "tested version" is equal to the any of the constraint version
192             # where the constraint comparator is for equality (any of "=", "<=", or ">=")
193             # then the "tested version" is in the range. Check is finished.
194              
195             return TRUE
196 38 100 100 88   134 if ((first { $constraint->comparator eq $_ } ('=', '<=', '>='))
  88         184  
197             && ($version_class->new($version) == $version_class->new($constraint->version)));
198              
199             # If the "tested version" is equal to the any of the constraint version
200             # where the constraint comparator is "=!" then the "tested version" is NOT
201             # in the range. Check is finished.
202              
203 36 100 100     131 return FALSE
204             if ($constraint->comparator eq '!='
205             && ($version_class->new($version) == $version_class->new($constraint->version)));
206              
207             # Split the constraint list in two sub lists:
208             # a first list where the comparator is "=" or "!="
209             # a second list where the comparator is neither "=" nor "!="
210              
211 34 100   61   121 push @first, $constraint if ((first { $constraint->comparator eq $_ } ('=', '!=')));
  61         124  
212 34 100   61   123 push @second, $constraint if (!(first { $constraint->comparator eq $_ } ('=', '!=')));
  61         114  
213              
214             }
215              
216 10 50       22 return FALSE unless @second;
217              
218 10 100       24 if (scalar @second == 1) {
219 2         7 return $self->constraint_contains($second[0], $version);
220             }
221              
222             # Iterate over the current and next contiguous constraints pairs (aka. pairwise)
223             # in the second list.
224              
225             # For each current and next constraint:
226              
227 8         15 my $is_first_iteration = TRUE;
228              
229 8         14 my $current_constraint = undef;
230 8         12 my $next_constraint = undef;
231              
232 8         19 foreach (_pairwise(@second)) {
233              
234 8         26 ($current_constraint, $next_constraint) = @{$_};
  8         21  
235              
236 8         26 DEBUG and say STDERR sprintf '-- Current constraint --> %s', $current_constraint;
237 8         19 DEBUG and say STDERR sprintf '-- Next constraint --> %s', $next_constraint;
238              
239             # If this is the first iteration and current comparator is "<" or <=" and
240             # the "tested version" is less than the current version then the "tested
241             # version" is IN the range. Check is finished.
242              
243 8 50       23 if ($is_first_iteration) {
244              
245             return TRUE
246 8 50 66 15   30 if ((first { $current_constraint->comparator eq $_ } ('<=', '<'))
  15         31  
247             && ($version_class->new($version) < $version_class->new($current_constraint->version)));
248              
249 8         20 $is_first_iteration = FALSE;
250              
251             }
252              
253             # If current comparator is ">" or >=" and next comparator is "<" or <="
254             # and the "tested version" is greater than the current version and the
255             # "tested version" is less than the next version then the "tested version"
256             # is IN the range. Check is finished.
257              
258 8 100 66 12   22 if ( (first { $current_constraint->comparator eq $_ } ('>', '>='))
  12 100 100     22  
      100        
      66        
259 14     14   55 && (first { $next_constraint->comparator eq $_ } ('<', '<='))
260             && ($version_class->new($version) > $version_class->new($current_constraint->version))
261             && ($version_class->new($version) < $version_class->new($next_constraint->version)))
262             {
263 3         21 return TRUE;
264             }
265              
266             # If current comparator is "<" or <=" and next comparator is ">" or >="
267             # then these versions are out the range. Continue to the next iteration.
268              
269 10     10   25 elsif ((first { $current_constraint->comparator eq $_ } ('<', '<='))
270 1     1   3 && (first { $next_constraint->comparator } ('>', '>=')))
271             {
272 1         4 next;
273             }
274              
275             }
276              
277             # If this is the last iteration and next comparator is ">" or >=" and the
278             # "tested version" is greater than the next version then the "tested version"
279             # is IN the range. Check is finished.
280              
281             return TRUE
282 5 50 66 10   19 if ((first { $next_constraint->comparator eq $_ } ('>', '>='))
  10         36  
283             && ($version_class->new($version) > $version_class->new($next_constraint->version)));
284              
285 5         30 return FALSE;
286              
287             }
288              
289             sub to_hash {
290 1     1 1 4 return {scheme => $_[0]->scheme, constraints => $_[0]->constraints};
291             }
292              
293 1     1 1 225 sub TO_JSON { shift->to_hash }
294              
295             sub _pairwise {
296              
297 8     8   14 my @out = ();
298              
299 8         24 for (my $i = 0; $i < scalar @_; $i++) {
300 16 100       53 push @out, [$_[$i], $_[$i + 1]] if $_[$i + 1];
301             }
302              
303 8         23 return @out;
304              
305             }
306              
307             1;
308              
309             __END__