File Coverage

lib/Changes.pm
Criterion Covered Total %
statement 374 851 43.9
branch 141 772 18.2
condition 82 345 23.7
subroutine 51 66 77.2
pod 29 31 93.5
total 677 2065 32.7


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