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   96 use warnings;
  13         26  
  13         436  
9 13     13   69 use strict;
  13         25  
  13         233  
10 13     13   59 use utf8;
  13         26  
  13         77  
11 13     13   388 use 5.010;
  13         43  
12              
13 13     13   110 use Scalar::Util qw(weaken blessed);
  13         50  
  13         820  
14 13     13   109 use List::Util qw(min max);
  13         29  
  13         1513  
15 13     13   88 use Carp qw(carp);
  13         28  
  13         23399  
16              
17             our $VERSION = '5.68';
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 120 return bless {
26             pending => {},
27             default_names => {},
28             names => {}
29             }, shift;
30             }
31              
32             sub push_callbacks {
33 13     13 0 45 my ($collection, $callbacks, $names) = @_;
34 13         72 my $pending = $collection->{pending};
35 13         39 my $my_names = $collection->{names};
36              
37             # add to pending callbacks and callback name-to-ID mapping.
38 13         78 @$pending { keys %$callbacks } = values %$callbacks;
39 13         58 @$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     255 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 49 my ($collection, @options) = @_;
71              
72             # handle options.
73 13         115 my ($caller, $data) = $collection->{caller};
74 13         74 while (@options) {
75 13         39 my $opt = shift @options;
76              
77 13 100       50 if ($opt eq 'caller') { $caller = shift @options } # custom caller
  11         25  
78 13 100       83 if ($opt eq 'data') { $data = shift @options } # fire data
  2         10  
79              
80             # boolean option.
81 13 50       87 $collection->{$opt} = 1 if $boolopts{$opt};
82              
83             }
84              
85             # create fire object.
86 13   100     198 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       114 $collection->sort if not $collection->{sorted};
94 13 50       64 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       59 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         70 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 101 my $collection = shift;
127 13 50       76 return unless $collection->{pending};
128 13         31 my %callbacks = %{ $collection->{pending} };
  13         83  
129 13         43 my (@sorted, %done, %waited);
130              
131             # iterate over the callback sets,
132             # which are array refs of [ priority, group, cb ]
133 13         44 my @callbacks = values %callbacks;
134 13         69 while (my $set = shift @callbacks) {
135 36         89 my ($priority, $group, $cb) = @$set;
136 36         69 my $cb_id = $cb->{id};
137 36         62 my $group_id = $group->[3];
138              
139 36 50       102 next if $done{$cb_id};
140              
141             # a real priority exists already.
142 36 100 66     169 if (defined $priority && $priority ne 'nan') {
143 22         49 push @sorted, $set;
144 22         45 $done{$cb_id} = 1;
145 22         74 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 14         28 my $wait_max = keys %callbacks;
162              
163 14         36 my $name_to_id = $collection->_group_names($group_id);
164             my $get_befores_afters = sub {
165 24     24   38 my ($key, @results) = shift;
166 24 100       63 my $list = $cb->{$key} or return;
167 18 100       50 $list = [ $list ] if ref $list ne 'ARRAY';
168              
169             # for each callback name, find its priority.
170 18         32 foreach my $their_name (@$list) {
171              
172             # map callback name to id, id to cbref, and cbref to priority.
173 28 100       63 my $their_id = $name_to_id->{$their_name} or next;
174 16 50       50 my $their_cb = $callbacks{$their_id} or next;
175 16         32 my $their_p = $their_cb->[0];
176              
177             # if their priority is nan,
178             # we have to wait until it is determined.
179 16 100       30 if ($their_p eq 'nan') {
180 9         25 my $wait_key = "$cb_id $their_id";
181             push @callbacks, $set
182 9 100       42 unless $waited{$key}++ > $wait_max;
183 9         23 return 1;
184             }
185              
186 7         23 push @results, $their_p;
187             }
188              
189 9         30 return (undef, @results);
190 14         65 };
191              
192 14 100       33 my ($next, @befores) = $get_befores_afters->('before'); next if $next;
  14         55  
193 10 100       19 ($next, my @afters ) = $get_befores_afters->('after'); next if $next;
  10         50  
194              
195             # figure the ideal priority.
196 5 50 66     34 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         33 my $refpoint = max @befores;
205 3         10 $priority = ++$refpoint;
206             }
207              
208             # only after.
209             elsif (@afters) {
210 2         15 my $refpoint = min @afters;
211 2         7 $priority = --$refpoint;
212             }
213              
214 5 50       36 $priority = 0 if $priority eq 'nan';
215              
216             # done with this callback.
217 5         12 $set->[0] = $priority;
218 5         11 push @sorted, $set;
219 5         47 $done{$cb_id} = 1;
220              
221             }
222              
223             # the final sort by numerical priority.
224 13         90 $collection->{sorted} = [ sort { $b->[0] <=> $a->[0] } @sorted ];
  23         85  
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   43 my ($collection, $fire) = @_;
260 13         94 my $ef_props = $fire->{$props};
261              
262             # store the collection.
263 13 50       75 my $remaining = $collection->{sorted} or return;
264 13         37 $ef_props->{collection} = $collection;
265              
266             # call each callback.
267 13         49 while (my $entry = shift @$remaining) {
268 26         72 my ($priority, $group, $cb) = @$entry;
269 26         64 my ($eo, $event_name, $args, $group_id) = @$group;
270              
271             # sanity check!
272 26 50 33     218 blessed $eo && $eo->isa('Evented::Object') or return;
273              
274             # callback name-to-ID mapping is specific to each group.
275 26         116 $ef_props->{callback_ids} = $collection->_group_names($group_id);
276              
277             # increment the callback counter.
278 26         48 $ef_props->{callback_i}++;
279              
280             # set the evented object of this callback.
281             # set the event name of this callback.
282 26         55 $ef_props->{object} = $eo; weaken($ef_props->{object}); # $fire->object
  26         115  
283 26         50 $ef_props->{name} = $event_name; # $fire->event_name
284              
285             # store identifiers.
286 26         61 $ef_props->{callback_id} = my $cb_id = $cb->{id};
287 26         60 $ef_props->{group_id} = $group_id;
288              
289             # create info about the call.
290 26         52 $ef_props->{callback_name} = $cb->{name}; # $fire->callback_name
291 26         53 $ef_props->{callback_priority} = $priority; # $fire->callback_priority
292 26 100       66 $ef_props->{callback_data} = $cb->{data} if defined $cb->{data}; # $fire->callback_data
293              
294             # this callback has been called already.
295 26 50       110 next if $ef_props->{called}{$cb_id};
296              
297             # this callback has probably been cancelled.
298 26 100       92 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         60 my @cb_args = @$args;
308 25         90 my $include_obj = grep $cb->{$_}, qw(with_eo with_obj with_evented_obj eo_obj);
309 25 50       119 unshift @cb_args, $fire unless $cb->{no_fire_obj};
310 25 50       66 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       122 : $cb->{code}(@cb_args);
319              
320             # set $fire->called($cb) true, and set $fire->last to the callback's name.
321 25         4321 $ef_props->{called}{$cb_id} = 1;
322 25         63 $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     96 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       157 if ($ef_props->{stop}) {
337 1         4 $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         81 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         31 $ef_props->{complete} = 1;
352 13         142 return $fire;
353              
354             }
355              
356             sub _group_names {
357 40     40   92 my ($collection, $group_id) = @_;
358 40   66     147 return $collection->{group_names}{$group_id} ||= do {
359 14   50     63 my $names_from_group = $collection->{names}{$group_id} || {};
360 14         35 my $default_names = $collection->{default_names};
361 14         106 my %names = (%$default_names, %$names_from_group);
362 14         86 \%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.