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.1
4             ## Copyright(c) 2023 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2022/12/09
7             ## Modified 2023/07/30
8             ## All rights reserved
9             ##
10             ##
11             ## This program is free software; you can redistribute it and/or modify it
12             ## under the same terms as Perl itself.
13             ##----------------------------------------------------------------------------
14             package Changes;
15             BEGIN
16             {
17 18     18   362953823 use strict;
  18         196  
  18         578  
18 18     18   95 use warnings;
  18         31  
  18         490  
19 18     18   93 use warnings::register;
  18         38  
  18         1931  
20 18     18   615 use parent qw( Module::Generic );
  18         338  
  18         112  
21 18     18   37411264 use vars qw( $VERSION $VERSION_LAX_REGEX $DATE_DISTZILA_RE $DATETIME_RE );
  18         37  
  18         1255  
22 18     18   8160 use Changes::Release;
  18         81  
  18         327  
23 18     18   7623 use Changes::Group;
  18         42  
  18         143  
24 18     18   14169 use Changes::Change;
  18         68  
  18         371  
25 18     18   7422 use Nice::Try;
  18         40  
  18         166  
26             # From version::regex
27 18     18   37460378 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         71 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         592 our $VERSION = 'v0.3.1';
53             };
54              
55 18     18   230 use strict;
  18         35  
  18         574  
56 18     18   110 use warnings;
  18         34  
  18         14532  
57              
58             sub init
59             {
60 22     22 1 2270 my $self = shift( @_ );
61 22         1277 $self->{defaults} = undef;
62 22         87 $self->{elements} = [];
63 22         72 $self->{epilogue} = undef;
64 22         56 $self->{file} = undef;
65 22         62 $self->{max_width} = 0;
66 22         86 $self->{mode} = '+<';
67 22         67 $self->{nl} = "\n";
68 22         73 $self->{preamble} = undef;
69 22         65 $self->{releases} = [];
70 22         58 $self->{time_zone} = undef;
71 22         59 $self->{type} = undef;
72 22         64 $self->{wrapper} = undef;
73 22         59 $self->{_init_strict_use_sub} = 1;
74 22         79 $self->{_init_params_order} = [qw( preset )];
75 22 50       174 $self->SUPER::init( @_ ) || return( $self->pass_error );
76 22         45975 return( $self );
77             }
78              
79             sub add_epilogue
80             {
81 1     1 1 25 my( $self, $text ) = @_;
82 1 50 33     11 if( !defined( $text ) || !length( "$text" ) )
83             {
84 0         0 return( $self->error( "No text was provided to add an epilogue." ) );
85             }
86 1         5 my $elements = $self->elements;
87 1         959 my $last = $elements->last;
88 1 50 33     74 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         15 $self->epilogue( $text );
93 1         994 return( $self );
94             }
95              
96             sub add_preamble
97             {
98 1     1 1 2278 my( $self, $text ) = @_;
99 1 50 33     27 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         155 return( $self );
105             }
106              
107             sub add_release
108             {
109 4     4 1 447 my $self = shift( @_ );
110 4         40 my( $rel, $opts );
111 4         15 my $elements = $self->elements;
112 4 100 66     4001 if( scalar( @_ ) == 1 && $self->_is_a( $_[0] => 'Changes::Release' ) )
113             {
114 2         87 $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         88 my $vers = $rel->version;
120 2 50       509 if( length( "$vers" ) )
121             {
122 2 100   2   64 my $same = $elements->grep(sub{ $self->_is_a( $_ => 'Changes::Release' ) && $_->version == "$vers" });
  2         417  
123 2 50       230 return( $self->error( "A similar release with version '$vers' is already registered." ) ) if( !$same->is_empty );
124             }
125             }
126             else
127             {
128 2         14 $opts = $self->_get_args_as_hash( @_ );
129 2 50 33     319 if( exists( $opts->{version} ) && defined( $opts->{version} ) && length( "$opts->{version}" ) )
      33        
130             {
131 2         6 my $vers = $opts->{version};
132 2 100   2   18 my $same = $elements->grep(sub{ $self->_is_a( $_ => 'Changes::Release' ) && $_->version == "$vers" });
  2         458  
133 2 50       282 return( $self->error( "A similar release with version '$vers' is already registered." ) ) if( !$same->is_empty );
134             }
135 2   50     63 $rel = $self->new_release( %$opts ) || return( $self->pass_error );
136 2         148 return( $self->add_release( $rel ) );
137             }
138 2         68 $elements->unshift( $self->new_line );
139 2         20 $elements->unshift( $rel );
140 2         25 return( $rel );
141             }
142              
143             sub as_string
144             {
145 21     21 1 217869 my $self = shift( @_ );
146 21         140 my $lines = $self->new_array;
147 21         560 my $preamble = $self->preamble;
148 21         19889 my $epilogue = $self->epilogue;
149 21 100 66     18687 if( defined( $preamble ) && !$preamble->is_empty )
150             {
151 4         61 $lines->push( $preamble->scalar );
152             }
153            
154             $self->elements->foreach(sub
155             {
156 71     71   20630 my $str;
157 71 50       298 $str = $_->as_string if( $self->_can( $_ => 'as_string' ) );
158 71 50       754 if( defined( $str ) )
159             {
160 71         265 $lines->push( $str->scalar );
161             }
162 21         160 });
163 21 100 66     3429 if( defined( $epilogue ) && !$epilogue->is_empty )
164             {
165 2         26 $lines->push( $epilogue->scalar );
166             }
167 21         114 return( $lines->join( '' ) );
168             }
169              
170             {
171 18     18   145 no warnings 'once';
  18         33  
  18         48539  
172             *serialize = \&as_string;
173             *serialise = \&as_string;
174             }
175              
176 114     114 1 14825104 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 671 sub elements { return( shift->_set_get_array_as_object( 'elements', @_ ) ); }
220              
221 24     24 1 41740 sub epilogue { return( shift->_set_get_scalar_as_object( 'epilogue', @_ ) ); }
222              
223 4     4 1 8079 sub file { return( shift->_set_get_file( 'file', @_ ) ); }
224              
225             sub freeze
226             {
227 20     20 0 54 my $self = shift( @_ );
228             $self->elements->foreach(sub
229             {
230 67 100   67   13808 if( $self->_can( $_ => 'freeze' ) )
231             {
232 44         1271 $_->freeze;
233             }
234 20         76 });
235 20         499 return( $self );
236             }
237              
238 0     0 1 0 sub history { return( shift->releases( @_ ) ); }
239              
240             sub load
241             {
242 1     1 1 3685702 my $this = shift( @_ );
243 1   50     6 my $file = shift( @_ ) ||
244             return( $this->error( "No changes file was provided to load." ) );
245 1         39 my $opts = $this->_get_args_as_hash( @_ );
246 1   50     148 my $self = $this->new( %$opts ) ||
247             return( $this->pass_error );
248 1   50     18 my $f = $self->new_file( $file ) ||
249             return( $this->pass_error( $self->error ) );
250 1   50     145959 my $mode = $self->mode // '+<';
251 1 50       154 $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     10000 my $lines = $f->lines ||
255             return( $this->pass_error( $f->error ) );
256 1 50       3185 $self->parse( $lines ) || return( $self->pass_error );
257 1         44 $self->freeze;
258 1         5 return( $self );
259             }
260              
261             sub load_data
262             {
263 19     19 1 40567 my $this = shift( @_ );
264 19         64 my $data = shift( @_ );
265 19         212 my $opts = $this->_get_args_as_hash( @_ );
266 19   50     144229 my $self = $this->new( %$opts ) ||
267             return( $this->pass_error );
268 19 50 33     348 return( $self ) if( !defined( $data ) || !length( "$data" ) );
269 19         444 my $lines = $self->new_array( [split( /(?<=\n)/, $data )] );
270             # $lines->chomp;
271 19 50       560 $self->parse( $lines ) || return( $self->pass_error );
272 19         761 $self->freeze;
273 19         77 return( $self );
274             }
275              
276 22     22 1 14821866 sub max_width { return( shift->_set_get_number( 'max_width', @_ ) ); }
277              
278             sub new_change
279             {
280 41     41 1 167 my $self = shift( @_ );
281 41         314 my $opts = $self->_get_args_as_hash( @_ );
282 41         7732 my $defaults = $self->defaults;
283 41 50       36204 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     380 my $c = Changes::Change->new( $opts ) ||
291             return( $self->pass_error( Changes::Change->error ) );
292 41         818 return( $c );
293             }
294              
295             sub new_group
296             {
297 6     6 1 18 my $self = shift( @_ );
298 6         51 my $opts = $self->_get_args_as_hash( @_ );
299 6         1109 my $defaults = $self->defaults;
300 6 50       5290 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     53 my $g = Changes::Group->new( $opts ) ||
316             return( $self->pass_error( Changes::Group->error ) );
317 6         104 return( $g );
318             }
319              
320             sub new_line
321             {
322 25     25 1 73 my $self = shift( @_ );
323 25 50       171 $self->_load_class( 'Changes::NewLine' ) || return( $self->pass_error );
324 25   50     4955 my $nl = Changes::NewLine->new( @_ ) ||
325             return( $self->pass_error( Changes::NewLine->error ) );
326 25         336 return( $nl );
327             }
328              
329             sub new_release
330             {
331 46     46 1 150 my $self = shift( @_ );
332 46         350 my $opts = $self->_get_args_as_hash( @_ );
333 46         9000 my $defaults = $self->defaults;
334 46 100       42854 if( defined( $defaults ) )
335             {
336 2         14 my $def = { %$defaults };
337 2         269 foreach my $opt ( qw( datetime_formatter format spacer time_zone ) )
338             {
339 8 50 66     58 if( !defined( $opts->{ $opt } ) &&
      66        
      66        
340             exists( $def->{ $opt } ) &&
341             defined( $def->{ $opt } ) &&
342             length( $def->{ $opt } ) )
343             {
344 4         16 $opts->{ $opt } = CORE::delete( $def->{ $opt } );
345             }
346             }
347 2   33     26 $opts->{defaults} //= $def;
348             }
349 46   50     521 my $rel = Changes::Release->new( $opts ) ||
350             return( $self->pass_error( Changes::Release->error ) );
351 46         829 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 20 sub nl { return( shift->_set_get_scalar_as_object( 'nl', @_ ) ); }
364              
365             sub parse
366             {
367 20     20 1 73 my $self = shift( @_ );
368 20   50     539 my $lines = shift( @_ ) || return( $self->error( "No array reference of lines was provided." ) );
369 20 50       164 return( $self->error( "Data provided is not an array reference of lines." ) ) if( !$self->_is_array( $lines ) );
370 20         379 $lines = $self->new_array( $lines );
371 20         672 my $preamble = $self->new_scalar;
372 20         46524377 my $epilogue;
373 20         129 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         504 my $nls = $self->new_array;
376 20   50     417 my $max_width = $self->max_width // 0;
377 20         3004725 my $debug = $self->debug;
378 20         494 my( $group, $release, $change );
379             # $type is the Changes file type. It contains the value guessed, otherwise it remains undef
380 20         124 my $type = $self->type;
381 20         17966 my $wrapper = $self->wrapper;
382 20         17428 my $tz = $self->time_zone;
383 20         127 my $defaults = $self->defaults;
384             # Cache it
385 20 100       21099 unless( defined( $DATETIME_RE ) )
386             {
387 15         231 $DATETIME_RE = $self->_get_datetime_regexp( 'all' );
388             }
389 20         26045317 for( my $i = 0; $i < scalar( @$lines ); $i++ )
390             {
391 127         70981 my $l = $lines->[$i];
392             # DistZilla release line
393             # 0.01 2022-12-11 08:07:12 Asia/Tokyo
394 127 100 100     20498 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         101 my $re = { %+ };
404             # Create the DateTime object
405 2 50       21 $self->_load_class( 'DateTime' ) || return( $self->pass_error );
406 2 50       84 $self->_load_class( 'DateTime::TimeZone' ) || return( $self->pass_error );
407 2 50       58 $self->_load_class( 'DateTime::Format::Strptime' ) || return( $self->pass_error );
408 2         204523 my( $dt, $tz, $fmt );
409 2 50 33     11 try
  2         4  
  2         4  
  2         15  
  0         0  
  2         5  
  2         12  
  2         6  
410 2     2   4 {
411 2         21 $tz = DateTime::TimeZone->new( name => $re->{r_tz} );
412             }
413 2 0 50     14 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     32108  
  2 0       10  
  2 0       6  
  2 0       5  
  2 0       4  
  2 0       4  
  2 0       11  
  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         5  
  0         0  
  0         0  
  2         15  
  2         12  
  2         6  
  2         10  
  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   160 }
  18 0 0     41  
  18 0 33     21324  
  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       7  
  0 0       0  
  2 0       93  
  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         8  
423            
424            
425 2 50 33     5 try
  2         4  
  2         3  
  2         10  
  0         0  
  2         4  
  2         5  
  2         5  
426 2     2   4 {
427 2         32 $fmt = DateTime::Format::Strptime->new(
428             pattern => "%F$re->{r_dt_space}%T$re->{r_tz_space}%O",
429             );
430             }
431 2 0 50     13 catch( $e )
  2 0 33     3914  
  2 0       8  
  2 0       6  
  2 0       4  
  2 0       3  
  2 0       4  
  2 0       10  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 0       0  
  0 50       0  
  0         0  
  0         0  
  0         0  
  2         8  
  0         0  
  2         6  
  0         0  
  0         0  
  2         11  
  2         10  
  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   144 }
  18 0 0     38  
  18 0 33     22507  
  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       7  
  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         10  
  0         0  
  0         0  
  0         0  
  0         0  
  2         8  
438            
439 2 50 33     5 try
  2         4  
  2         3  
  2         9  
  0         0  
  2         4  
  2         5  
  2         5  
440 2     2   4 {
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         22 time_zone => $tz,
449             );
450 2         1754 $dt->set_formatter( $fmt );
451             }
452 2 0 50     12 catch( $e )
  2 0 33     145  
  2 0       7  
  2 0       5  
  2 0       4  
  2 0       3  
  2 0       5  
  2 0       9  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 0       0  
  0 50       0  
  0         0  
  0         0  
  0         0  
  2         10  
  0         0  
  2         4  
  0         0  
  0         0  
  2         7  
  2         11  
  2         5  
  2         7  
  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   147 }
  18 0 0     34  
  18 0 33     66437  
  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       7  
  0 0       0  
  2 0       92  
  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         7  
457            
458 2 100       14 if( !$nls->is_empty )
459             {
460 1         23 $elements->push( $nls->list );
461 1         14 $nls->reset;
462             }
463 2         90 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       27 ( defined( $tz ) ? ( time_zone => $tz ) : () ),
    50          
    50          
475             ( defined( $defaults ) ? ( defaults => $defaults ) : () ),
476             debug => $debug,
477             );
478 2         16 $elements->push( $release );
479 2 50 33     32 if( defined( $preamble ) && !$preamble->is_empty )
480             {
481 0         0 $self->preamble( $preamble );
482 0         0 undef( $preamble );
483             }
484 2 50       25 unless( defined( $type ) )
485             {
486 2         5 $type = 'distzilla';
487 2         12 $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         9743 my $re = { %+ };
504 34   50     1345 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       34869437 if( !$nls->is_empty )
507             {
508 16         406 $elements->push( $nls->list );
509 16         240 $nls->reset;
510             }
511 34         910 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       594 ( defined( $tz ) ? ( time_zone => $tz ) : () ),
    50          
    50          
524             ( defined( $defaults ) ? ( defaults => $defaults ) : () ),
525             debug => $debug,
526             );
527 34         299 $elements->push( $release );
528 34 100 100     601 if( defined( $preamble ) && !$preamble->is_empty )
529             {
530 3         47 $self->preamble( $preamble );
531 3         530 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         1797 my $re = { %+ };
545 8 100       87 if( !$nls->is_empty )
546             {
547 5         126 $elements->push( $nls->list );
548 5         73 $nls->reset;
549             }
550 8         213 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       112 ( defined( $tz ) ? ( time_zone => $tz ) : () ),
    50          
    50          
561             ( defined( $defaults ) ? ( defaults => $defaults ) : () ),
562             debug => $debug,
563             );
564 8         76 $elements->push( $release );
565 8 50 33     132 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         189 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     91 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         123999 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     91 ( defined( $defaults ) ? ( defaults => $defaults ) : () ),
    50          
598             debug => $debug,
599             );
600 6 50       33 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       200 if( !$nls->is_empty )
613             {
614 0         0 $release->elements->push( $nls->list );
615 0         0 $nls->reset;
616             }
617 6         177 $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         2592 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     801 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       54 if( !$nls->is_empty )
644             {
645 0         0 $group->elements->push( $nls->list );
646 0         0 $nls->reset;
647             }
648 7         202 $group->elements->push( $change );
649             }
650             elsif( defined( $release ) )
651             {
652 34 50       251 if( !$nls->is_empty )
653             {
654 0         0 $release->elements->push( $nls->list );
655 0         0 $nls->reset;
656             }
657 34         973 $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         106 my $re = { %+ };
674             # We have an ongoing change, so this is likely a wrapped line. We append the text
675 4 50       24 if( defined( $change ) )
676             {
677 4   33     41 $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         10699 $change->nl( $re->{c_nl} );
680 4         11516 $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         621 my $re = { %+ };
721             # If we are still in the preamble, this might just be a multi lines preamble
722 27 100       199 if( $elements->is_empty )
    50          
723             {
724             # $preamble->append( "$l\n" );
725 4         61 $preamble->append( $l );
726             }
727             # Otherwise, this is a blank line, which separates elements
728             elsif( defined( $release ) )
729             {
730 23         518 undef( $change );
731 23         56 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     408 ? ( 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         930 $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         93 my $re = { %+ };
768 1 50       5 if( !$nls->is_empty )
769             {
770 1         13 $elements->push( $nls->list );
771 1         16 $nls->reset;
772 1         10 undef( $release );
773 1         2 undef( $change );
774 1         2 undef( $group );
775 1         9 $epilogue = $self->new_scalar( $l );
776 1         37 $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         34726 $self->elements( $elements );
790 20         19935 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   8974 my( $self, $text ) = @_;
798 4 50 33     40 if( defined( $text ) && $text->defined )
799             {
800 4 100       52 unless( $text =~ /[\015\012]$/ms )
801             {
802 1   50     21 $text->append( $self->nl // "\n" );
803             }
804 4 100       1042 unless( $text =~ /[\015\012]{2,}$/ms )
805             {
806 1   50     10 $text->append( $self->nl // "\n" );
807             }
808             }
809 4         958 return( $text );
810             },
811 45     45 1 33066 } }, @_ ) ); }
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 927707 my $self = shift( @_ );
857 62     405   291 my $a = $self->elements->grep(sub{ $self->_is_a( $_ => 'Changes::Release' ) });
  405         49551  
858 62         6770 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 87 my $self = shift( @_ );
881 20 50       116 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   153 }
  18 0 0     34  
  18 0 0     7931  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0         0  
  0         0  
  0         0  
  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       110 if( !defined( $self->{time_zone} ) )
904             {
905 20 50       97 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         1027 return;
913             }
914             }
915             else
916             {
917 0         0 return( $self->{time_zone} );
918             }
919             }
920              
921 23     23 1 955 sub type { return( shift->_set_get_scalar_as_object( 'type', @_ ) ); }
922              
923 20     20 1 204 sub wrapper { return( shift->_set_get_code( 'wrapper', @_ ) ); }
924              
925             sub write
926             {
927 1     1 1 1228 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         1044 my $str = $self->as_string;
931 1 50       54 return( $self->pass_error ) if( !defined( $str ) );
932 1 50       5 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     29 my $fh = $f->open( '>', { binmode => 'utf-8', autoflush => 1 } ) ||
938             return( $self->pass_error( $f->error ) );
939 1 50       52146 $fh->print( $str->scalar ) || return( $self->pass_error( $fh->error ) );
940 1         287 $fh->close;
941 1         198 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.1
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             =head2 serialise
1209              
1210             This is an alias for L</as_string>
1211              
1212             =head2 serialize
1213              
1214             This is an alias for L</as_string>
1215              
1216             =head2 time_zone
1217              
1218             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.
1219              
1220             If set, it will be passed to all new L<Changes::Release> object upon parsing with L</parse>
1221              
1222             It returns a L<DateTime::TimeZone> object upon success, or an L<error|Module::Generic/error> if an error occurred.
1223              
1224             =head2 type
1225              
1226             Sets or get the type of C<Changes> file format this is.
1227              
1228             =head2 wrapper
1229              
1230             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>.
1231              
1232             If set, this is passed by L</parse> when creating L<Changes::Change> objects.
1233              
1234             See L<Changes::Change/as_string> for more information.
1235              
1236             =head2 write
1237              
1238             This will open the file set with L</file> in write clobbering mode and print out the result from L</as_string>.
1239              
1240             It returns the current object upon success, and an L<error|Module::Generic/error> if an error occurred.
1241              
1242             =head1 AUTHOR
1243              
1244             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
1245              
1246             =head1 SEE ALSO
1247              
1248             L<Changes::Release>, L<Changes::Group>, L<Changes::Change>, L<Changes::Version>, L<Changes::NewLine>
1249              
1250             =head1 COPYRIGHT & LICENSE
1251              
1252             Copyright(c) 2022 DEGUEST Pte. Ltd.
1253              
1254             All rights reserved
1255              
1256             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
1257              
1258             =cut