File Coverage

blib/lib/Version/Semantic.pm
Criterion Covered Total %
statement 90 92 97.8
branch 52 54 96.3
condition 15 18 83.3
subroutine 24 24 100.0
pod 0 16 0.0
total 181 204 88.7


line stmt bran cond sub pod time code
1             # Prefer numeric version for backwards compatibility
2 6     6   708880 BEGIN { require 5.010000 }; ## no critic ( RequireUseStrict, RequireUseWarnings )
3 6     6   30 use strict;
  6         10  
  6         187  
4 6     6   35 use warnings;
  6         14  
  6         499  
5              
6             package Version::Semantic;
7              
8             $Version::Semantic::VERSION = 'v1.3.0';
9              
10 6     6   3479 use overload '<=>' => 'compare_to', '""' => 'to_string';
  6         10411  
  6         41  
11              
12 6     6   3710 use PerlX::Maybe ();
  6         18552  
  6         3829  
13              
14             sub _croakf ( $@ );
15              
16             my $prefix_re = qr/v/;
17              
18             #
19             my $id_re = qr/(?: [0-9] | [a-zA-Z-] )+/x;
20              
21             # (ok)
22             my $num_id_re = qr/0 | [1-9] | [1-9] [0-9]+/x;
23              
24             #
25             my $alnum_id_re = qr/[a-zA-Z-] | [a-zA-Z-] $id_re | $id_re [a-zA-Z-] | $id_re [a-zA-Z-] $id_re/x;
26              
27             #
28             #my $build_id_re = qr/[0-9a-zA-Z-]+/;
29             my $build_id_re = qr/$alnum_id_re | [0-9]+/x;
30             # (ok)
31             my $build_re = qr/$build_id_re (?: \. $build_id_re )*/x;
32              
33             #
34             #my $pre_release_id_re = qr/$num_id_re | [0-9]* [a-zA-Z-] [0-9a-zA-Z-]*/x;
35             my $pre_release_id_re = qr/$num_id_re | $alnum_id_re/x;
36             # (ok)
37             my $pre_release_re = qr/$pre_release_id_re (?: \. $pre_release_id_re )*/x;
38              
39             # Use BNF terminology
40             # https://semver.org/spec/v2.0.0.html#backusnaur-form-grammar-for-valid-semver-versions
41 9     9 0 48 sub prefix { shift->{ prefix } }
42 87     87 0 356 sub major { shift->{ major } }
43 77     77 0 279 sub minor { shift->{ minor } }
44 80     80 0 348 sub patch { shift->{ patch } }
45 20     20 0 87 sub version_core { shift->{ version_core } }
46 63     63 0 311 sub pre_release { shift->{ pre_release } }
47 4     4 0 20 sub build { shift->{ build } }
48              
49 4     4 0 52 sub has_prefix { defined shift->{ prefix } }
50 59     59 0 254 sub has_pre_release { defined shift->{ pre_release } }
51 13     13 0 94 sub has_build { defined shift->{ build } }
52              
53             {
54             ## no critic ( ProhibitComplexRegexes )
55             # On purpose use "build" (the BNF symbol name) instead of "buildmetadata" as
56             # the name of the last named capture group
57             # (ok)
58             my $semver_re = qr/
59             (? $prefix_re)?
60             (? $num_id_re) \. (? $num_id_re) \. (? $num_id_re)
61             (?: - (? $pre_release_re) )?
62             (?: \+ (? $build_re) )?
63             /x;
64 1     1 0 205890 sub semver_re { $semver_re }
65              
66             # Constructor as factory method
67             sub parse {
68 152     152 0 569036 my ( $class, $version ) = @_;
69 152   100     404 $version //= '';
70              
71 152 100       4664 $version =~ m/\A$semver_re\z/
72             or _croakf "Version '%s' is not a semantic version", $version;
73              
74 110         1422 $class->new( %+ )
75             }
76             }
77              
78             {
79             my %attrs = (
80             prefix => $prefix_re,
81             major => $num_id_re,
82             minor => $num_id_re,
83             patch => $num_id_re,
84             pre_release => $pre_release_re,
85             build => $build_re
86             );
87              
88             sub new {
89 124     124 0 129996 my $invocant = shift;
90 124         177 my %args;
91              
92             # Validate args
93             {
94 6     6   69 use warnings FATAL => qw( misc uninitialized );
  6         12  
  6         7833  
  124         178  
95 124         1011 %args = @_
96             };
97 122         380 foreach ( keys %args ) {
98 442 50       1054 unless ( defined $args{ $_ } ) {
99 0         0 delete $args{ $_ };
100             next
101 0         0 }
102             _croakf "Unknown attribute name '%s'", $_
103 442 100       811 unless exists $attrs{ $_ };
104             _croakf "Attribute '%s' has invalid value '%s'", $_, $args{ $_ }
105 441 100       26736 unless $args{ $_ } =~ m/\A $attrs{ $_ } \z/x
106             }
107              
108 120         263 my $class;
109 120 100       377 if ( $class = ref $invocant ) {
110             # Shallow copy
111 8         60 %args = ( %$invocant, %args )
112             } else {
113 112         216 $class = $invocant;
114             }
115              
116             # Don't move this attribute checking
117             exists $args{ $_ } or _croakf "Required attribute '%s' not set", $_
118 120   66     577 foreach qw( major minor patch );
119              
120             bless { %args,
121 119   100     630 version_core => ( $args{ prefix } // '' ) . join( '.', map { $args{ $_ } } qw( major minor patch ) ) } => $class
  357         1867  
122             }
123             }
124              
125             {
126             my $trial_pre_release = qr/\A ( TRIAL ) ( [0-9]* ) \z/x;
127              
128             sub increment {
129             # Obvious strategies are major|minor|patch
130 11     11 0 68 my ( $self, $strategy, $pre_release ) = @_;
131 11   100     31 $strategy //= 'patch';
132              
133 11 100       26 if ( $strategy eq 'trial' ) {
134 4 100       9 if ( $self->has_pre_release ) {
135 3 100       5 if ( my ( $string, $number ) = $self->pre_release =~ $trial_pre_release ) {
136 2 100       9 return $self->new( pre_release => $string . ( ( $number eq '' ? 0 : $number ) + 1 ) )
137             } else {
138 1         3 _croakf "Pre-release extension '%s' does not match '%s'", $self->pre_release, $trial_pre_release
139             }
140             } else {
141 1         4 _croakf "Cannot apply '%s' version incrementation strategy to non pre-release version '%s'", $strategy, $self
142             }
143             }
144 7 100       26 return $self->new( patch => $self->patch + 1, PerlX::Maybe::maybe pre_release => $pre_release )
145             if $strategy eq 'patch';
146 3 100       13 return $self->new( minor => $self->minor + 1, patch => 0, PerlX::Maybe::maybe pre_release => $pre_release )
147             if $strategy eq 'minor';
148 2 100       9 return $self->new( major => $self->major + 1, minor => 0, patch => 0, PerlX::Maybe::maybe pre_release => $pre_release )
149             if $strategy eq 'major';
150              
151 1         4 _croakf "Version incrementation strategy '%s' is not implemented", $strategy
152             }
153             }
154              
155             # https://semver.org/spec/v2.0.0.html#spec-item-11
156             sub compare_to {
157 33     33 0 1476 my ( $self, $other ) = @_;
158              
159             # 11.2
160 33         84 for ( qw( major minor patch ) ) {
161 88 100       335 return $self->$_ <=> $other->$_ if $self->$_ != $other->$_
162             }
163 20         56 $self->_compare_pre_release( $other )
164             }
165              
166             sub to_string {
167 8     8 0 21 my ( $self ) = @_;
168              
169 8         27 my $string = $self->version_core;
170 8 100       19 $string .= '-' . $self->pre_release if $self->has_pre_release;
171 8 100       18 $string .= '+' . $self->build if $self->has_build;
172 8         49 $string
173             }
174              
175             sub _compare_pre_release {
176 20     20   44 my ( $self, $other ) = @_;
177              
178             # Split pre-release into list of dot separated identifiers
179 20 100       46 my @a = $self->has_pre_release ? split /\./, $self->pre_release : ();
180 20 100       50 my @b = $other->has_pre_release ? split /\./, $other->pre_release : ();
181              
182             # 11.3
183 20 100       51 if ( @a ) {
184 18 100       52 return -1 if not @b
185             } else {
186 2 100       14 return ( @b ? 1 : 0 )
187             }
188              
189             # 11.4
190 16 100       45 my $len = @a < @b ? @a : @b;
191 16         48 for ( my $i = 0 ; $i < $len ; $i++ ) {
192 22         47 my $ai = $a[ $i ];
193 22         34 my $bi = $b[ $i ];
194              
195 22         253 my $ai_is_num = $ai =~ m/\A $num_id_re \z/x;
196 22         141 my $bi_is_num = $bi =~ m/\A $num_id_re \z/x;
197              
198             # 11.4.1
199 22 100 100     142 if ( $ai_is_num and $bi_is_num ) {
    100 66        
    100 66        
200 5         14 my $sign = $ai <=> $bi;
201 5 100       41 return $sign if $sign != 0
202             # 11.4.3
203             } elsif ( $ai_is_num and not $bi_is_num ) {
204 1         11 return -1
205             # 11.4.3
206             } elsif ( not $ai_is_num and $bi_is_num ) {
207 1         9 return 1
208             } else {
209 15         32 my $sign = $ai cmp $bi;
210 15 100       87 return $sign if $sign != 0
211             }
212             }
213              
214             # 11.4.4
215 5         45 @a <=> @b
216             }
217              
218             sub _croakf ( $@ ) {
219 48     48   297 require Carp;
220 48 50       277 @_ = ( ( @_ == 1 ? shift : sprintf shift, @_ ) . ', stopped' );
221 48         4049 goto &Carp::croak
222             }
223              
224             1