File Coverage

blib/lib/Aspect/Library/Listenable.pm
Criterion Covered Total %
statement 96 100 96.0
branch 21 32 65.6
condition 11 18 61.1
subroutine 21 21 100.0
pod 0 12 0.0
total 149 183 81.4


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