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 0 4 0.0
total 199 259 76.8


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