File Coverage

blib/lib/Aspect/Library/Listenable.pm
Criterion Covered Total %
statement 99 103 96.1
branch 21 32 65.6
condition 11 18 61.1
subroutine 22 22 100.0
pod 0 12 0.0
total 153 187 81.8


line stmt bran cond sub pod time code
1             package Aspect::Library::Listenable;
2              
3             # TODO: update docs to explain always_fire, setting handler params
4             # specifically instead of getting default (event), and event cloning
5              
6 2     2   1444 use strict;
  2         5  
  2         72  
7 2     2   10 use warnings;
  2         3  
  2         67  
8 2     2   9 use Carp ();
  2         3  
  2         28  
9 2     2   9 use Scalar::Util ();
  2         3  
  2         30  
10 2     2   8 use Sub::Install ();
  2         3  
  2         22  
11 2     2   769 use Aspect::Modular ();
  2         4  
  2         38  
12 2     2   10 use Aspect::Advice::Before ();
  2         3  
  2         25  
13 2     2   834 use Aspect::Library::Listenable::Event ();
  2         4  
  2         2350  
14              
15             our $VERSION = '0.97_06';
16             our @ISA = 'Aspect::Modular';
17              
18             sub import {
19 2     2   25 my $into = caller();
20              
21             Sub::Install::install_sub( {
22             code => $_,
23             into => $into,
24 2         60 } ) foreach qw{
25             add_listener
26             remove_listener
27             };
28              
29 2         4285 return 1;
30             }
31              
32             sub get_advice {
33 3     3 0 12 my ($self, $event_name, $pointcut, %event_params) = @_;
34             Aspect::Advice::Before->new(
35             lexical => $self->lexical,
36             pointcut => $pointcut,
37             code => sub {
38 7     7   7 my $context = $_;
39 7         20 my $listenable = $context->self;
40 7         13 my %params = %event_params;
41              
42 7         8 local $_;
43 7 50       10 return unless has_listeners($listenable, $event_name);
44              
45 7         11 my $always_fire = delete $params{__always_fire};
46 7         10 my %old_state = get_listenable_state($listenable, \%params);
47 7         26 $context->original->( $context->args );
48 7         17 my %new_state = get_listenable_state($listenable, \%params);
49              
50             return if
51 7 100 66     39 !$always_fire &&
      100        
52             keys %old_state &&
53             is_equal_state(\%old_state, \%new_state);
54              
55 6         12 my @params = $context->args;
56 6         6 shift @params; # remove $self
57             my $event = Aspect::Library::Listenable::Event->new(
58             name => $event_name,
59             source => $listenable,
60             params => \@params,
61             %new_state,
62 6         23 map {("old_$_" => $old_state{$_})} keys %old_state,
  1         4  
63             );
64              
65 6         11 fire_event($event);
66             },
67 3         49 );
68             }
69              
70             sub add_listener ($$$) {
71 11     11 0 773 my ($listenable, $event_name, $listener) = @_;
72 11 100       16 Carp::croak "listenable is not a hash based object: [$listenable]"
73             unless is_hash($listenable);
74 10         16 my $key = get_listener_key($event_name);
75 10 100       25 $listenable->{$key} = [] unless exists $listenable->{$key};
76 10         15 my $listeners = get_listeners($listenable, $event_name);
77 10         9 my $lastIndex = (push @$listeners, $listener) - 1;
78 10 100       35 if ( ref $listener eq 'ARRAY' ) { # type 3 listener
    50          
79 1         5 Scalar::Util::weaken( $listeners->[$lastIndex]->[1] );
80             } elsif ( ref $listener ne 'CODE' ) { # type 2 listener
81 0         0 Scalar::Util::weaken( $listeners->[$lastIndex] );
82             }
83             }
84              
85             sub remove_listener ($$$) {
86 10     10 0 3318 my ($listenable, $event_name, $listener) = @_;
87 10         13 my $listeners = get_listeners($listenable, $event_name);
88 10 50       16 Carp::croak "listenable has no listeners for event: [$event_name]"
89             unless $listeners;
90 10         7 my $oldSize = @$listeners;
91 10         33 foreach my $i (0..@$listeners - 1) {
92 10         7 my $l = $listeners->[$i];
93 10 50 66     67 if ((ref $l eq 'ARRAY' && $listener eq $l->[1]) || $listener eq $l) {
      66        
94 10         11 splice @$listeners, $i, 1;
95 10         11 last;
96             }
97             }
98 10 50       81 Carp::croak "listener not found: [$event_name, $listener]"
99             if $oldSize == @$listeners;
100             }
101              
102             # private helpers -------------------------------------------------------------
103              
104             sub fire_event {
105 6     6 0 8 my $event = shift;
106 6         27 my ($source, $event_name) = ($event->source, $event->name);
107 6 50       11 return unless has_listeners($source, $event_name);
108 6         3 notify_listener($_, $event) for @{get_listeners($source, $event_name)};
  6         7  
109             }
110              
111             sub notify_listener {
112 7     7 0 9 my ($listener, $event) = @_;
113 7         7 local $_;
114 7         13 my $clone = $event->clone;
115 7         6 my $method_name;
116 7 100       15 if (ref $listener eq 'CODE') {
    50          
117 6         13 &$listener($clone);
118             } elsif (ref $listener eq 'ARRAY') {
119 1         2 my $o = $listener->[1];
120 1 50       3 return unless defined $o; # could be a dead weak ref
121 1         2 $method_name = $listener->[0];
122             my @params = $listener->[2]?
123 1 50       6 map { $event->$_ } @{$listener->[2]}:
  1         4  
  1         2  
124             ($clone);
125 1         7 $o->$method_name(@params);
126             } else {
127 0 0       0 return unless defined $listener; # could be a dead weak ref
128 0         0 $method_name = 'handle_event_'. $event->name;
129 0         0 $listener->$method_name($clone);
130             }
131             }
132              
133             sub has_listeners {
134 13     13 0 13 my ($listenable, $event_name) = @_;
135 13         13 my $listeners = get_listeners($listenable, $event_name);
136 13   33     56 return $listeners && @$listeners;
137             }
138              
139             sub get_listeners {
140 39     39 0 34 my ($listenable, $event_name) = @_;
141 39         36 return $listenable->{get_listener_key($event_name)};
142             }
143              
144 49     49 0 90 sub get_listener_key { '_'. __PACKAGE__. '_'. pop }
145              
146             sub get_listenable_state {
147 14     14 0 12 my ($listenable, $event_params) = @_;
148 14         12 local $_;
149             return map {
150 14         21 my $state_getter = $event_params->{$_};
  4         5  
151 4         8 $_ => $listenable->$state_getter;
152             } keys %$event_params
153             }
154              
155             sub is_equal_state {
156 2     2 0 4 my ($old_state, $new_state) = @_;
157 2         4 for my $key (keys %$new_state) {
158             return 0 unless
159 2 100       5 is_equal_value($old_state->{$key}, $new_state->{$key});
160             }
161 1         21 return 1;
162             }
163              
164             # TODO: need smarter ways to figure out equality
165             sub is_equal_value {
166 2     2 0 3 my ($old, $new) = @_;
167             return
168 2   33     33 (!defined $new && !defined $old) ||
169             (
170             defined $new && defined $old &&
171             !ref($old) && !ref($new) &&
172             $old eq $new
173             );
174             }
175              
176             sub is_hash {
177 11     11 0 47 shift =~ /=?([A-Z]+)\(/;
178 11         47 return $1 eq 'HASH';
179             }
180              
181             1;
182              
183             __END__