File Coverage

lib/Changes.pm
Criterion Covered Total %
statement 391 860 45.4
branch 135 750 18.0
condition 76 313 24.2
subroutine 49 69 71.0
pod 28 29 96.5
total 679 2021 33.6


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## Changes file management - ~/lib/Changes.pm
3             ## Version v0.2.2
4             ## Copyright(c) 2022 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2022/12/09
7             ## Modified 2023/01/30
8             ## All rights reserved
9             ##
10             ##
11             ## This program is free software; you can redistribute it and/or modify it
12             ## under the same terms as Perl itself.
13             ##----------------------------------------------------------------------------
14             package Changes;
15             BEGIN
16             {
17 18     18   345132831 use strict;
  18         191  
  18         615  
18 18     18   2086 use warnings;
  18         44  
  18         541  
19 18     18   97 use warnings::register;
  18         41  
  18         2071  
20 18     18   671 use parent qw( Module::Generic );
  18         392  
  18         119  
21 18     18   34570844 use vars qw( $VERSION $VERSION_LAX_REGEX $DATE_DISTZILA_RE $DATETIME_RE );
  18         41  
  18         1253  
22 18     18   8465 use Changes::Release;
  18         80  
  18         365  
23 18     18   8113 use Changes::Group;
  18         42  
  18         153  
24 18     18   15017 use Changes::Change;
  18         79  
  18         406  
25 18     18   7612 use Nice::Try;
  18         47  
  18         167  
26             # From version::regex
27 18     18   38349163 our $VERSION_LAX_REGEX = qr/(?^x: (?^x:
28             (?<has_v>v) (?<ver>(?^:[0-9]+) (?: (?^:\.[0-9]+)+ (?^:_[0-9]+)? )?)
29             |
30             (?<ver>(?^:[0-9]+)? (?^:\.[0-9]+){2,} (?^:_[0-9]+)?)
31             ) | (?^x: (?<ver>(?^:[0-9]+) (?: (?^:\.[0-9]+) | \. )? (?^:_[0-9]+)?)
32             |
33             (?<ver>(?^:\.[0-9]+) (?^:_[0-9]+)?)
34             )
35             )/;
36             # 2022-12-11 08:07:12 Asia/Tokyo
37 18         70 our $DATE_DISTZILA_RE = qr/
38             (?<r_year>\d{4})
39             -
40             (?<r_month>\d{1,2})
41             -
42             (?<r_day>\d{1,2})
43             (?<r_dt_space>[[:blank:]\h]+)
44             (?<r_hour>\d{1,2})
45             :
46             (?<r_minute>\d{1,2})
47             :
48             (?<r_second>\d{1,2})
49             (?<r_tz_space>[[:blank:]\h]+)
50             (?<r_tz>\S+)
51             /x;
52 18         701 our $VERSION = 'v0.2.2';
53             };
54              
55 18     18   266 use strict;
  18         32  
  18         609  
56 18     18   104 use warnings;
  18         36  
  18         63977  
57              
58             sub init
59             {
60 22     22 1 2244 my $self = shift( @_ );
61 22         1235 $self->{defaults} = undef;
62 22         99 $self->{elements} = [];
63 22         70 $self->{epilogue} = undef;
64 22         63 $self->{file} = undef;
65 22         74 $self->{max_width} = 0;
66 22         74 $self->{mode} = '+<';
67 22         65 $self->{nl} = "\n";
68 22         73 $self->{preamble} = undef;
69 22         63 $self->{releases} = [];
70 22         57 $self->{time_zone} = undef;
71 22         51 $self->{type} = undef;
72 22         68 $self->{wrapper} = undef;
73 22         66 $self->{_init_strict_use_sub} = 1;
74 22 50       182 $self->SUPER::init( @_ ) || return( $self->pass_error );
75 22         13653 return( $self );
76             }
77              
78             sub add_epilogue
79             {
80 1     1 1 19 my( $self, $text ) = @_;
81 1 50 33     10 if( !defined( $text ) || !length( "$text" ) )
82             {
83 0         0 return( $self->error( "No text was provided to add an epilogue." ) );
84             }
85 1         7 my $elements = $self->elements;
86 1         203 my $last = $elements->last;
87 1 50 33     67 if( defined( $last ) && !$self->_is_a( $last => 'Changes::NewLine' ) )
88             {
89 0   0     0 $elements->push( $self->new_line( nl => ( $self->nl // "\n" ) ) );
90             }
91 1         5 $self->epilogue( $text );
92 1         222 return( $self );
93             }
94              
95             sub add_preamble
96             {
97 1     1 1 2737 my( $self, $text ) = @_;
98 1 50 33     21 if( !defined( $text ) || !length( "$text" ) )
99             {
100 0         0 return( $self->error( "No text was provided to add a premable." ) );
101             }
102 1         17 $self->preamble( $text );
103 1 50       250 unless( $text =~ /[\015\012]$/ms )
104             {
105 1   50     13 $self->preamble->append( $self->nl // "\n" );
106             }
107 1 50       189 unless( $text =~ /[\015\012]{2,}$/ms )
108             {
109 1   50     4 $self->preamble->append( $self->nl // "\n" );
110             }
111 1         201 return( $self );
112             }
113              
114             sub add_release
115             {
116 4     4 1 596 my $self = shift( @_ );
117 4         7 my( $rel, $opts );
118 4         14 my $elements = $self->elements;
119 4 100 66     711 if( scalar( @_ ) == 1 && $self->_is_a( $_[0] => 'Changes::Release' ) )
120             {
121 2         90 $rel = shift( @_ );
122 2 50       99 if( $elements->exists( $rel ) )
123             {
124 0         0 return( $self->error( "A very same release object with version '", $rel->version, "' is already registered." ) );
125             }
126 2         94 my $vers = $rel->version;
127 2 50       212 if( length( "$vers" ) )
128             {
129 2 100   2   77 my $same = $elements->grep(sub{ $self->_is_a( $_ => 'Changes::Release' ) && $_->version == "$vers" });
  2         397  
130 2 50       215 return( $self->error( "A similar release with version '$vers' is already registered." ) ) if( !$same->is_empty );
131             }
132             }
133             else
134             {
135 2         16 $opts = $self->_get_args_as_hash( @_ );
136 2 50 33     320 if( exists( $opts->{version} ) && defined( $opts->{version} ) && length( "$opts->{version}" ) )
      33        
137             {
138 2         6 my $vers = $opts->{version};
139 2 100   2   18 my $same = $elements->grep(sub{ $self->_is_a( $_ => 'Changes::Release' ) && $_->version == "$vers" });
  2         484  
140 2 50       223 return( $self->error( "A similar release with version '$vers' is already registered." ) ) if( !$same->is_empty );
141             }
142 2   50     57 $rel = $self->new_release( %$opts ) || return( $self->pass_error );
143 2         18 return( $self->add_release( $rel ) );
144             }
145 2         64 $elements->unshift( $self->new_line );
146 2         21 $elements->unshift( $rel );
147 2         32 return( $rel );
148             }
149              
150             sub as_string
151             {
152 21     21 1 60929 my $self = shift( @_ );
153 21         162 my $lines = $self->new_array;
154 21         414 my $preamble = $self->preamble;
155 21         4867 my $epilogue = $self->epilogue;
156 21 100 66     4107 if( defined( $preamble ) && !$preamble->is_empty )
157             {
158 4         65 $lines->push( $preamble->scalar );
159             }
160            
161             $self->elements->foreach(sub
162             {
163 71     71   10924 $self->message( 4, "Calling as_string on $_" );
164 71         1427 my $str;
165 71 50       257 $str = $_->as_string if( $self->_can( $_ => 'as_string' ) );
166 71 50       779 if( defined( $str ) )
167             {
168 71         224 $lines->push( $str->scalar );
169             }
170 21         137 });
171 21 100 66     3370 if( defined( $epilogue ) && !$epilogue->is_empty )
172             {
173 2         23 $lines->push( $epilogue->scalar );
174             }
175 21         116 return( $lines->join( '' ) );
176             }
177              
178             {
179             *serialize = \&as_string;
180             *serialise = \&as_string;
181             }
182              
183 114     114 1 15085802 sub defaults { return( shift->_set_get_hash_as_mix_object( { field => 'defaults', undef_ok => 1 }, @_ ) ); }
184              
185             sub delete_release
186             {
187 0     0 1 0 my $self = shift( @_ );
188 0         0 my $elements = $self->elements;
189 0         0 my $removed = $self->new_array;
190 0         0 foreach my $rel ( @_ )
191             {
192 0 0       0 if( $self->_is_a( $rel => 'Changes::Release' ) )
193             {
194 0         0 my $pos = $elements->pos( $rel );
195 0 0       0 $self->message( 4, "No release object found for object $rel (", overload::StrVal( $rel ), ")" ) if( !defined( $pos ) );
196 0         0 my $until = 1;
197 0   0     0 while( defined( $elements->[ $pos + $until ] ) && $self->_is_a( $elements->[ $pos + $until ] => 'Changes::NewLine' ) )
198             {
199 0         0 $until++;
200             }
201 0         0 $elements->delete( $pos, $until );
202 0         0 $removed->push( $rel );
203             }
204             else
205             {
206 0         0 my $vers = $rel;
207 0 0 0     0 if( !defined( $vers ) || !length( "$vers" ) )
208             {
209 0 0       0 warn( "No version provided to remove its corresponding release object.\n" ) if( $self->_warnings_is_enabled );
210 0         0 next;
211             }
212 0 0   0   0 my $found = $elements->grep(sub{ $self->_is_a( $_ => 'Changes::Release' ) && $_->version == $vers });
  0         0  
213 0 0       0 if( $found->is_empty )
214             {
215 0         0 next;
216             }
217             $found->foreach(sub
218             {
219 0     0   0 my $deleted = $self->delete_release( $_ );
220 0 0       0 $removed->push( $deleted->list ) if( !$deleted->is_empty );
221 0         0 });
222             }
223             }
224 0         0 return( $removed );
225             }
226              
227 148     148 1 3102 sub elements { return( shift->_set_get_array_as_object( 'elements', @_ ) ); }
228              
229 24     24 1 8403 sub epilogue { return( shift->_set_get_scalar_as_object( 'epilogue', @_ ) ); }
230              
231 4     4 1 11189 sub file { return( shift->_set_get_file( 'file', @_ ) ); }
232              
233             sub freeze
234             {
235 20     20 0 55 my $self = shift( @_ );
236 20         82 $self->messagef( 5, "Freezing the state for all %d objects.", $self->elements->length );
237             $self->elements->foreach(sub
238             {
239 67 100   67   4883 if( $self->_can( $_ => 'freeze' ) )
240             {
241 44         816 $self->message( 5, "Calling freeze on $_" );
242 44         956 $_->freeze;
243             }
244 20         183223 });
245 20         522 return( $self );
246             }
247              
248 0     0 1 0 sub history { return( shift->releases( @_ ) ); }
249              
250             sub load
251             {
252 1     1 1 3664232 my $this = shift( @_ );
253 1   50     9 my $file = shift( @_ ) ||
254             return( $this->error( "No changes file was provided to load." ) );
255 1         26 my $opts = $this->_get_args_as_hash( @_ );
256 1   50     149 my $self = $this->new( %$opts ) ||
257             return( $this->pass_error );
258 1   50     21 my $f = $self->new_file( $file ) ||
259             return( $this->pass_error( $self->error ) );
260 1   50     50907 my $mode = $self->mode // '+<';
261 1 50       152 $f->open( "$mode", { binmode => 'utf-8', autoflush => 1 } ) ||
262             return( $this->pass_error( $f->error ) );
263             # my $lines = $f->lines( chomp => 1 ) ||
264 1   50     7017 my $lines = $f->lines ||
265             return( $this->pass_error( $f->error ) );
266 1 50       1844 $self->parse( $lines ) || return( $self->pass_error );
267 1         42 $self->freeze;
268 1         5 return( $self );
269             }
270              
271             sub load_data
272             {
273 19     19 1 33661 my $this = shift( @_ );
274 19         56 my $data = shift( @_ );
275 19         222 my $opts = $this->_get_args_as_hash( @_ );
276 19   50     142480 my $self = $this->new( %$opts ) ||
277             return( $this->pass_error );
278 19 50 33     321 return( $self ) if( !defined( $data ) || !length( "$data" ) );
279 19         428 my $lines = $self->new_array( [split( /(?<=\n)/, $data )] );
280             # $lines->chomp;
281 19 50       467 $self->parse( $lines ) || return( $self->pass_error );
282 19         676 $self->freeze;
283 19         87 return( $self );
284             }
285              
286 22     22 1 15290891 sub max_width { return( shift->_set_get_number( 'max_width', @_ ) ); }
287              
288             sub new_change
289             {
290 41     41 1 132 my $self = shift( @_ );
291 41         246 my $opts = $self->_get_args_as_hash( @_ );
292 41         6999 my $defaults = $self->defaults;
293 41 50       7811 if( defined( $defaults ) )
294             {
295 0         0 foreach my $opt ( qw( spacer1 marker spacer2 ) )
296             {
297 0 0 0     0 $opts->{ $opt } //= $defaults->{ $opt } if( defined( $defaults->{ $opt } ) );
298             }
299             }
300 41   50     368 my $c = Changes::Change->new( $opts ) ||
301             return( $self->pass_error( Changes::Change->error ) );
302 41         591 return( $c );
303             }
304              
305             sub new_group
306             {
307 6     6 1 18 my $self = shift( @_ );
308 6         37 my $opts = $self->_get_args_as_hash( @_ );
309 6         1046 my $defaults = $self->defaults;
310 6 50       1167 if( defined( $defaults ) )
311             {
312 0         0 my $def = { %$defaults };
313 0         0 foreach my $opt ( qw( spacer type ) )
314             {
315 0 0 0     0 if( !defined( $opts->{ "group_${opt}" } ) &&
      0        
      0        
316             exists( $def->{ "group_${opt}" } ) &&
317             defined( $def->{ "group_${opt}" } ) &&
318             length( $def->{ "group_${opt}" } ) )
319             {
320 0         0 $opts->{ $opt } = CORE::delete( $def->{ "group_${opt}" } );
321             }
322             }
323 0   0     0 $opts->{defaults} //= $def;
324             }
325 6   50     48 my $g = Changes::Group->new( $opts ) ||
326             return( $self->pass_error( Changes::Group->error ) );
327 6         60 return( $g );
328             }
329              
330             sub new_line
331             {
332 25     25 1 70 my $self = shift( @_ );
333 25 50       160 $self->_load_class( 'Changes::NewLine' ) || return( $self->pass_error );
334 25   50     4731 my $nl = Changes::NewLine->new( @_ ) ||
335             return( $self->pass_error( Changes::NewLine->error ) );
336 25         324 return( $nl );
337             }
338              
339             sub new_release
340             {
341 46     46 1 134 my $self = shift( @_ );
342 46         277 my $opts = $self->_get_args_as_hash( @_ );
343 46         8621 my $defaults = $self->defaults;
344 46 100       9249 if( defined( $defaults ) )
345             {
346 2         15 my $def = { %$defaults };
347 2         271 foreach my $opt ( qw( datetime_formatter format spacer time_zone ) )
348             {
349 8 50 66     55 if( !defined( $opts->{ $opt } ) &&
      66        
      66        
350             exists( $def->{ $opt } ) &&
351             defined( $def->{ $opt } ) &&
352             length( $def->{ $opt } ) )
353             {
354 4         13 $opts->{ $opt } = CORE::delete( $def->{ $opt } );
355             }
356             }
357 2   33     22 $opts->{defaults} //= $def;
358             }
359 46   50     573 my $rel = Changes::Release->new( $opts ) ||
360             return( $self->pass_error( Changes::Release->error ) );
361 46         847 return( $rel );
362             }
363              
364             sub new_version
365             {
366 0     0 1 0 my $self = shift( @_ );
367 0 0       0 $self->_load_class( 'Changes::Version' ) || return( $self->pass_error );
368 0   0     0 my $v = Changes::Version->new( @_ ) ||
369             return( $self->pass_error( Changes::Version->error ) );
370 0         0 return( $v );
371             }
372              
373 2     2 1 377 sub nl { return( shift->_set_get_scalar_as_object( 'nl', @_ ) ); }
374              
375             sub parse
376             {
377 20     20 1 64 my $self = shift( @_ );
378 20   50     574 my $lines = shift( @_ ) || return( $self->error( "No array reference of lines was provided." ) );
379 20 50       170 return( $self->error( "Data provided is not an array reference of lines." ) ) if( !$self->_is_array( $lines ) );
380 20         359 $lines = $self->new_array( $lines );
381 20         649 my $preamble = $self->new_scalar;
382 20         45883288 my $epilogue;
383 20         135 my $elements = $self->new_array;
384             # Temporary array buffer of new lines found that we store here until we read more of the context in the Changes file and we decide what to do with them.
385 20         453 my $nls = $self->new_array;
386 20   50     319 my $max_width = $self->max_width // 0;
387 20         2360642 my $debug = $self->debug;
388 20         571 my( $group, $release, $change );
389             # $type is the Changes file type. It contains the value guessed, otherwise it remains undef
390 20         147 my $type = $self->type;
391 20         4197 my $wrapper = $self->wrapper;
392 20         3786 my $tz = $self->time_zone;
393 20         139 my $defaults = $self->defaults;
394 20         3984 $self->messagef( 4, "Parsing %d lines of data.", $lines->length );
395             # Cache it
396 20 100       179659 unless( defined( $DATETIME_RE ) )
397             {
398 15         2272 $DATETIME_RE = $self->_get_datetime_regexp( 'all' );
399             }
400             # $self->message( 4, "Using datetime regular expression:\n$DATETIME_RE" );
401 20         25722657 for( my $i = 0; $i < scalar( @$lines ); $i++ )
402             {
403 127         22756 my $l = $lines->[$i];
404 127         1017 $self->message( 5, "Checking line '$l'" );
405             # DistZilla release line
406             # 0.01 2022-12-11 08:07:12 Asia/Tokyo
407 127 100 100     23066 if( $l =~ /^
    100 33        
    100 33        
    100 33        
    100          
    100          
    100          
    100          
    50          
408             [[:blank:]\h]*
409             (?<r_vers>$VERSION_LAX_REGEX)
410             (?<v_space>[[:blank:]\h][[:blank:]\h\W]*)
411             (?<r_datetime>$DATE_DISTZILA_RE)
412             [[:blank:]\h]*
413             (?<r_nl>[\015\012]+)?$
414             /msx )
415             {
416 2         139 my $re = { %+ };
417 2     0   36 $self->message( 4, "Found a release line of type DistZilla with regexp data: ", sub{ $self->dump( $re ) } );
  0         0  
418             # Create the DateTime object
419 2 50       56 $self->_load_class( 'DateTime' ) || return( $self->pass_error );
420 2 50       121 $self->_load_class( 'DateTime::TimeZone' ) || return( $self->pass_error );
421 2 50       60 $self->_load_class( 'DateTime::Format::Strptime' ) || return( $self->pass_error );
422 2         217057 my( $dt, $tz, $fmt );
423 2 50 33     10 try
  2         4  
  2         4  
  2         14  
  0         0  
  2         5  
  2         11  
  2         9  
424 2     2   7 {
425 2         21 $tz = DateTime::TimeZone->new( name => $re->{r_tz} );
426             }
427 2 0 50     18 catch( $e where { /The[[:blank:]\h]+timezone[[:blank:]\h]+'(?:.*?)'[[:blank:]\h]+could[[:blank:]\h]+not[[:blank:]\h]+be[[:blank:]\h]+loaded/i } )
  2 0 33     34487  
  2 0       11  
  2 0       5  
  2 0       3  
  2 0       5  
  2 0       4  
  2 0       14  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0         0  
  0         0  
  2         11  
  0         0  
  2         7  
  0         0  
  0         0  
  2         16  
  2         15  
  2         6  
  2         8  
  0         0  
  0         0  
  0         0  
  0         0  
428 0     0   0 {
429 0         0 $self->message( 5, "Error instantiating DateTime::TimeZone with time zone '$re->{r_tz}': $e" );
430 0 0       0 warn( "Warning only: invalid time zone '$re->{r_tz}' specified in release at line ", ( $i + 1 ), "\n" ) if( $self->_warnings_is_enabled );
431 0         0 $tz = DateTime::TimeZone->new( name => 'UTC' );
432             }
433 0 0 0     0 catch( $e )
  0 0 0     0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
434 0     0   0 {
435 0 0       0 warn( "Warning only: error trying to instantiate a new DateTime::TimeZone object with time zone '$re->{r_tz}': $e\n" ) if( $self->_warnings_is_enabled );
436 0         0 $tz = DateTime::TimeZone->new( name => 'UTC' );
437 18 0 0 18   161 }
  18 0 0     35  
  18 0 33     21822  
  0 0 33     0  
  0 0 33     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  2 0       6  
  0 0       0  
  2 0       66  
  0 0       0  
  0 0       0  
  0 50       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 50       0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  2         13  
  0         0  
  0         0  
  0         0  
  0         0  
  2         12  
438            
439 2         18 $self->message( 5, "Time zone set to '", $tz->name, "'" );
440            
441 2 50 33     222 try
  2         4  
  2         5  
  2         12  
  0         0  
  2         4  
  2         6  
  2         5  
442 2     2   4 {
443 2         29 $fmt = DateTime::Format::Strptime->new(
444             pattern => "%F$re->{r_dt_space}%T$re->{r_tz_space}%O",
445             );
446             }
447 2 0 50     15 catch( $e )
  2 0 33     3931  
  2 0       11  
  2 0       4  
  2 0       4  
  2 0       5  
  2 0       4  
  2 0       9  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 0       0  
  0 50       0  
  0         0  
  0         0  
  0         0  
  2         8  
  0         0  
  2         7  
  0         0  
  0         0  
  2         7  
  2         12  
  2         7  
  2         8  
  0         0  
  0         0  
  0         0  
  0         0  
448 0     0   0 {
449 0 0       0 warn( "Error only: failed to create a DateTime::Format::Strptime with pattern '%F$re->{r_dt_space}%T$re->{r_tz_space}%Z': $e\n" ) if( $self->_warnings_is_enabled );
450 0         0 $fmt = DateTime::Format::Strptime->new(
451             pattern => "%F %T %O",
452             );
453 18 0 0 18   152 }
  18 0 0     36  
  18 0 33     21456  
  0 0 33     0  
  0 0 33     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  2 0       9  
  0 0       0  
  2 0       142  
  0 0       0  
  0 0       0  
  0 50       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 50       0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  2         10  
  0         0  
  0         0  
  0         0  
  0         0  
  2         13  
454            
455 2 50 33     6 try
  2         7  
  2         4  
  2         11  
  0         0  
  2         3  
  2         7  
  2         5  
456 2     2   4 {
457             $dt = DateTime->new(
458             year => $re->{r_year},
459             month => $re->{r_month},
460             day => $re->{r_day},
461             hour => $re->{r_hour},
462             minute => $re->{r_minute},
463             second => $re->{r_second},
464 2         24 time_zone => $tz,
465             );
466 2         1834 $dt->set_formatter( $fmt );
467             }
468 2 0 50     16 catch( $e )
  2 0 33     172  
  2 0       8  
  2 0       5  
  2 0       4  
  2 0       5  
  2 0       4  
  2 0       10  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 0       0  
  0 50       0  
  0         0  
  0         0  
  0         0  
  2         11  
  0         0  
  2         18  
  0         0  
  0         0  
  2         10  
  2         12  
  2         8  
  2         6  
  0         0  
  0         0  
  0         0  
  0         0  
469 0     0   0 {
470 0 0       0 warn( "Warning only: error trying to instantiate a DateTime value based on the date and time of the release at line ", ( $i + 1 ), ": $e\n" ) if( $self->_warnings_is_enabled );
471 0         0 $dt = DateTime->now( time_zone => $tz );
472 18 0 0 18   162 }
  18 0 0     37  
  18 0 33     67212  
  0 0 33     0  
  0 0 33     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  2 0       7  
  0 0       0  
  2 0       50  
  0 0       0  
  0 0       0  
  0 50       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 50       0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  2         11  
  0         0  
  0         0  
  0         0  
  0         0  
  2         7  
473 2         15 $self->message( 5, "DateTime object time zone set to '", $dt->time_zone->name, "'" );
474            
475 2 100       91 if( !$nls->is_empty )
476             {
477 1         28 $elements->push( $nls->list );
478 1         17 $nls->reset;
479             }
480 2         70 undef( $group );
481             $release = $self->new_release(
482             version => $re->{r_vers},
483             datetime => $dt,
484             spacer => $re->{v_space},
485             ( defined( $re->{r_note} ) ? ( note => $re->{r_note} ) : () ),
486             raw => $l,
487             line => ( $i + 1 ),
488             container => $self,
489             # Could be undef if this is the last line with no trailing crlf
490             nl => $re->{r_nl},
491 2 50       31 ( defined( $tz ) ? ( time_zone => $tz ) : () ),
    50          
    50          
492             ( defined( $defaults ) ? ( defaults => $defaults ) : () ),
493             debug => $debug,
494             );
495 2         18 $elements->push( $release );
496 2 50 33     34 if( defined( $preamble ) && !$preamble->is_empty )
497             {
498 0         0 $self->preamble( $preamble );
499 0         0 undef( $preamble );
500             }
501 2 50       40 unless( defined( $type ) )
502             {
503 2         5 $type = 'distzilla';
504 2         11 $self->type( $type );
505             }
506             }
507             # Release line
508             # v0.1.0 2022-11-17T08:12:31+0900
509             # 0.01 - 2022-11-17
510             elsif( $l =~ /^
511             [[:blank:]\h]*
512             (?<r_vers>$VERSION_LAX_REGEX)
513             (?<v_space>[[:blank:]\h][[:blank:]\h\W]*)
514             (?<r_date>$DATETIME_RE)
515             (?:
516             (?<d_space>[[:blank:]\h]+)
517             (?<r_note>.+?))?(?<r_nl>[\015\012]+)?
518             $/msx )
519             {
520 34         9471 my $re = { %+ };
521 34     0   564 $self->message( 4, "Found a release line with regexp data: ", sub{ $self->dump( $re ) } );
  0         0  
522 34   50     1942 my $dt = $self->_parse_timestamp( $re->{r_date} ) ||
523             return( $self->pass_error( "Cannot parse datetime timestamp although the regular expression matched: ", $self->error->message ) );
524 34 100       33335037 if( !$nls->is_empty )
525             {
526 16         370 $elements->push( $nls->list );
527 16         271 $nls->reset;
528             }
529 34         889 undef( $group );
530 34         187 $self->message( 5, "Datetime object from parsing is '", overload::StrVal( $dt ), " ($dt)" );
531             $release = $self->new_release(
532             version => $re->{r_vers},
533             # datetime => $re->{r_date},
534             datetime => $dt,
535             spacer => $re->{v_space},
536             ( defined( $re->{r_note} ) ? ( note => $re->{r_note} ) : () ),
537             raw => $l,
538             line => ( $i + 1 ),
539             container => $self,
540             # Could be undef if this is the last line with no trailing crlf
541             nl => $re->{r_nl},
542 34 100       16182 ( defined( $tz ) ? ( time_zone => $tz ) : () ),
    50          
    50          
543             ( defined( $defaults ) ? ( defaults => $defaults ) : () ),
544             debug => $debug,
545             );
546 34         278 $elements->push( $release );
547 34 100 100     577 if( defined( $preamble ) && !$preamble->is_empty )
548             {
549 3         39 $self->preamble( $preamble );
550 3         1594 undef( $preamble );
551             }
552             }
553             elsif( $l =~ /^
554             [[:blank:]\h]*
555             (?<r_vers>$VERSION_LAX_REGEX)
556             (?:
557             (?<v_space>[[:blank:]\h][[:blank:]\h\W]*)
558             (?<r_note>[^\015\012]*)
559             )?
560             (?<r_nl>[\015\012]+)?
561             /msx )
562             {
563 8         1820 my $re = { %+ };
564 8     0   106 $self->message( 4, "Found a release line with no datetime and regexp data: ", sub{ $self->dump( $re ) } );
  0         0  
565 8 100       258 if( !$nls->is_empty )
566             {
567 5         113 $elements->push( $nls->list );
568 5         68 $nls->reset;
569             }
570 8         202 undef( $group );
571             $release = $self->new_release(
572             version => $re->{r_vers},
573             spacer => $re->{v_space},
574             ( defined( $re->{r_note} ) ? ( note => $re->{r_note} ) : () ),
575             raw => $l,
576             line => ( $i + 1 ),
577             container => $self,
578             # Could be undef if this is the last line with no trailing crlf
579             nl => $re->{r_nl},
580 8 100       109 ( defined( $tz ) ? ( time_zone => $tz ) : () ),
    50          
    50          
581             ( defined( $defaults ) ? ( defaults => $defaults ) : () ),
582             debug => $debug,
583             );
584 8         60 $elements->push( $release );
585 8 50 33     136 if( defined( $preamble ) && !$preamble->is_empty )
586             {
587 0         0 $self->preamble( $preamble );
588 0         0 undef( $preamble );
589             }
590             }
591             # Group line
592             elsif( $l =~ /^(?<g_space>[[:blank:]\h]+)(?<data>(?:\[(?<g_name>[^\]]+)\]|(?<g_name_colon>\w[^\:]+)\:))[[:blank:]\h]*(?<g_nl>[\015\012]+)?$/ms )
593             {
594 6         159 my $re = { %+ };
595 6     0   81 $self->message( 4, "Found a group line with regexp data: ", sub{ $self->dump( $re ) } );
  0         0  
596             # Depending on where we are we treat this either as a group, or as a mere comment of a release change
597             # 1) This is a continuity of the previous change line
598             # We assert this by checking if the space before is longer than the prefix of the change, which would imply an indentation that would put it below the change, and thus not a group
599 6 50 50     204 if( defined( $change ) && length( $re->{g_space} // '' ) > $change->prefix->length )
      66        
600             {
601 0   0     0 $self->messagef( 4, "Group text has a leading space length (%d) longer than the previous change prefix (%d)", length( $re->{g_space} // '' ), $change->prefix->length );
602 0         0 $change->text->append( $re->{data} );
603             # Since this is a wrapped line, we remove any excessive leading spaces and replace them by just one space
604 0         0 $l =~ s/^[[:blank:]\h]+/ /g;
605 0         0 $change->raw->push( $l );
606             }
607             else
608             {
609 6   33     28894 $self->message( 4, "Creating a new group object with name '", ( $re->{g_name} // $re->{g_name_colon} ), "'" );
610             # A group is above a change, so if we already have an ongoing change object, we stop using it
611 6         131 undef( $change );
612             $group = $self->new_group(
613             name => ( $re->{g_name} // $re->{g_name_colon} ),
614             spacer => $re->{g_space},
615             raw => $l,
616             line => ( $i + 1 ),
617             type => ( defined( $re->{g_name_colon} ) ? 'colon' : 'bracket' ),
618             # Could be undef if this is the last line with no trailing crlf
619             nl => $re->{g_nl},
620 6 50 33     76 ( defined( $defaults ) ? ( defaults => $defaults ) : () ),
    50          
621             debug => $debug,
622             );
623 6 50       19 if( !defined( $release ) )
624             {
625 0 0       0 warn( "Found a group token outside of a release information at line ", ( $i + 1 ), "\n" ) if( $self->_warnings_is_enabled );
626 0 0       0 if( !$nls->is_empty )
627             {
628 0         0 $elements->push( $nls->list );
629 0         0 $nls->reset;
630             }
631 0         0 $elements->push( $group );
632             }
633             else
634             {
635 6 50       36 if( !$nls->is_empty )
636             {
637 0         0 $release->elements->push( $nls->list );
638 0         0 $nls->reset;
639             }
640 6         147 $release->elements->push( $group );
641             }
642             }
643             }
644             # Change line
645             elsif( defined( $release ) &&
646             $l =~ /^(?<c_space1>[[:blank:]\h]*)(?<marker>(?:[^\w[:blank:]\h]|[\_\x{30FC}]))(?<c_space2>[[:blank:]\h]+)(?<c_text>.+?)(?<c_nl>[\015\012]+)?$/ms )
647             {
648 41         2559 my $re = { %+ };
649 41     0   570 $self->message( 4, "Found a change line with regexp data: ", sub{ $self->dump( $re ) } );
  0         0  
650             $change = $self->new_change(
651             ( defined( $re->{c_space1} ) ? ( spacer1 => $re->{c_space1} ) : () ),
652             ( defined( $re->{c_space2} ) ? ( spacer2 => $re->{c_space2} ) : () ),
653             marker => $re->{marker},
654             max_width => $max_width,
655             ( defined( $re->{c_text} ) ? ( text => $re->{c_text} ) : () ),
656             # Could be undef if this is the last line with no trailing crlf
657             nl => $re->{c_nl},
658             # raw => "$l\n",
659 41   50     1673 raw => $l,
660             ( defined( $wrapper ) ? ( wrapper => $wrapper ) : () ),
661             line => ( $i + 1 ),
662             debug => $debug,
663             ) || return( $self->pass_error );
664            
665 41 100       247 if( defined( $group ) )
    50          
666             {
667 7 50       39 if( !$nls->is_empty )
668             {
669 0         0 $group->elements->push( $nls->list );
670 0         0 $nls->reset;
671             }
672 7         169 $self->message( 5, "Adding change object '$change' to group" );
673 7         187 $group->elements->push( $change );
674             }
675             elsif( defined( $release ) )
676             {
677 34 50       238 if( !$nls->is_empty )
678             {
679 0         0 $release->elements->push( $nls->list );
680 0         0 $nls->reset;
681             }
682 34         933 $self->message( 5, "Adding change object '$change' directly to release" );
683 34         1054 $release->elements->push( $change );
684             }
685             else
686             {
687 0 0       0 warn( "Found a change token outside of a release information at line ", ( $i + 1 ), "\n" ) if( $self->_warnings_is_enabled );
688 0 0       0 if( !$nls->is_empty )
689             {
690 0         0 $elements->push( $nls->list );
691 0         0 $nls->reset;
692             }
693 0         0 $self->message( 5, "Adding change object '$change' directly to top Changes object" );
694 0         0 $elements->push( $change );
695             }
696             }
697             # Some previous line continuity
698             elsif( $l =~ /^(?<space>[[:blank:]\h]+)(?<data>\S+.*?)(?<c_nl>[\015\012]+)?$/ms )
699             {
700 4         90 my $re = { %+ };
701 4     0   47 $self->message( 4, "Found the continuity of a change wrapped line with regexp data: ", sub{ $self->dump( $re ) } );
  0         0  
702             # We have an ongoing change, so this is likely a wrapped line. We append the text
703 4 50       101 if( defined( $change ) )
704             {
705 4         31 $self->message( 4, "Appending '$l' to previous change object." );
706 4   33     78 $change->text->append( ( $change->nl // $self->nl ) . ( $re->{space} . $re->{data} ) );
707             # Which might be undef if, for example, this is the last line and there is no trailing crlf
708 4         1970 $change->nl( $re->{c_nl} );
709 4         1899 $change->raw->append( $l );
710             }
711             # Ok, then some weirdly formatted change text
712             else
713             {
714 0         0 $self->message( 4, "Treating this line as a new change line, albeit with some non-standard formatting." );
715             $change = $self->new_change(
716             ( defined( $re->{c_space1} ) ? ( spacer1 => $re->{c_space1} ) : () ),
717             ( defined( $re->{c_space2} ) ? ( spacer2 => $re->{c_space2} ) : () ),
718             marker => $re->{marker},
719             max_width => $max_width,
720             ( defined( $re->{c_text} ) ? ( text => $re->{c_text} ) : () ),
721             nl => $re->{c_nl},
722             # raw => "$l\n",
723 0   0     0 raw => $l,
724             line => ( $i + 1 ),
725             debug => $debug,
726             ) || return( $self->pass_error );
727 0 0       0 if( defined( $group ) )
    0          
728             {
729 0 0       0 if( !$nls->is_empty )
730             {
731 0         0 $group->elements->push( $nls->list );
732 0         0 $nls->reset;
733             }
734 0         0 $group->elements->push( $change );
735             }
736             elsif( defined( $release ) )
737             {
738 0 0       0 if( !$nls->is_empty )
739             {
740 0         0 $release->elements->push( $nls->list );
741 0         0 $nls->reset;
742             }
743 0         0 $release->elements->push( $change );
744             }
745             }
746             }
747             # Blank line
748             elsif( $l =~ /^(?<space>[[:blank:]\h]*)(?<nl>[\015\012]+)?$/ )
749             {
750 27         566 my $re = { %+ };
751 27     0   309 $self->message( 4, "Found a blank line with regexp data: ", sub{ $self->dump( $re ) } );
  0         0  
752             # If we are still in the preamble, this might just be a multi lines preamble
753 27 100       732 if( $elements->is_empty )
    50          
754             {
755 4         52 $self->message( 4, "Adding the blank line as part of the preamble." );
756             # $preamble->append( "$l\n" );
757 4         58 $preamble->append( $l );
758             }
759             # Otherwise, this is a blank line, which separates elements
760             elsif( defined( $release ) )
761             {
762 23         493 $self->message( 4, "Resetting change and group object, and adding a new line to the latest release list of changes." );
763 23         388 undef( $change );
764 23         52 undef( $group );
765             # We do not undef the latest release object, because we could have blank lines inside a release section
766             # $release->changes->push( $self->new_line );
767             $nls->push( $self->new_line(
768             line => ( $i + 1 ),
769             (
770             ( defined( $re->{nl} ) && defined( $re->{space} ) )
771 23 50 33     396 ? ( nl => ( $re->{space} // '' ) . ( $re->{nl} // '' ) )
      50        
      50        
772             : ( nl => undef )
773             ),
774             raw => $l,
775             debug => $debug
776             ));
777             }
778             else
779             {
780 0 0       0 warn( "I found an empty line outside a release and no release object to associate it to.\n" ) if( $self->_warnings_is_enabled );
781             # $releases->push( $self->new_line );
782 0         0 $nls->push( $self->new_line( raw => $l, debug => $debug ) );
783             }
784             }
785             # Preamble
786             elsif( $elements->is_empty )
787             {
788 4         749 $self->message( 4, "Adding line as part of the preamble." );
789 4         82 $preamble->append( $l );
790             }
791             # Epilogue
792             # We found a line with no leading space with new blank lines before it and no epilogue yet, or maybe no blank lines, but with epilogue already set.
793             elsif( $l =~ /^(\S+.*?)(?<nl>[\015\012]+)?$/ms &&
794             (
795             ( !$nls->is_empty && !defined( $epilogue ) ) ||
796             ( defined( $epilogue ) && !defined( $release ) && !defined( $group ) && !defined( $change ) )
797             ) &&
798             # If elements are empty this would rather be part of the preamble
799             !$elements->is_empty )
800             {
801 1         64 my $re = { %+ };
802 1     0   10 $self->message( 4, "Found an epilogue line with regexp data: ", sub{ $self->dump( $re ) } );
  0         0  
803 1 50       22 if( !$nls->is_empty )
804             {
805 1         12 $self->message( 4, "This is our first epilogue line." );
806 1         15 $elements->push( $nls->list );
807 1         12 $nls->reset;
808 1         8 undef( $release );
809 1         2 undef( $change );
810 1         1 undef( $group );
811 1         5 $epilogue = $self->new_scalar( $l );
812 1         25 $self->epilogue( $epilogue );
813             }
814             else
815             {
816 0         0 $self->message( 4, "This is a follow-on epilogue line." );
817 0         0 $epilogue->append( $l );
818             }
819             }
820             else
821             {
822 0         0 chomp( $l );
823 0 0       0 warn( "Found an unrecognisable line: '$l'\n" ) if( $self->_warnings_is_enabled );
824             }
825             }
826 20         9451 $self->elements( $elements );
827 20         4096 return( $self );
828             }
829              
830 47     47 1 25499 sub preamble { return( shift->_set_get_scalar_as_object( 'preamble', @_ ) ); }
831              
832             sub releases
833             {
834 62     62 1 281108 my $self = shift( @_ );
835 62     405   285 my $a = $self->elements->grep(sub{ $self->_is_a( $_ => 'Changes::Release' ) });
  405         19674  
836 62         6655 return( $a );
837             }
838              
839 0     0 1 0 sub remove_release { return( shift->delete_release( @_ ) ); }
840              
841             sub time_zone
842             {
843 20     20 1 64 my $self = shift( @_ );
844 20 50       105 if( @_ )
845             {
846 0         0 my $v = shift( @_ );
847 0 0       0 if( $self->_is_a( $v => 'DateTime::TimeZone' ) )
848             {
849 0         0 $self->{time_zone} = $v;
850             }
851             else
852             {
853 0 0 0     0 try
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
854 0     0   0 {
855 0 0       0 $self->_load_class( 'DateTime::TimeZone' ) || return( $self->pass_error );
856 0         0 my $tz = DateTime::TimeZone->new( name => "$v" );
857 0         0 $self->{time_zone} = $tz;
858             }
859 0 0 0     0 catch( $e )
  0 0 0     0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
860 0     0   0 {
861 0         0 return( $self->error( "Error setting time zone for '$v': $e" ) );
862 18 0 0 18   157 }
  18 0 0     34  
  18 0 0     7726  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
863             }
864 0         0 $self->reset(1);
865             }
866 20 50       107 if( !defined( $self->{time_zone} ) )
867             {
868 20 50       83 if( Want::want( 'OBJECT' ) )
869             {
870 0         0 require Module::Generic::Null;
871 0         0 rreturn( Module::Generic::Null->new( wants => 'OBJECT' ) );
872             }
873             else
874             {
875 20         933 return;
876             }
877             }
878             else
879             {
880 0         0 return( $self->{time_zone} );
881             }
882             }
883              
884 23     23 1 720 sub type { return( shift->_set_get_scalar_as_object( 'type', @_ ) ); }
885              
886 20     20 1 204 sub wrapper { return( shift->_set_get_code( 'wrapper', @_ ) ); }
887              
888             sub write
889             {
890 1     1 1 1533 my $self = shift( @_ );
891 1   50     7 my $f = $self->file ||
892             return( $self->error( "No Changes file has been set to write to." ) );
893 1         250 my $str = $self->as_string;
894 1 50       40 return( $self->pass_error ) if( !defined( $str ) );
895 1 50       4 if( $str->is_empty )
896             {
897 0 0       0 warn( "Warning only: nothing to write to change file $f\n" ) if( $self->_warnings_is_enabled );
898 0         0 return( $self );
899             }
900 1   50     24 my $fh = $f->open( '>', { binmode => 'utf-8', autoflush => 1 } ) ||
901             return( $self->pass_error( $f->error ) );
902 1 50       16345 $fh->print( $str->scalar ) || return( $self->pass_error( $fh->error ) );
903 1         318 $fh->close;
904 1         196 return( $self );
905             }
906              
907             1;
908             # NOTE: POD
909             __END__
910              
911             =encoding utf-8
912              
913             =head1 NAME
914              
915             Changes - Changes file management
916              
917             =head1 SYMOPSIS
918              
919             use Changes;
920             my $c = Changes->load( '/some/where/Changes',
921             {
922             file => '/some/where/else/CHANGES',
923             max_width => 78,
924             type => 'cpan',
925             debug => 4,
926             }) || die( Changes->error );
927             say "Found ", $c->releases->length, " releases.";
928             my $rel = $c->add_release(
929             version => 'v0.1.1',
930             # Accepts relative time
931             datetime => '+1D',
932             note => 'CPAN update',
933             ) || die( $c->error );
934             $rel->changes->push( $c->new_change(
935             text => 'Minor corrections in unit tests',
936             ) ) || die( $rel->error );
937             # or
938             my $change = $rel->add_change( text => 'Minor corrections in unit tests' );
939             $rel->delete_change( $change );
940             my $array_object = $c->delete_release( $rel ) ||
941             die( $c->error );
942             say sprintf( "%d releases removed.", $array_object->length );
943             # or $c->remove_release( $rel );
944             # Writing to /some/where/else/CHANGES even though we read from /some/where/Changes
945             $c->write || die( $c->error );
946              
947             =head1 VERSION
948              
949             v0.2.2
950              
951             =head1 DESCRIPTION
952              
953             This module is designed to read and update C<Changes> files that are provided as part of change management in software distribution.
954              
955             It is not limited to CPAN, and is versatile and flexible giving you a lot of control.
956              
957             Its distinctive value compared to other modules that handle C<Changes> file is that it does not attempt to reformat release and change information if they have not been modified. This ensure not just speed, but also that existing formatting of C<Changes> file remain unchanged. You can force reformatting of any release section by calling L<Changes::Release/reset>
958              
959             This module does not L<perlfunc/die> upon error, but instead returns an L<error object|Module::Generic/error>, so you need to check for the return value when you call any methods in this package distribution.
960              
961             =head1 CONSTRUCTOR
962              
963             =head2 new
964              
965             Provided with an optional hash or hash reference of properties-values pairs, and this will instantiate a new L<Changes> object and return it.
966              
967             Supported properties are the same as the methods listed below.
968              
969             If an error occurs, this will return an L<error|Module::Generic/error>
970              
971             =head2 load
972              
973             Provided with a file path, and an optional hash or hash reference of parameters, and this will parse the C<Changes> file and return a new object. Thus, this method can be called either using an existing object, or as a class function:
974              
975             my $c2 = $c->load( '/some/where/Changes' ) ||
976             die( $c->error );
977             # or
978             my $c = Changes->load( '/some/where/Changes' ) ||
979             die( Changes->error );
980              
981             =head2 load_data
982              
983             Provided with some string and an optional hash or hash reference of parameters and this will parse the C<Changes> file data and return a new object. Thus, this method can be called either using an existing object, or as a class function:
984              
985             my $c2 = $c->load_data( $changes_data ) ||
986             die( $c->error );
987             # or
988             my $c = Change->load_data( $changes_data ) ||
989             die( Changes->error );
990              
991             =head1 METHODS
992              
993             =head2 add_epilogue
994              
995             Provided with a text and this will set it as the Changes file epilogue, i.e. an optional text that will appear at the end of the Changes file.
996              
997             If the last element is not a blank line to separate the epilogue from the last release information, then it will be added as necessary.
998              
999             It returns the current object upon success, or an L<error|Module::Generic/error> upon error.
1000              
1001             =head2 add_preamble
1002              
1003             Provided with a text and this will set it as the Changes file preamble.
1004              
1005             If the text does not have 2 blank new lines at the end, those will be added in order to separate the preamble from the first release line.
1006              
1007             It returns the current object upon success, or an L<error|Module::Generic/error> upon error.
1008              
1009             =head2 add_release
1010              
1011             This takes either an L<Changes::Release> or an hash or hash reference of options required to create one (for that refer to the L<Changes::Release> class), and returns the newly added release object.
1012              
1013             The new release object will be added on top of the elements stack with a blank new line separating it from the other releases.
1014              
1015             If the same object is found, or an object with the same version number is found, an error is returned, otherwise it returns the release object thus added.
1016              
1017             =head2 as_string
1018              
1019             Returns a L<string object|Module::Generic::Scalar> representing the entire C<Changes> file. It does so by getting the value set with L<preamble>, and by calling C<as_string> on each element stored in L</elements>. Those elements can be L<Changes::Release> and L<Changes::Group> and possibly L<Changes::Change> object.
1020              
1021             If an error occurred, it returns an L<error|Module::Generic/error>
1022              
1023             The result of this method is cached so that the second time it is called, the cache is used unless there has been any change.
1024              
1025             =head2 defaults
1026              
1027             Sets or gets an hash of default values for the L<Changes::Release> or L<Changes::Change> object when it is instantiated upon parsing with L</parse> or by the C<new_release> or C<new_change> method found in L<Changes>, L<Changes::Release> and L<Changes::Group>
1028              
1029             Default is C<undef>, which means no default value is set.
1030              
1031             my $ch = Changes->new(
1032             file => '/some/where/Changes',
1033             defaults => {
1034             # for Changes::Release
1035             datetime_formatter => sub
1036             {
1037             my $dt = shift( @_ ) || DateTime->now;
1038             require DateTime::Format::Strptime;
1039             my $fmt = DateTime::Format::Strptime->new(
1040             pattern => '%FT%T%z',
1041             locale => 'en_GB',
1042             );
1043             $dt->set_formatter( $fmt );
1044             $dt->set_time_zone( 'Asia/Tokyo' );
1045             return( $dt );
1046             },
1047             # No need to provide it if it is just a space though, because it will default to it anyway
1048             spacer => ' ',
1049             # Not necessary if the custom datetime formatter has already set it
1050             time_zone => 'Asia/Tokyo',
1051             # for Changes::Change
1052             spacer1 => "\t",
1053             spacer2 => ' ',
1054             marker => '-',
1055             max_width => 72,
1056             wrapper => $code_reference,
1057             # for Changes::Group
1058             group_spacer => "\t",
1059             group_type => 'bracket', # [Some group]
1060             }
1061             );
1062              
1063             =head2 delete_release
1064              
1065             This takes a list of release to remove and returns an L<array object|Module::Generic::Array> of those releases thus removed.
1066              
1067             A release provided can either be a L<Changes::Release> object, or a version string.
1068              
1069             When removing a release object, it will also take care of removing following blank new lines that typically separate a release from the rest.
1070              
1071             If an error occurred, this will return an L<error|Module::Generic/error>
1072              
1073             =head2 elements
1074              
1075             Sets or gets an L<array object|Module::Generic::Array> of all the elements within the C<Changes> file. Those elements can be L<Changes::Release>, L<Changes::Group>, L<Changes::Change> and C<Changes::NewLine> objects.
1076              
1077             =head2 epilogue
1078              
1079             Sets or gets the text of the epilogue. An epilogue is a chunk of text, possibly multi line, that appears at the bottom of the Changes file after the last release information, separated by a blank line.
1080              
1081             =head2 file
1082              
1083             my $file = $c->file;
1084             $c->file( '/some/where/Changes' );
1085              
1086             Sets or gets the file path of the Changes file. This returns a L<file object|Module::Generic::File>
1087              
1088             =for Pod::Coverage freeze
1089              
1090             =head2 history
1091              
1092             This is an alias for L</releases> and returns an L<array object|Module::Generic::Array> of L<Changes::Release> objects.
1093              
1094             =head2 max_width
1095              
1096             Sets or gets the maximum line width for a change inside a release. The line width includes an spaces at the beginning of the line and not just the text of the change itself.
1097              
1098             For example:
1099              
1100             v0.1.0 2022-11-17T08:12:42+0900
1101             - Some very long line of change going here, which can be wrapped here at 78 characters
1102              
1103             wrapped at 78 characters would become:
1104              
1105             v0.1.0 2022-11-17T08:12:42+0900
1106             - Some very long line of change going here, which can be wrapped here at
1107             78 characters
1108              
1109             =head2 new_change
1110              
1111             Returns a new L<Changes::Change> object, passing it any parameters provided.
1112              
1113             If an error occurred, it returns an L<error object|Module::Generic/error>
1114              
1115             =head2 new_group
1116              
1117             Returns a new L<Changes::Group> object, passing it any parameters provided.
1118              
1119             If an error occurred, it returns an L<error object|Module::Generic/error>
1120              
1121             =head2 new_line
1122              
1123             Returns a new C<Changes::NewLine> object, passing it any parameters provided.
1124              
1125             If an error occurred, it returns an L<error object|Module::Generic/error>
1126              
1127             =head2 new_release
1128              
1129             Returns a new L<Changes::Release> object, passing it any parameters provided.
1130              
1131             If an error occurred, it returns an L<error object|Module::Generic/error>
1132              
1133             =head2 new_version
1134              
1135             Returns a new C<Changes::Version> object, passing it any parameters provided.
1136              
1137             If an error occurred, it returns an L<error object|Module::Generic/error>
1138              
1139             =head2 nl
1140              
1141             Sets or gets the new line character, which defaults to C<\n>
1142              
1143             It returns a L<number object|Module::Generic::Number>
1144              
1145             =head2 parse
1146              
1147             Provided with an array reference of lines to parse and this will parse each line and create all necessary L<release|Changes::Release>, L<group|Changes::Group> and L<change|Changes::Change> objects.
1148              
1149             It returns the current object it was called with upon success, and returns an L<error|Module::Generic/error> upon error.
1150              
1151             =head2 preamble
1152              
1153             Sets or gets the text of the preamble. A preamble is a chunk of text, possibly multi line, that appears at the top of the Changes file before any release information.
1154              
1155             =head2 releases
1156              
1157             Read only. This returns an L<array object|Module::Generic::Array> containing all the L<release objects|Changes::Release> within the Changes file.
1158              
1159             =head2 remove_release
1160              
1161             This is an alias for L</delete_release>
1162              
1163             =head2 serialise
1164              
1165             This is an alias for L</as_string>
1166              
1167             =head2 serialize
1168              
1169             This is an alias for L</as_string>
1170              
1171             =head2 time_zone
1172              
1173             Sets or gets a time zone to use for the release date. A valid time zone can either be an olson time zone string such as C<Asia/Tokyo>, or an L<DateTime::TimeZone> object.
1174              
1175             If set, it will be passed to all new L<Changes::Release> object upon parsing with L</parse>
1176              
1177             It returns a L<DateTime::TimeZone> object upon success, or an L<error|Module::Generic/error> if an error occurred.
1178              
1179             =head2 type
1180              
1181             Sets or get the type of C<Changes> file format this is.
1182              
1183             =head2 wrapper
1184              
1185             Sets or gets a code reference as a callback mechanism to return a properly wrapped change text. This allows flexibility beyond the default use of L<Text::Wrap> and L<Text::Format> by L<Changes::Change>.
1186              
1187             If set, this is passed by L</parse> when creating L<Changes::Change> objects.
1188              
1189             See L<Changes::Change/as_string> for more information.
1190              
1191             =head2 write
1192              
1193             This will open the file set with L</file> in write clobbering mode and print out the result from L</as_string>.
1194              
1195             It returns the current object upon success, and an L<error|Module::Generic/error> if an error occurred.
1196              
1197             =head1 AUTHOR
1198              
1199             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
1200              
1201             =head1 SEE ALSO
1202              
1203             L<Changes::Release>, L<Changes::Group>, L<Changes::Change>, L<Changes::Version>, L<Changes::NewLine>
1204              
1205             =head1 COPYRIGHT & LICENSE
1206              
1207             Copyright(c) 2022 DEGUEST Pte. Ltd.
1208              
1209             All rights reserved
1210              
1211             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
1212              
1213             =cut