File Coverage

lib/Time/Activated.pm
Criterion Covered Total %
statement 62 63 98.4
branch 18 24 75.0
condition 4 6 66.6
subroutine 14 14 100.0
pod 5 5 100.0
total 103 112 91.9


line stmt bran cond sub pod time code
1             package Time::Activated;
2              
3             ## no critic (ProhibitSubroutinePrototypes, ProhibitAutomaticExportation)
4              
5 10     10   927137 use strict;
  10         19  
  10         279  
6 10     10   41 use warnings;
  10         10  
  10         239  
7              
8 10     10   148 use 5.8.8;
  10         29  
9              
10             =pod
11              
12             =encoding UTF-8
13              
14             =cut
15              
16             =head1 NAME
17              
18             Time::Activated - Syntactic sugar over time activated code supporting DateTime and ISO8601 (a.k.a. "Javascript dates").
19              
20             =head1 VERSION
21              
22             Version 1.01
23              
24             =cut
25              
26             our $VERSION = 1.01;
27              
28             =head1 SYNOPSIS
29              
30             use Time::Activated;
31              
32             # simple statements
33             time_activated after_moment '1985-01-01T00:00:00' => execute_logic { print "New feature beginning Jan 1st 1985!" };
34             time_activated before_moment '1986-12-31T00:00:00' => execute_logic { print "This feature ends by 1986!" };
35             time_activated before_moment '2000' => execute_logic { print "Let's dance like its 1999!" };
36             time_activated
37             between_moments '2016-01-01T00:00:00' => '2016-12-31T23:59:59' =>
38             execute_logic { print "Business logic exception for 2016!" };
39              
40             # combined statements a la try {} catch {} by Try::Tiny (tm)
41             time_activated
42             after_moment '1985-01T00:00:00-03:00' => execute_logic { print "New business logic!" }, # <-- Gotcha! it is a ,
43             before_moment '1986-12-31T00:00:00-03:00' => execute_logic { print "Old business logic!" };
44              
45             # elements get evaluated in order
46             time_activated
47             before_moment '1986-12-31T00:00:00-03:00' => execute_logic { print "Old business logic!" }, # <-- Switch that ;
48             after_moment '1985-01-01T00:00:00-03:00' => execute_logic { print "New business logic!" }; # <-- Switch that ,
49              
50             # overlapping allowed, all matching items get executed
51             time_activated
52             after_moment '2018', execute_logic { print "This is from 2018-01-01 and on." },
53             after_moment '2018-06-01', execute_logic { print "This is from 2018-06-01 and on. On top of the previuos." };
54              
55             # Alternate syntax
56             time_activated
57             after_moment '2018', execute_logic { print "Welcome to new business process for 2018!" }, #=> is a ,
58             after_moment '2019', execute_logic { print "This is added on top of 2018 processes for 2019!" };
59              
60             # DateTime objects can be used to define points in time
61             my $dt = DateTime->new(year=>2018, month=>10, day=>16);
62             time_activated after_moment $dt => execute_logic { print "This happens after 2018-10-16!" };
63              
64             =head1 DESCRIPTION
65              
66             This modules aims at managing and documenting time activated code such as that which may araise from migrations and planified process changes in a way that can be
67             integrated and tested in advance.
68              
69             You can use Time::Activated C<before>, C<after> and C<between> to state which parts of code will be executed on certain dates due to changing business rules,
70             programmed web service changes in endpoints/contracts or other time related events.
71              
72             =head1 USAGE
73              
74              
75              
76             =cut
77              
78 10     10   36 use Exporter 5.57 'import';
  10         152  
  10         725  
79             our @EXPORT = our @EXPORT_OK = qw(time_activated before_moment after_moment between_moments execute_logic);
80              
81             =head1 EXPORTS
82              
83             By default Time::Activated exports C<time_activated>, C<before>, C<after>, C<between> and C<execute>.
84              
85             If you need to rename the C<time_activated>, C<after>, C<before>, C<between> or C<executye> keyword consider using L<Sub::Import|Sub::Import> to
86             get L<Sub::Exporter|Sub::Exporter>'s flexibility.
87              
88             If automatic exporting sound nasty: use Time::Activated qw();
89              
90             =head1 SYNTAX
91              
92             time_activated "CONDITION" "WHEN" "WHAT"
93              
94             =head2 "CONDITION"
95              
96             Can be any of C<after_moment>, C<before_moment>, C<between_moments>.
97             C<after_moment>, accepts a parameters representing a point in time B<at and after> which the execute_logic statement will be executed.
98             C<before_moment>, accepts a parameters representing a point in time B<before, but not including>, which the execute_logic statement will be executed.
99             C<between_moments>, accepts two parameters representing a range in time B<between, both limits included>, which the execute_logic statement will be executed.
100              
101             =head2 "WHEN"
102              
103             Is either a DateTime object or a scalar representing a iso8601 (a.k.a. Javascript date)
104              
105             Expansion is supported so '2000', '2000-01', '2000-01-01' and '2000-01-01T00:00' all are equivalents to '2000-01-01T00:00:00'.
106              
107             Timezones are supported and honored. Thus:
108              
109             time_activated
110             after_moment '1999-12-31T23:00:00-01:00' => execute_logic { print('Matches from 2000-01-01T00:00:00 GMT!') },
111             after_moment '2000-01-01T00:00:00+01:00' => execute_logic { print('Matches from 1999-01-01T23:00:00 GMT!') };
112              
113             C<after> includes the exact time which is used as parameter, C<before> does not.
114             Thus using C<after> and C<before> with the same time parameter ensures that only one statement gets executed.
115             i.e.:
116              
117             time_activated
118             before_moment SOME_DATE => execute { print "Before!" },
119             after_moment SOME_DATE => execute { print "After!" };
120              
121              
122             =head2 "WHAT"
123              
124             Is either an anonymous code block or a reference to subroutine
125             Code that will be executed on a given conditions in many ways:
126              
127             time_activated
128             after_moment '2001' => execute_logic \&my_great_new_feature; #No parameters can be passed with references...
129              
130             time_activated
131             after_moment '2000' => execute_logic { print 'Y2K ready!' },
132             after_moment '2001' => execute_logic (\&my_great_new_feature), #References with multilines need ()
133             after_moment '2002' => execute_logic { &my_great_new_feature("We need parameters by 2002")};
134              
135             =head2 CONSTANTS
136              
137             It is cool to use constants documenting both time and intent.
138              
139             use constants PROCESS_X_CUTOVER_DATE => '2017-01-01T00:00:00';
140              
141             time_activated after_moment PROCESS_X_CUTOVER_DATE => execute_logic { &new_business_process($some_state) };
142              
143             =cut
144              
145             =head1 TESTING
146              
147             L<Test::MockTime|Test::MockTime> is your friend.
148              
149             use Test::More tests => 1;
150             use Time::Activated;
151             use Test::MockTime;
152              
153             Test::MockTime::set_absolute_time('1986-05-27T00:00:00Z');
154             time_activated after_moment '1985-01-01T00:00:00-03:00' => execute_logic { pass('Basic after') }; # this gets executed
155              
156             Test::MockTime::set_absolute_time('1984-05-27T00:00:00Z');
157             time_activated after_moment '1985-01-01T00:00:00-03:00' => execute_logic { fail('Basic after') }; # this does not get executed
158              
159             =cut
160              
161 10     10   65 use Carp;
  10         26  
  10         787  
162             $Carp::Internal{ __PACKAGE__ }++;
163              
164 10     10   4135 use Sub::Name 0.08;
  10         4806  
  10         525  
165 10     10   8088 use DateTime;
  10         3533943  
  10         500  
166 10     10   6714 use DateTime::Format::ISO8601;
  10         405729  
  10         6449  
167              
168             =head1 SUBROUTINES/METHODS
169              
170             =head2 time_activated
171              
172             C<time_activated> is both the syntactical placeholder for grammar in C<Time::Activated> and the internal implementation of the modules functionality.
173              
174             Syntactically the structure is like so (note the ','s and ';'):
175              
176             time_activated
177             after_moment ..., execute_logic ...,
178             before_moment ..., execute_logic ...,
179             between_moments ..., ... execute_logic ...;
180              
181             Alternatively some can be changed for a => for a fancy syntax. This abuses anonymous hashes, some inteligent selections of prototypes (stolen from L<Try::Tiny|Try::Tiny>) and probably
182             other clever perl-ish syntactical elements that escape my understanding. Note '=>'s, ','s and ';':
183              
184             time_activated
185             after_moment ... => execute_logic ...,
186             before_moment ... => execute_logic ...,
187             between_moments ... => ... => execute_logic ...; #Given. This does not look so fancy but more into the weird side...
188              
189             =cut
190              
191             # Blatantly stolen from Try::Tiny since it really makes sence and changing it produces headaches.
192             # Need to prototype as @ not $$ because of the way Perl evaluates the prototype.
193             # Keeping it at $$ means you only ever get 1 sub because we need to eval in a list
194             # context & not a scalar one
195              
196             sub time_activated (@) {
197 49     49 1 104 my (@stanzas) = @_;
198 49         67 my $activations = 0;
199              
200 49         138 my $now = DateTime->now();
201 49         10300 foreach my $stanza (@stanzas) {
202 99 100       765 if (ref($stanza) eq 'Time::Activated::Before') {
    100          
    50          
203 26 100       95 if ($now < $stanza->{before}) {
204 16         1910 $stanza->{code}();
205 16         4510 $activations++;
206             }
207             } elsif (ref($stanza) eq 'Time::Activated::After') {
208 41 100       385 if ($now >= $stanza->{after}) {
209 33         4729 $stanza->{code}();
210 33         10362 $activations++;
211             }
212             } elsif (ref($stanza) eq 'Time::Activated::Between') {
213 32 100       125 if ($stanza->{after} > $stanza->{before}) {
214 11         611 my $before = $stanza->{after};
215 11         18 $stanza->{after} = $stanza->{before};
216 11         14 $stanza->{before} = $before;
217             }
218 32 100 66     1207 if ($now >= $stanza->{after} && $now <= $stanza->{before}) {
219 26         6594 $stanza->{code}();
220 26         7524 $activations++;
221             };
222             } else {
223 0 0       0 croak('time_activated() encountered an unexpected argument (' . ( defined $stanza ? $stanza : 'undef' ) . ') - perhaps a missing semi-colon?' );
224             }
225             }
226 49         2724 return $activations;
227             }
228              
229             =head2 before_moment
230              
231             C<before_moment> defines a point in time before B<not including the exact point in time> which code is executed.
232              
233             This does not happen before January 1st 2018 at 00:00 but does happen from that exact point in time and on.
234              
235             time_activated
236             before_moment '2018', execute_logic { print "We are awaiting for 1/1/2018..." };
237              
238             Another fancy way to say do not do that before January 1st 2018 at 00:00.
239              
240             ime_activated
241             before_moment '2018' => execute_logic { print "We are awaiting for 1/1/2018..." };
242              
243             A fancy way to combine before statements.
244              
245             time_activated
246             before_moment '2018' => execute_logic { print "We are awaiting for 1/1/2018..." },
247             before_moment '2019' => execute_logic { print "Not quite there for 1/1/2019..." };
248              
249             =cut
250              
251             sub before_moment ($$;@) {
252 26     26 1 51 my ( $before, $block, @rest ) = @_;
253              
254 26 50       70 croak 'Useless bare before_moment()' unless wantarray;
255              
256 26         39 my $caller = caller;
257 26         158 subname("${caller}::before_moment{...} " => $block);
258              
259 26         58 return (bless({before => _spawn_dt($before), code => $block},'Time::Activated::Before'), @rest);
260             }
261              
262             =head2 after_moment
263              
264             C<after_moment> defines a point in time after B<including the exact point in time> which code is executed.
265              
266             time_activated
267             after_moment '2018' => execute { print "Wea are either at 1/1/2018 or after it..." };
268              
269             As with C<before_moment> statements can be combined with C<before_moment>, C<after_moment> and C<between_moments> with no limit.
270              
271             =cut
272              
273             sub after_moment ($$;@) {
274 41     41 1 124 my ( $after, $block, @rest ) = @_;
275              
276 41 50       118 croak 'Useless bare after_moment()' unless wantarray;
277              
278 41         62 my $caller = caller;
279 41         248 subname("${caller}::after _moment{...} " => $block);
280              
281 41         78 return (bless({after => _spawn_dt($after), code => $block},'Time::Activated::After'), @rest);
282             }
283              
284             =head2 between_moments
285              
286             C<between_moments> defines two points in time between which code is executes B<including both exact points in time>.
287              
288             time_activated
289             between_moment '2018' => '2018-12-31T23:59:59' => execute_logic { print "This is 2018..." };
290              
291             As with C<before_moments> statements can be combined with C<before_moment>, C<after_moment> and C<between_moment> with no limit.
292              
293             =cut
294              
295             sub between_moments ($$$;@) {
296 32     32 1 107 my ( $after, $before, $block, @rest ) = @_;
297              
298 32 50       115 croak 'Useless bare between_moments()' unless wantarray;
299              
300 32         73 my $caller = caller;
301 32         184 subname("${caller}::between_moments{...} " => $block);
302              
303 32         62 return (bless({before => _spawn_dt($before), after => _spawn_dt($after), code => $block},'Time::Activated::Between'), @rest);
304             }
305              
306             =head2 execute_logic
307              
308             Exists for the sole reason of verbosity.
309             Accepts a single parameters that must be a subroutine or anonymous code block.
310              
311             execute_logic { print "This is a verbose way of saying that this will be executed!" };
312              
313             =cut
314              
315             sub execute_logic(&) {
316 99     99 1 120931 my ($code) = @_;
317 99         444 return $code;
318             }
319              
320             =head2 PRIVATES
321              
322             =head3 _spawn_dt
323              
324             C<_spawn_dt> is a private function defined in hopes that additional date formats can be used to define points in time.
325              
326             =cut
327              
328             sub _spawn_dt {
329 131     131   166 my ($iso8601_or_datetime) = @_;
330              
331 131 100 66     791 my $dt = ref $iso8601_or_datetime && $iso8601_or_datetime->isa('DateTime')
332             ? $iso8601_or_datetime
333             : DateTime::Format::ISO8601->parse_datetime($iso8601_or_datetime);
334              
335 131         46612 return $dt;
336             }
337              
338             1;
339              
340             __END__
341              
342             =head1 DIAGNOSTICS
343              
344             =over 4
345              
346             =item time_activated
347              
348             (F) time_activated() encountered an unexpected argument...
349              
350             time_activated is not followed by either after_moment, before_moment or between_moments
351              
352             time_activated wierd_sub(); #<- Plain weird but it could somehow happen
353              
354             =item after_moment before_moment between_moments
355              
356             (F) Useless bare after_moment()
357             (F) Useless bare before_moment()
358             (F) Useless bare between_moments()
359              
360             Use of xxxxx() with no time_activated before it.
361             Generally the result of a ; instead of a ,.
362              
363             time_activated
364             after_moment '2018' {}; #<- mind the ;
365             before_moment '2018' {}; #<- This one triggers a 'Useless bare before()' since it is not part of the time_activated call
366              
367             =head1 BUGS AND LIMITATIONS
368              
369             No known bugs, but you cannot have this syntax.
370             Some , and/or => required:
371              
372             time_activated
373             before_moment '2016-09-24' {}
374             after_moment '2016-10-24' {};
375              
376             =head1 DEPENDENCIES
377              
378             L<DateTime|DateTime>, L<DateTime::Format::ISO8601|DateTime::Format::ISO8601>, L<Carp|Carp>, L<Exporter|Exporter>, L<Sub::Name|Sub::Name>.
379              
380             =head1 INCOMPATIBILITIES
381              
382             Versions prior to 1.00 have collission with Moose.
383             Naturally, Moose wins and compatibility breaks from 0.12 to 1.00.
384              
385             Some old distributions that cannot set dates beyond 2038 fail some tests.
386              
387             =head1 SEE ALSO
388              
389             =over 4
390              
391             =item L<Try::Tiny|Try::Tiny>
392              
393             A non related module that became the inspiration for Time::Activated.
394              
395             =back
396              
397             =head1 VERSION CONTROL
398              
399             L<http://github.com/gbarco/Time-Activated/>
400              
401             =head1 SUPPORT
402              
403             Bugs may be submitted through L<the RT bug tracker|https://rt.cpan.org/Public/Dist/Display.html?Name=Time-Activated>
404             (or L<bug-Time-Activated@rt.cpan.org|mailto:bug-Time-Activated@rt.cpan.org>).
405              
406             =head1 AUTHOR
407              
408             =over 4
409              
410             =item *
411              
412             Gonzalo Barco <gbarco uy at gmail.com, no spaces>
413              
414             =back
415              
416             =head1 LICENSE AND COPYRIGHT
417              
418             Copyright 2016 Gonzalo Barco.
419              
420             This program is free software; you can redistribute it and/or modify it
421             under the terms of either: the GNU General Public License as published
422             by the Free Software Foundation; or the Artistic License.
423              
424             See http://dev.perl.org/licenses/ for more information.
425              
426             =cut