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 9     9   226660 use strict;
  9         10  
  9         190  
6 9     9   28 use warnings;
  9         9  
  9         151  
7              
8 9     9   91 use 5.8.8;
  9         22  
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 0.12
23              
24             =cut
25              
26             our $VERSION = 0.12;
27              
28             =head1 SYNOPSIS
29              
30             use Time::Activated;
31              
32             # simple statements
33             time_activated after '1985-01-01T00:00:00', execute { print "A new feature has been activeted beginning Jan 1st 1985!" };
34             time_activated before '1986-12-31T00:00:00', execute { print "Support for this feature ends by 1986!" };
35             time_activated before '2000', execute { print "Let's dance like its 1999!" };
36             time_activated between '2016-01-01T00:00:00', '2016-12-31T23:59:59', execute { print "Business logic exception for 2016!" };
37              
38             # combined statements a la try {} catch {} by Try::Tiny (tm)
39             time_activated
40             after '1985-01T00:00:00-03:00', execute { print "New business logic!" }, # <-- Gotcha! it is a ,
41             before '1986-12-31T00:00:00-03:00', execute { print "Old business logic!" };
42              
43             # elements get evaluated in order
44             time_activated
45             before '1986-12-31T00:00:00-03:00', execute { print "Old business logic!" }, # <-- Switch that ;
46             after '1985-01-01T00:00:00-03:00', execute { print "New business logic!" }; # <-- Switch that ,
47              
48             # all overlapping allowed, all matching gets executed
49             time_activated
50             after '2018', execute { print "This is from 2018-01-01 and on." },
51             after '2018-06-01', execute { print "This is from 2018-06-01 and on only, but on top of what we do after 2018-01-01." };
52              
53             # FANCY and... cof... recommended syntax...
54             time_activated
55             after '2018' => execute { print "Welcome to new business process for 2018!" },
56             after '2019' => execute { print "This is added on top of 2018 processes for 2019!" };
57              
58             # DateTime objects can be used to define points in time
59             my $dt = DateTime->new(year=>2018, month=>10, day=>16);
60             time_activated
61             after $dt => execute { print "This happens after 2018-10-16!" };
62              
63             =head1 DESCRIPTION
64              
65             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
66             integrated and tested in advance.
67              
68             You can use Time::Activated C, C and C to state which parts of code will be executed on certain dates due to changing business rules,
69             programmed web service changes in endpoints/contracts or other time related events.
70              
71             =cut
72              
73 9     9   28 use Exporter 5.57 'import';
  9         97  
  9         521  
74             our @EXPORT = our @EXPORT_OK = qw(time_activated before after between execute);
75              
76             =head1 EXPORTS
77              
78             By default Time::Activated exports C, C, C, C and C.
79              
80             If you need to rename the C, C, C, C or C keyword consider using L to
81             get L's flexibility.
82              
83             If automatic exporting sound nasty: use Time::Activated qw();
84              
85             =head1 SYNTAX
86              
87             time_activated "CONDITION" "WHEN" "WHAT"
88              
89             =head2 "CONDITION"
90              
91             Can be any of C, C, C.
92             C, accepts a parameters representing a point in time B which the execute statement will be executed.
93             C, accepts a parameters representing a point in time B, which the execute statement will be executed.
94             C, accepts two parameters representing a range in time B, which the execute statement will be executed.
95              
96             =head2 "WHEN"
97              
98             Is either a DateTime object or a scalar representing a iso8601 (a.k.a. Javascript date)
99              
100             Expension is supported so '2000', '2000-01', '2000-01-01' and '2000-01-01T00:00' all are equivalents to '2000-01-01T00:00:00'.
101             Timezones are supported and honored. Thus:
102              
103             time_activated
104             after '1999-12-31T23:00:00-01:00' => execute { print('Matches from 2000-01-01T00:00:00 GMT!') },
105             after '2000-01-01T00:00:00+01:00' => execute { print('Matches from 1999-01-01T23:00:00 GMT!') };
106              
107             C includes the exact time which is used as parameter, C does not.
108             Thus using C and C with the same time parameter ensures that only one statement gets executed.
109             i.e.:
110              
111             time_activated
112             before SOME_DATE => execute { print "Before!" },
113             after SOME_DATE => execute { print "After!" };
114              
115              
116             =head2 "WHAT"
117              
118             Is either an anonymous code block or a reference to subroutine
119             Code that will be executed on a given conditions in many ways:
120              
121             time_activated
122             after '2001' => execute \&my_great_new_feature; #No parameters can be passed with references...
123              
124             time_activated
125             after '2000' => execute { print 'Y2K ready!' },
126             after '2001' => execute (\&my_great_new_feature), #References with multilines need ()
127             after '2002' => execute { &my_great_new_feature("We need parameters by 2002")};
128              
129             =head2 CONSTANTS
130              
131             It is cool to use constants documenting both time and intent.
132              
133             use constants PROCESS_X_CUTOVER_DATE => '2017-01-01T00:00:00';
134              
135             time_activated after PROCESS_X_CUTOVER_DATE => execute { &new_business_process($some_state) };
136              
137             =cut
138              
139             =head1 TESTING
140              
141             L is your friend.
142              
143             use Test::More tests => 1;
144             use Time::Activated;
145             use Test::MockTime;
146              
147             Test::MockTime::set_absolute_time('1986-05-27T00:00:00Z');
148             time_activated after '1985-01-01T00:00:00-03:00' => execute { pass('Basic after') }; # this gets executed
149              
150             Test::MockTime::set_absolute_time('1984-05-27T00:00:00Z');
151             time_activated after '1985-01-01T00:00:00-03:00' => execute { fail('Basic after') }; # this does not get executed
152              
153             =cut
154              
155 9     9   30 use Carp;
  9         12  
  9         564  
156             $Carp::Internal{ __PACKAGE__ }++;
157              
158 9     9   2886 use Sub::Name 0.08;
  9         2979  
  9         341  
159 9     9   5579 use DateTime;
  9         676164  
  9         264  
160 9     9   3742 use DateTime::Format::ISO8601;
  9         249722  
  9         4227  
161              
162             =head1 SUBROUTINES/METHODS
163              
164             =head2 time_activated
165              
166             C is both the syntactical placeholder for gramar in C and the internal implementation of the modules functionality.
167              
168             Syntactically the structure is like so (note the ','s and ';'):
169              
170             time_activated
171             after ..., execute ...,
172             before ..., execute ...,
173             between ..., ... execute ...;
174              
175             Alternatively some can be changed for a => for a fancy syntax. This abuses anonymous hashes, some inteligent selections of prototypes (stolen from L) and probably
176             other clever perl-ish syntactical elements that escape my understanding. Note '=>'s, ','s and ';':
177              
178             time_activated
179             after ... => execute ...,
180             before ... => execute ...,
181             between ... => ... => execute ...; #Given. This does not look so fancy but more into the weird side...
182              
183             =cut
184              
185             # Blatantly stolen from Try::Tiny since it really makes sence and changing it produces headaches.
186             # Need to prototype as @ not $$ because of the way Perl evaluates the prototype.
187             # Keeping it at $$ means you only ever get 1 sub because we need to eval in a list
188             # context & not a scalar one
189              
190             sub time_activated (@) {
191 49     49 1 76 my (@stanzas) = @_;
192 49         42 my $activations = 0;
193              
194 49         111 my $now = DateTime->now();
195 49         6973 foreach my $stanza (@stanzas) {
196 99 100       612 if (ref($stanza) eq 'Time::Activated::Before') {
    100          
    50          
197 26 100       72 if ($now < $stanza->{before}) {
198 16         1531 $stanza->{code}();
199 16         4009 $activations++;
200             }
201             } elsif (ref($stanza) eq 'Time::Activated::After') {
202 41 100       267 if ($now >= $stanza->{after}) {
203 33         3533 $stanza->{code}();
204 33         6760 $activations++;
205             }
206             } elsif (ref($stanza) eq 'Time::Activated::Between') {
207 32 100       94 if ($stanza->{after} > $stanza->{before}) {
208 11         442 my $before = $stanza->{after};
209 11         12 $stanza->{after} = $stanza->{before};
210 11         12 $stanza->{before} = $before;
211             }
212 32 100 66     926 if ($now >= $stanza->{after} && $now <= $stanza->{before}) {
213 26         5146 $stanza->{code}();
214 26         6706 $activations++;
215             };
216             } else {
217 0 0       0 croak('time_activated() encountered an unexpected argument (' . ( defined $stanza ? $stanza : 'undef' ) . ') - perhaps a missing semi-colon?' );
218             }
219             }
220 49         2013 return $activations;
221             }
222              
223             =head2 before
224              
225             C defines a point in time before B which code is executed.
226              
227             This does not happen before January 1st 2018 at 00:00 but does happen from that exact point in time and on.
228              
229             time_activated
230             before '2018', execute { print "We are awaiting for 1/1/2018..." };
231              
232             Another fancy way to say do not do that before January 1st 2018 at 00:00.
233              
234             ime_activated
235             before '2018' => execute { print "We are awaiting for 1/1/2018..." };
236              
237             A fancy way to combine before statements.
238              
239             time_activated
240             before '2018' => execute { print "We are awaiting for 1/1/2018..." },
241             before '2019' => execute { print "Not quite there for 1/1/2019..." };
242              
243             =cut
244              
245             sub before ($$;@) {
246 26     26 1 39 my ( $before, $block, @rest ) = @_;
247              
248 26 50       54 croak 'Useless bare before()' unless wantarray;
249              
250 26         30 my $caller = caller;
251 26         125 subname("${caller}::before {...} " => $block);
252              
253 26         41 return (bless({before => _spawn_dt($before), code => $block},'Time::Activated::Before'), @rest);
254             }
255              
256             =head2 after
257              
258             C defines a point in time after B which code is executed.
259              
260             time_activated
261             after '2018' => execute { print "Wea are either at 1/1/2018 or after it..." };
262              
263             As with C statements can be combined with C, C and C with no limit.
264              
265             =cut
266              
267             sub after ($$;@) {
268 41     41 1 61 my ( $after, $block, @rest ) = @_;
269              
270 41 50       80 croak 'Useless bare after()' unless wantarray;
271              
272 41         50 my $caller = caller;
273 41         171 subname("${caller}::after {...} " => $block);
274              
275 41         66 return (bless({after => _spawn_dt($after), code => $block},'Time::Activated::After'), @rest);
276             }
277              
278             =head2 between
279              
280             C defines two points in time between which code is executes B.
281              
282             time_activated
283             between '2018' => '2018-12-31T23:59:59' => execute { print "This is 2018..." };
284              
285             As with C statements can be combined with C, C and C with no limit.
286              
287             =cut
288              
289             sub between ($$$;@) {
290 32     32 1 45 my ( $after, $before, $block, @rest ) = @_;
291              
292 32 50       74 croak 'Useless bare between()' unless wantarray;
293              
294 32         49 my $caller = caller;
295 32         169 subname("${caller}::between {...} " => $block);
296              
297 32         48 return (bless({before => _spawn_dt($before), after => _spawn_dt($after), code => $block},'Time::Activated::Between'), @rest);
298             }
299              
300             =head2 execute
301              
302             Exists for the sole reason of verbosity.
303             Accepts a single parameters that must be a subroutine or anonymous code block.
304             i
305             execute { print "This is a verbose way of saying that this will be executed!" };
306              
307             =cut
308              
309             sub execute(&) {
310 99     99 1 72286 my ($code) = @_;
311 99         276 return $code;
312             }
313              
314             =head2 PRIVATES
315              
316             =head3 _spawn_dt
317              
318             C<_spawn_dt> is a private function defined in hopes that additional date formats can be used to define points in time.
319             Currently supported formtats for all date time.
320              
321             =cut
322              
323             sub _spawn_dt {
324 131     131   122 my ($iso8601_or_datetime) = @_;
325              
326 131 100 66     546 my $dt = ref $iso8601_or_datetime && $iso8601_or_datetime->isa('DateTime')
327             ? $iso8601_or_datetime
328             : DateTime::Format::ISO8601->parse_datetime($iso8601_or_datetime);
329              
330 131         28547 return $dt;
331             }
332              
333             1;
334              
335             __END__