File Coverage

lib/Changes.pm
Criterion Covered Total %
statement 291 412 70.6
branch 108 226 47.7
condition 58 156 37.1
subroutine 44 53 83.0
pod 29 31 93.5
total 530 878 60.3


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