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   1161109 use strict;
  10         20  
  10         323  
6 10     10   46 use warnings;
  10         13  
  10         271  
7              
8 10     10   154 use 5.8.8;
  10         28  
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.00
23              
24             =cut
25              
26             our $VERSION = 1.00;
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   41 use Exporter 5.57 'import';
  10         174  
  10         755  
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   76 use Carp;
  10         28  
  10         805  
162             $Carp::Internal{ __PACKAGE__ }++;
163              
164 10     10   4543 use Sub::Name 0.08;
  10         5276  
  10         603  
165 10     10   8674 use DateTime;
  10         3425694  
  10         495  
166 10     10   6434 use DateTime::Format::ISO8601;
  10         395705  
  10         7009  
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 86 my (@stanzas) = @_;
198 49         55 my $activations = 0;
199              
200 49         144 my $now = DateTime->now();
201 49         10092 foreach my $stanza (@stanzas) {
202 99 100       748 if (ref($stanza) eq 'Time::Activated::Before') {
    100          
    50          
203 26 100       93 if ($now < $stanza->{before}) {
204 16         1973 $stanza->{code}();
205 16         4929 $activations++;
206             }
207             } elsif (ref($stanza) eq 'Time::Activated::After') {
208 41 100       363 if ($now >= $stanza->{after}) {
209 33         4418 $stanza->{code}();
210 33         9773 $activations++;
211             }
212             } elsif (ref($stanza) eq 'Time::Activated::Between') {
213 32 100       134 if ($stanza->{after} > $stanza->{before}) {
214 11         569 my $before = $stanza->{after};
215 11         17 $stanza->{after} = $stanza->{before};
216 11         13 $stanza->{before} = $before;
217             }
218 32 100 66     1209 if ($now >= $stanza->{after} && $now <= $stanza->{before}) {
219 26         6374 $stanza->{code}();
220 26         7473 $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         2579 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 66 my ( $before, $block, @rest ) = @_;
253              
254 26 50       74 croak 'Useless bare before_moment()' unless wantarray;
255              
256 26         37 my $caller = caller;
257 26         184 subname("${caller}::before_moment{...} " => $block);
258              
259 26         59 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 85 my ( $after, $block, @rest ) = @_;
275              
276 41 50       103 croak 'Useless bare after_moment()' unless wantarray;
277              
278 41         66 my $caller = caller;
279 41         239 subname("${caller}::after _moment{...} " => $block);
280              
281 41         93 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 67 my ( $after, $before, $block, @rest ) = @_;
297              
298 32 50       101 croak 'Useless bare between_moments()' unless wantarray;
299              
300 32         72 my $caller = caller;
301 32         192 subname("${caller}::between_moments{...} " => $block);
302              
303 32         66 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 111259 my ($code) = @_;
317 99         392 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             Currently supported formtats for all date time.
326              
327             =cut
328              
329             sub _spawn_dt {
330 131     131   152 my ($iso8601_or_datetime) = @_;
331              
332 131 100 66     733 my $dt = ref $iso8601_or_datetime && $iso8601_or_datetime->isa('DateTime')
333             ? $iso8601_or_datetime
334             : DateTime::Format::ISO8601->parse_datetime($iso8601_or_datetime);
335              
336 131         46293 return $dt;
337             }
338              
339             1;
340              
341             __END__
342              
343             =head1 DIAGNOSTICS
344              
345             =over 4
346              
347             =item time_activated
348              
349             (F) time_activated() encountered an unexpected argument...
350              
351             time_activated is not followed by either after_moment, before_moment or between_moments
352              
353             time_activated wierd_sub(); #<- Plain weird but it could somehow happen
354              
355             =item after_moment before_moment between_moments
356              
357             (F) Useless bare after_moment()
358             (F) Useless bare before_moment()
359             (F) Useless bare between_moments()
360              
361             Use of xxxxx() with no time_activated before it.
362             Generally the result of a ; instead of a ,.
363              
364             time_activated
365             after_moment '2018' {}; #<- mind the ;
366             before_moment '2018' {}; #<- This one triggers a 'Useless bare before()' since it is not part of the time_activated call
367              
368             =head1 BUGS AND LIMITATIONS
369              
370             No known bugs, but you cannot have this syntax.
371             Some , and/or => required:
372              
373             time_activated
374             before_moment '2016-09-24' {}
375             after_moment '2016-10-24' {};
376              
377             =head1 DEPENDENCIES
378              
379             L<DateTime|DateTime>, L<DateTime::Format::ISO8601|DateTime::Format::ISO8601>, L<Carp|Carp>, L<Exporter|Exporter>, L<Sub::Name|Sub::Name>.
380              
381             =head1 INCOMPATIBILITIES
382              
383             Versions prior to 1.00 have collission with Moose.
384             Naturally, Moose wins and compatibility breaks from 0.12 to 1.00.
385              
386             =head1 SEE ALSO
387              
388             =over 4
389              
390             =item L<Try::Tiny|Try::Tiny>
391              
392             A non related module that became the inspiration for Time::Activated.
393              
394             =back
395              
396             =head1 VERSION CONTROL
397              
398             L<http://github.com/gbarco/Time-Activated/>
399              
400             =head1 SUPPORT
401              
402             Bugs may be submitted through L<the RT bug tracker|https://rt.cpan.org/Public/Dist/Display.html?Name=Time-Activated>
403             (or L<bug-Time-Activated@rt.cpan.org|mailto:bug-Time-Activated@rt.cpan.org>).
404              
405             =head1 AUTHOR
406              
407             =over 4
408              
409             =item *
410              
411             Gonzalo Barco <gbarco uy at gmail.com, no spaces>
412              
413             =back
414              
415             =head1 LICENSE AND COPYRIGHT
416              
417             Copyright 2016 Gonzalo Barco.
418              
419             This program is free software; you can redistribute it and/or modify it
420             under the terms of either: the GNU General Public License as published
421             by the Free Software Foundation; or the Artistic License.
422              
423             See http://dev.perl.org/licenses/ for more information.
424              
425             =cut