File Coverage

blib/lib/Async/Hooks.pm
Criterion Covered Total %
statement 32 32 100.0
branch 16 18 88.8
condition 9 11 81.8
subroutine 7 7 100.0
pod 3 3 100.0
total 67 71 94.3


line stmt bran cond sub pod time code
1             package Async::Hooks;
2             {
3             $Async::Hooks::VERSION = '0.16';
4             }
5              
6             # ABSTRACT: Hook system with asynchronous capabilities
7              
8 3     3   108705 use Mo qw(is default);
  3         1886  
  3         16  
9 3     3   13899 use Carp 'confess';
  3         7  
  3         224  
10 3     3   1849 use Async::Hooks::Ctl;
  3         8  
  3         79  
11 3     3   2632 use namespace::clean;
  3         71332  
  3         22  
12              
13              
14             has registry => (
15             is => 'ro',
16             default => sub { {} },
17             );
18              
19              
20             sub hook {
21 23     23 1 34288 my ($self, $hook, $cb) = @_;
22              
23 23 100       440 confess("Missing first parameter, the hook name, ") unless $hook;
24 21 100       341 confess("Missing second parameter, the coderef callback, ")
25             unless ref($cb) eq 'CODE';
26              
27 19   100     99 my $cbs = $self->{registry}{$hook} ||= [];
28 19         33 push @$cbs, $cb;
29              
30 19         43 return;
31             }
32              
33              
34             sub has_hooks_for {
35 6     6 1 2453 my ($self, $hook) = @_;
36              
37 6 50       18 confess("Missing first parameter, the hook name, ") unless $hook;
38              
39 6         10 my $reg = $self->{registry};
40 6 100       24 return 0 unless exists $reg->{$hook};
41 4         7 return scalar(@{$reg->{$hook}});
  4         24  
42             }
43              
44              
45             sub call {
46 22     22 1 39332 my ($self, $hook, $args, $cleanup) = @_;
47 22 50 33     98 ($args, $cleanup) = (undef, $args) if ref($args) eq 'CODE' && !$cleanup;
48              
49 22 100       189 confess("Missing first parameter, the hook name, ") unless $hook;
50 21 100 100     370 confess("Second parameter, the arguments list, must be a arrayref, ")
51             if $args && ref($args) ne 'ARRAY';
52 19 100 100     394 confess("Third parameter, the cleanup callback, must be a coderef, ")
53             if $cleanup && ref($cleanup) ne 'CODE';
54              
55 17         32 my $r = $self->{registry};
56 17 100       56 my $cbs = exists $r->{$hook} ? $r->{$hook} : [];
57              
58 17         108 return Async::Hooks::Ctl->new([@$cbs], $args, $cleanup)->next;
59             }
60              
61              
62             1; # End of Async::Hooks
63              
64              
65             __END__
66             =pod
67              
68             =head1 NAME
69              
70             Async::Hooks - Hook system with asynchronous capabilities
71              
72             =head1 VERSION
73              
74             version 0.16
75              
76             =head1 SYNOPSIS
77              
78             use Async::Hooks;
79              
80             my $nc = Async::Hooks->new;
81              
82             # Hook a callback on 'my_hook_name' chain
83             $nc->hook('my_hook_name', sub {
84             my ($ctl, $args) = @_;
85             my $url = $args->[0];
86              
87             # Async HTTP get, calls sub when it finishes
88             http_get($url, sub {
89             my ($data) = @_;
90              
91             return $ctl->done unless defined $data;
92              
93             # You can use unused places in $args as a stash
94             $args->[1] = $data;
95              
96             $ctl->next;
97             });
98             });
99              
100             $nc->hook('my_hook_name', sub {
101             my ($ctl, $args) = @_;
102              
103             # example transformation
104             $args->[1] =~ s/(</?)(\w+)/"$1".uc($2)/ge;
105              
106             $ctl->next;
107             });
108              
109             # call hook with arguments
110             $nc->call('my_hook_name', ['http://search.cpan.org/']);
111              
112             # call hook with arguments and cleanup
113             $nc->call('my_hook_name', ['http://search.cpan.org/'], sub {
114             my ($ctl, $args, $is_done) = @_;
115              
116             if (defined $args->[1]) {
117             print "Success!\n"
118             }
119             else {
120             print "Oops, could not retrieve URL $args->[0]\n";
121             }
122             });
123              
124             =head1 DESCRIPTION
125              
126             This module allows you to create hooks on your own modules that other
127             developers can use to extend your functionality, or just react to
128             important state modifications.
129              
130             There are other modules that provide the same functionality (see
131             L<SEE ALSO> section). The biggest diference is that you can pause
132             processing of the chain of callbacks at any point and start a
133             asynchronous network request, and resume processing when that request
134             completes.
135              
136             Developers are not expect to subclass from C<Async::Hooks>. The
137             recomended usage is to stick a C<Async::Hooks> instance in a slot or as
138             a singleton for your whole app, and then delegate some methods to it.
139              
140             For example, using L<Moose|Moose> you can just:
141              
142             has 'hooks' => (
143             isa => 'Async::Hooks',
144             is => 'ro',
145             default => sub { Async::Hooks->new },
146             lazy => 1,
147             handles => [qw( hook call )],
148             );
149              
150             There are two main usages for hooks: notification or delegation of
151             responsability.
152              
153             You can define hook points for notification of important events inside
154             your class. For example, if you where writting a feed aggregator, you
155             could define a hook for notification of new items.
156              
157             In some other cases, your module wants to make part of its bussiness
158             logic extendable or even replaceable. For example, a SMTP server can ask
159             if a specific mail address is a valid RCPT. All the registered callbacks
160             would be called and if one of them has a definitive answer she can just
161             stop the chain. You can even define a default callback to be called at
162             the end, as a cleanup step.
163              
164             You don't need to pre-declare or create a hook. Clients of your module
165             should consult your documentation to discover which hooks to you support
166             and then they should just call the C<hook()> method. It takes two
167             parameters: a scalar, the hook name, and a coderef, the callback.
168              
169             To call the hook chain, you use the C<call()> method. It requires a
170             scalar, the hook to call, as the first parameter. The second
171             optional parameter is an arrayref with arguments, or undef. The third
172             optional argument, a coderef, is a cleanup callback. This callback
173             will be called at the end of the chain or as soon as some callback
174             ends the chain.
175              
176             The callbacks all have a common signature. They receive two parameters.
177             The first one is a L<Async::Hooks::Ctl|Async::Hooks::Ctl> object, used
178             to control the chain of callbacks. The second is an arrayref with the
179             arguments you used when the hook was called. Something like this:
180              
181             sub my_callback {
182             my ($ctl, $args) = @_;
183             ....
184             }
185              
186             A third parameter is passed to the cleanup callback: a C<$is_done> flag,
187             with a true value if the chain was ended prematurely C<done()> or
188             C<stop()>.
189              
190             The callback only has one responsability: decide if you want to decline
191             processing of this event, or stop processing if we are done with it.
192             Cleanup callbacks I<MUST> just return.
193              
194             To do that, callbacks must call one of two methods:
195             C<< $ctl->decline() >> or C<< $ctl->done() >>. You can also use
196             C<next()> or C<declined()> as alias to C<decline()>, and C<stop()>
197             as alias to C<done()>, whatever feels better.
198              
199             But you can delay that decision. You can start a network request,
200             asynchronously, and only decide to decline or stop when the response
201             arrives. For example, if you use the L<AnyEvent::HTTP|AnyEvent::HTTP>
202             module to make a HTTP request, you could do something like this:
203              
204             sub check_server_is_up_cb {
205             my ($ctl, $args) = @_;
206             my ($url) = @$args;
207              
208             http_get($url, sub {
209             my ($data, $headers) = @_;
210              
211             if (defined $data) {
212             push @$args = $data;
213             return $ctl->done;
214             }
215              
216             return $ctl->next;
217             });
218             }
219              
220             In this example, we start a HTTP GET, and use a second callback to
221             process the result. If a sucessful result is found, we stop the chain.
222              
223             While the HTTP request is being made, your application can keep on
224             processing other tasks.
225              
226             =head1 METHODS
227              
228             =over 4
229              
230             =item $registry = Async::Hooks->new()
231              
232             Creates a L<Async::Hooks|Async::Hooks> object that acts as a registry
233             for hooks.
234              
235             You can have several object at the same time, independent of each other.
236              
237             =item $registry->hook($hook_name, \&cb);
238              
239             Register a callback with a specific hook.
240              
241             The callback will be called with two parameters: a
242             L<Async::Hooks::Ctl|Async::Hooks::Ctl> object and an arrayref with
243             arguments.
244              
245             =item $registry->call($hook_name [, \@args] [, \&cleanup])
246              
247             Calls a specific hook name chain. You can optionally provide an arrayref
248             with arguments that each callback will receive.
249              
250             The optional cleanup callback will be called at the end of the chain, or
251             when a callback calls C<< $ctl->done() >>.
252              
253             =item $count = $registry->has_hooks_for($hook);
254              
255             Returns the number of callbacks registered with C<$hook>.
256              
257             =back
258              
259             =head1 SEE ALSO
260              
261             There are a couple of modules that do similar things to this one:
262              
263             =over 4
264              
265             =item * L<Object::Event|Object::Event>
266              
267             =item * L<Class::Observable|Class::Observable>
268              
269             =item * L<Event::Notify|Event::Notify>
270              
271             =item * L<Notification::Center|Notification::Center>
272              
273             =back
274              
275             Of those four, only L<Object::Event|Object::Event> version 1.0 and later
276             provides the same ability to pause a chain, do some asynchrounous work
277             and resume chain processing later.
278              
279             =head1 ACKNOWLEDGEMENTS
280              
281             The code was inspired by the C<run_hook_chain> and C<hook_chain_fast>
282             code of the L<DJabberd project|DJabberd> (see the
283             L<DJabberd::VHost|DJabberd::VHost> module source code). Hat tip to Brad
284             Fitzpatrick.
285              
286             =head1 AUTHOR
287              
288             Pedro Melo <melo@cpan.org>
289              
290             =head1 COPYRIGHT AND LICENSE
291              
292             This software is Copyright (c) 2011 by Pedro Melo.
293              
294             This is free software, licensed under:
295              
296             The Artistic License 2.0 (GPL Compatible)
297              
298             =cut
299