File Coverage

blib/lib/Evented/Object/Collection.pm
Criterion Covered Total %
statement 128 147 87.0
branch 45 68 66.1
condition 12 25 48.0
subroutine 14 15 93.3
pod 2 4 50.0
total 201 259 77.6


line stmt bran cond sub pod time code
1             # Copyright (c) 2011-17, Mitchell Cooper
2             #
3             # Evented::Object: a simple yet featureful base class event framework.
4             # https://github.com/cooper/evented-object
5             #
6             package Evented::Object::Collection; # leave this package name the same FOREVER.
7              
8 13     13   95 use warnings;
  13         24  
  13         433  
9 13     13   66 use strict;
  13         24  
  13         229  
10 13     13   59 use utf8;
  13         24  
  13         74  
11 13     13   391 use 5.010;
  13         45  
12              
13 13     13   118 use Scalar::Util qw(weaken blessed);
  13         43  
  13         791  
14 13     13   96 use List::Util qw(min max);
  13         37  
  13         1453  
15 13     13   92 use Carp qw(carp);
  13         24  
  13         23631  
16              
17             our $VERSION = '5.67';
18             our $events = $Evented::Object::events;
19             our $props = $Evented::Object::props;
20              
21             my $dummy;
22             my %boolopts = map { $_ => 1 } qw(safe return_check fail_continue);
23              
24             sub new {
25 13     13 0 109 return bless {
26             pending => {},
27             default_names => {},
28             names => {}
29             }, shift;
30             }
31              
32             sub push_callbacks {
33 13     13 0 43 my ($collection, $callbacks, $names) = @_;
34 13         82 my $pending = $collection->{pending};
35 13         32 my $my_names = $collection->{names};
36              
37             # add to pending callbacks and callback name-to-ID mapping.
38 13         105 @$pending { keys %$callbacks } = values %$callbacks;
39 13         74 @$my_names{ keys %$names } = values %$names;
40              
41             # set default names for any callback names which were not found
42             $collection->{default_names}{ $_->[2]{name} } ||= $_->[2]{id}
43 13   33     291 for values %$callbacks;
44             }
45              
46             #
47             # Available fire options
48             # ----------------------
49             #
50             # safe calls all callbacks within eval blocks.
51             # consumes no parameter.
52             #
53             # return_check causes the event to ->stop if any callback returns false
54             # BUT IT WAITS until all have been fired. so if one returns false,
55             # the rest will be called, but $fire->stopper will be true afterward.
56             # consumes no parameter.
57             #
58             # caller specify an alternate [caller 1] value, mostly for internal use.
59             # parameter = caller(1) info wrapped in an array reference.
60             #
61             # fail_continue if 'safe' is enabled and a callback raises an exception, it will
62             # by default ->stop the fire. this option tells it to continue instead.
63             # consumes no parameter.
64             #
65             # data some data to fire with the event. esp. good for things that might be
66             # useful at times but not accessed frequently enough to be an argument.
67             # parameter = the data.
68             #
69             sub fire {
70 13     13 1 59 my ($collection, @options) = @_;
71              
72             # handle options.
73 13         114 my ($caller, $data) = $collection->{caller};
74 13         71 while (@options) {
75 13         41 my $opt = shift @options;
76              
77 13 100       53 if ($opt eq 'caller') { $caller = shift @options } # custom caller
  11         25  
78 13 100       79 if ($opt eq 'data') { $data = shift @options } # fire data
  2         6  
79              
80             # boolean option.
81 13 50       80 $collection->{$opt} = 1 if $boolopts{$opt};
82              
83             }
84              
85             # create fire object.
86 13   100     262 my $fire = Evented::Object::EventFire->new(
87             caller => $caller ||= [caller 1], # $fire->caller
88             data => $data, # $fire->data
89             collection => $collection
90             );
91              
92             # if it hasn't been sorted, do so now.
93 13 50       101 $collection->sort if not $collection->{sorted};
94 13 50       59 my $callbacks = $collection->{sorted} or return $fire;
95              
96             # if return_check is enabled, add a callback to be fired last that will
97             # check the return values. this is basically hackery using a dummy object.
98 13 50       55 if ($collection->{return_check}) {
99 0         0 my $cb = {
100             name => 'eventedObject.returnCheck',
101             caller => $caller,
102             code => \&_return_check
103             };
104 0   0     0 my $group = [
105             $dummy ||= Evented::Object->new,
106             'returnCheck',
107             [],
108             "$dummy/returnCheck"
109             ];
110 0         0 push @$callbacks, [
111             -inf, # [0] $priority
112             $group, # [1] $group
113             $cb # [2] $cb
114             ];
115 0         0 $cb->{id} = "$$group[3]/$$cb{name}";
116 0         0 $collection->{pending}{ $cb->{id} } = $cb;
117             }
118              
119             # call them.
120 13         60 return $collection->_call_callbacks($fire);
121              
122             }
123              
124             # sorts the callbacks, trying its best to listen to before and after.
125             sub sort : method {
126 13     13 1 105 my $collection = shift;
127 13 50       77 return unless $collection->{pending};
128 13         28 my %callbacks = %{ $collection->{pending} };
  13         79  
129 13         39 my (@sorted, %done, %waited);
130              
131             # iterate over the callback sets,
132             # which are array refs of [ priority, group, cb ]
133 13         45 my @callbacks = values %callbacks;
134 13         65 while (my $set = shift @callbacks) {
135 37         94 my ($priority, $group, $cb) = @$set;
136 37         75 my $cb_id = $cb->{id};
137 37         58 my $group_id = $group->[3];
138              
139 37 50       95 next if $done{$cb_id};
140              
141             # a real priority exists already.
142 37 100 66     227 if (defined $priority && $priority ne 'nan') {
143 22         43 push @sorted, $set;
144 22         45 $done{$cb_id} = 1;
145 22         70 next;
146             }
147              
148              
149             # TODO: if before and afters cannot be resolved, the callback dependencies
150             # are currently skipped. maybe there should be a way to specify that a callback
151             # dependency is REQUIRED, meaning to skip the callback entirely if it cannot
152             # be done. or maybe something more sophisticated that can prioritize the
153             # befores and afters in this way. for now though, we will just try to not
154             # specify impossible befores and afters.
155              
156              
157              
158             # callback priority determination can be postponed until another's
159             # priority is determined. the maxmium number of times one particular
160             # callback can be postponed is the number of total callbacks.
161 15         28 my $wait_max = keys %callbacks;
162              
163 15         36 my $name_to_id = $collection->_group_names($group_id);
164             my $get_befores_afters = sub {
165 25     25   46 my ($key, @results) = shift;
166 25 100       60 my $list = $cb->{$key} or return;
167 19 100       50 $list = [ $list ] if ref $list ne 'ARRAY';
168              
169             # for each callback name, find its priority.
170 19         38 foreach my $their_name (@$list) {
171              
172             # map callback name to id, id to cbref, and cbref to priority.
173 29 100       64 my $their_id = $name_to_id->{$their_name} or next;
174 17 50       39 my $their_cb = $callbacks{$their_id} or next;
175 17         27 my $their_p = $their_cb->[0];
176              
177             # if their priority is nan,
178             # we have to wait until it is determined.
179 17 100       42 if ($their_p eq 'nan') {
180 10         23 my $wait_key = "$cb_id $their_id";
181             push @callbacks, $set
182 10 100       40 unless $waited{$key}++ > $wait_max;
183 10         28 return 1;
184             }
185              
186 7         18 push @results, $their_p;
187             }
188              
189 9         33 return (undef, @results);
190 15         65 };
191              
192 15 100       33 my ($next, @befores) = $get_befores_afters->('before'); next if $next;
  15         56  
193 10 100       18 ($next, my @afters ) = $get_befores_afters->('after'); next if $next;
  10         57  
194              
195             # figure the ideal priority.
196 5 50 66     26 if (@befores && @afters) {
    100          
    50          
197 0         0 my $a_refpoint = min @afters;
198 0         0 my $b_refpoint = max @befores;
199 0         0 $priority = ($a_refpoint + $b_refpoint) / 2;
200             }
201              
202             # only before. just have 1 higher priority.
203             elsif (@befores) {
204 3         18 my $refpoint = max @befores;
205 3         16 $priority = ++$refpoint;
206             }
207              
208             # only after.
209             elsif (@afters) {
210 2         27 my $refpoint = min @afters;
211 2         7 $priority = --$refpoint;
212             }
213              
214 5 50       27 $priority = 0 if $priority eq 'nan';
215              
216             # done with this callback.
217 5         11 $set->[0] = $priority;
218 5         9 push @sorted, $set;
219 5         40 $done{$cb_id} = 1;
220              
221             }
222              
223             # the final sort by numerical priority.
224 13         79 $collection->{sorted} = [ sort { $b->[0] <=> $a->[0] } @sorted ];
  24         86  
225              
226             }
227              
228             # Nov. 22, 2013 revision
229             # ----------------------
230             #
231             # collection a set of callbacks about to be fired. they might belong to multiple
232             # objects or maybe even multiple events. they can each have their own
233             # arguments, and they all have their own options, code references, etc.
234             #
235             # group represents the group to which a callback belongs. a group consists of
236             # the associated evented object, event name, and arguments.
237             #
238             # This revision eliminates all of these nested structures by reworking the way
239             # a callback collection works. A collection should be an array of callbacks.
240             # This array, unlike before, will contain an additional element: an array
241             # reference representing the "group."
242             #
243             # @collection = (
244             # [ $priority, $group, $cb ],
245             # [ $priority, $group, $cb ],
246             # ...
247             # )
248             #
249             # $group = $cb =
250             # [ $eo, $event_name, $args, $id ] { code, caller, %opts }
251             #
252             # This format has several major advantages over the former one. Specifically,
253             # it makes it very simple to determine which callbacks will be called in the
254             # future, which ones have been called already, how many are left, etc.
255             #
256              
257             # call the passed callback priority sets.
258             sub _call_callbacks {
259 13     13   44 my ($collection, $fire) = @_;
260 13         71 my $ef_props = $fire->{$props};
261              
262             # store the collection.
263 13 50       84 my $remaining = $collection->{sorted} or return;
264 13         39 $ef_props->{collection} = $collection;
265              
266             # call each callback.
267 13         52 while (my $entry = shift @$remaining) {
268 26         68 my ($priority, $group, $cb) = @$entry;
269 26         68 my ($eo, $event_name, $args, $group_id) = @$group;
270              
271             # sanity check!
272 26 50 33     217 blessed $eo && $eo->isa('Evented::Object') or return;
273              
274             # callback name-to-ID mapping is specific to each group.
275 26         109 $ef_props->{callback_ids} = $collection->_group_names($group_id);
276              
277             # increment the callback counter.
278 26         51 $ef_props->{callback_i}++;
279              
280             # set the evented object of this callback.
281             # set the event name of this callback.
282 26         52 $ef_props->{object} = $eo; weaken($ef_props->{object}); # $fire->object
  26         100  
283 26         48 $ef_props->{name} = $event_name; # $fire->event_name
284              
285             # store identifiers.
286 26         75 $ef_props->{callback_id} = my $cb_id = $cb->{id};
287 26         47 $ef_props->{group_id} = $group_id;
288              
289             # create info about the call.
290 26         49 $ef_props->{callback_name} = $cb->{name}; # $fire->callback_name
291 26         54 $ef_props->{callback_priority} = $priority; # $fire->callback_priority
292 26 100       73 $ef_props->{callback_data} = $cb->{data} if defined $cb->{data}; # $fire->callback_data
293              
294             # this callback has been called already.
295 26 50       101 next if $ef_props->{called}{$cb_id};
296              
297             # this callback has probably been cancelled.
298 26 100       91 next unless $collection->{pending}{$cb_id};
299              
300              
301             # determine arguments.
302             #
303             # no compat <3.0: used to always have obj unless specified with no_obj or later no_fire_obj.
304             # no compat <2.9: with_obj -> eo_obj
305             # compat: all later version had a variety of with_obj-like-options below.
306             #
307 25         55 my @cb_args = @$args;
308 25         91 my $include_obj = grep $cb->{$_}, qw(with_eo with_obj with_evented_obj eo_obj);
309 25 50       99 unshift @cb_args, $fire unless $cb->{no_fire_obj};
310 25 50       61 unshift @cb_args, $eo if $include_obj;
311              
312             # set return values.
313             $ef_props->{last_return} = # set last return value.
314             $ef_props->{return}{$cb_id} = # set this callback's return value.
315              
316             # call the callback with proper arguments.
317 0         0 $collection->{safe} ? eval { $cb->{code}(@cb_args) }
318 25 50       110 : $cb->{code}(@cb_args);
319              
320             # set $fire->called($cb) true, and set $fire->last to the callback's name.
321 25         5048 $ef_props->{called}{$cb_id} = 1;
322 25         60 $ef_props->{last_callback} = $cb->{name};
323              
324             # no longer pending.
325 25         55 delete $collection->{pending}{$cb_id};
326              
327             # stop if eval failed.
328 25 50 33     92 if ($collection->{safe} and my $err = $@) {
329 0         0 chomp $err;
330             $ef_props->{error}{$cb_id} = # not used for anything
331 0         0 $ef_props->{exception} = $err;
332 0 0       0 $fire->stop($err) unless $collection->{fail_continue};
333             }
334              
335             # if stop is true, $fire->stop was called. stop the iteration.
336 25 100       142 if ($ef_props->{stop}) {
337 1         3 $ef_props->{stopper} = $cb->{name}; # set $fire->stopper.
338 1         3 last;
339             }
340              
341             }
342              
343             # dispose of things that are no longer needed.
344 13         77 delete @$ef_props{ qw(
345             callback_name callback_priority
346             callback_data callback_i object
347             collection
348             ) };
349              
350             # return the event object.
351 13         30 $ef_props->{complete} = 1;
352 13         133 return $fire;
353              
354             }
355              
356             sub _group_names {
357 41     41   91 my ($collection, $group_id) = @_;
358 41   66     146 return $collection->{group_names}{$group_id} ||= do {
359 14   50     54 my $names_from_group = $collection->{names}{$group_id} || {};
360 14         28 my $default_names = $collection->{default_names};
361 14         120 my %names = (%$default_names, %$names_from_group);
362 14         79 \%names
363             }
364             }
365              
366             sub _return_check {
367 0     0     my $fire = shift;
368 0 0         my %returns = %{ $fire->{$props}{return} || {} };
  0            
369 0           foreach my $cb_id (keys %returns) {
370 0 0         next if $returns{$cb_id};
371 0           return $fire->stop("$cb_id returned false with return_check enabled");
372             }
373 0           return 1;
374             }
375              
376             1;
377              
378             =head1 NAME
379              
380             B - represents a group of pending
381             L callbacks.
382              
383             =head1 DESCRIPTION
384              
385             L are returned by the evented object 'prepare' methods. They
386             represent a group of callbacks that are about to be fired. Using collections
387             allows you to prepare a fire ahead of time before executing it. You can also
388             fire events with special options this way.
389              
390             =head1 METHODS
391              
392              
393             =head2 $col->fire(@options)
394              
395             Fires the pending callbacks with the specified options, if any. If the callbacks
396             have not yet been sorted, they are sorted before the event is fired.
397              
398             $eo->prepare(some_event => @arguments)->fire('safe');
399              
400             B
401              
402             =over
403              
404             =item *
405              
406             B<@options> - I, a mixture of boolean and key:value options for the
407             event fire.
408              
409             =back
410              
411             B<@options> - fire options
412              
413             =over
414              
415             =item *
416              
417             B - I, use an alternate C<[caller 1]> value for the event
418             fire. This is typically only used internally.
419              
420             =item *
421              
422             B - I, if true, the event will yield that it was stopped
423             if any of the callbacks return a false value. Note however that if one callbacks
424             returns false, the rest will still be called. The fire object will only yield
425             stopped status after all callbacks have been called and any number of them
426             returned false.
427              
428             =item *
429              
430             B - I, wrap all callback calls in C for safety. if any of
431             them fail, the event will be stopped at that point with the error.
432              
433             =item *
434              
435             B - I, if C above is enabled, this tells the fire
436             to continue even if one of the callbacks fails. This could be dangerous if any
437             of the callbacks expected a previous callback to be done when it actually
438             failed.
439              
440             =item *
441              
442             B - I, a scalar value that can be fetched by
443             C<< $fire->data >> from within the callbacks. Good for data that might be useful
444             sometimes but not frequently enough to deserve a spot in the argument list. If
445             C is a hash reference, its values can be fetched conveniently with
446             C<< $fire->data('key') >>.
447              
448             =back
449              
450             =head2 $col->sort
451              
452             Sorts the callbacks according to C, C, and C options.
453              
454             =head1 AUTHOR
455              
456             L
457              
458             Copyright E 2011-2017. Released under New BSD license.
459              
460             Comments, complaints, and recommendations are accepted. Bugs may be reported on
461             L.