File Coverage

blib/lib/Sub/Chain/Group.pm
Criterion Covered Total %
statement 141 143 98.6
branch 53 60 88.3
condition 28 31 90.3
subroutine 20 20 100.0
pod 10 10 100.0
total 252 264 95.4


line stmt bran cond sub pod time code
1             # vim: set ts=2 sts=2 sw=2 expandtab smarttab:
2             #
3             # This file is part of Sub-Chain-Group
4             #
5             # This software is copyright (c) 2010 by Randy Stauner.
6             #
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             #
10 4     4   23153 use strict;
  4         8  
  4         116  
11 4     4   21 use warnings;
  4         7  
  4         286  
12              
13             package Sub::Chain::Group;
14             # git description: v0.013-4-g6f84b56
15              
16             our $AUTHORITY = 'cpan:RWSTAUNER';
17             # ABSTRACT: Group chains of subs by field name
18             $Sub::Chain::Group::VERSION = '0.014';
19 4     4   20 use Carp qw(croak carp);
  4         7  
  4         281  
20              
21             # this seems a little dirty, but it's not appropriate to put it in Sub::Chain
22 4     4   3042 use Sub::Chain;
  4         86984  
  4         145  
23             {
24 4     4   1081 no warnings 'once';
  4         9  
  4         223  
25             push(@Sub::Chain::CARP_NOT, __PACKAGE__);
26             }
27              
28 4     4   3180 use Set::DynamicGroups ();
  4         5600  
  4         75  
29 4     4   3934 use Module::Load ();
  4         4196  
  4         6462  
30              
31              
32             sub new {
33 14     14 1 8031 my $class = shift;
34 14 50       62 my %opts = ref $_[0] ? %{$_[0]} : @_;
  0         0  
35              
36             my $self = {
37             chain_class => delete $opts{chain_class} || 'Sub::Chain',
38             chain_args => delete $opts{chain_args} || {},
39             fields => {},
40             groups => Set::DynamicGroups->new(),
41             queue => [],
42             hooks => {},
43             hook_as_hash => delete $opts{hook_as_hash},
44 14   100     161 warn_no_field => 'single',
      100        
45             };
46              
47 14         208 foreach my $enum (
48             [warn_no_field => qw(never single always)],
49             ){
50 14         41 my ($key, @vals) = @$enum;
51 14 100       66 if( my $val = delete $opts{ $key } ){
52             croak qq['$key' cannot be set to '$val'; must be one of: ] . join(', ', @vals)
53 4 100       8 unless grep { $val eq $_ } @vals;
  12         43  
54 3         11 $self->{ $key } = $val;
55             }
56             }
57              
58 13         58 Module::Load::load($self->{chain_class});
59              
60             # TODO: warn about remaining unused options?
61              
62 13         4391 bless $self, $class;
63             }
64              
65              
66             sub append {
67 37     37 1 11234 my ($self, $sub) = (shift, shift);
68 37 50       132 my %opts = ref $_[0] ? %{$_[0]} : @_;
  0         0  
69              
70 37   100     43 CORE::push(@{ $self->{queue} ||= [] },
  37         173  
71             [$sub, $self->_normalize_spec(\%opts)]);
72              
73 37         127 return $self;
74             }
75              
76              
77             sub call {
78 26     26 1 4501 my ($self) = shift;
79              
80             $self->dequeue
81 26 100       94 if $self->{queue};
82              
83 26         28 my $out;
84 26         59 my $opts = {multi => 1};
85 26         48 my $ref = ref $_[0];
86              
87 26         34 my ($before, $after) = @{ $self->{hooks} }{qw( before after )};
  26         71  
88              
89 26 100       78 if( $ref eq 'HASH' ){
    100          
90 6         9 my $in = { %{ $_[0] } };
  6         22  
91 6 100       25 $in = $before->call($in) if $before;
92 6         118 $out = {};
93 6         26 while( my ($key, $value) = each %$in ){
94 15         610 $out->{$key} = $self->_call_one($key, $value, $opts);
95             }
96 6 100       400 $out = $after->call($out) if $after;
97             }
98             elsif( $ref eq 'ARRAY' ){
99 5         9 my $fields = [ @{ $_[0] } ];
  5         13  
100 5         6 my $values = [ @{ $_[1] } ];
  5         12  
101 5 100       19 $values = $self->_call_hook($before, $values, $fields) if $before;
102 5         22 $out = [];
103 5         15 foreach my $i ( 0 .. @$fields - 1 ){
104 17         583 CORE::push(@$out,
105             $self->_call_one($fields->[$i], $values->[$i], $opts));
106             }
107 5 100       247 $out = $self->_call_hook($after, $out, $fields) if $after;
108             }
109             else {
110 15         27 my ($key, $val) = @_;
111 15 100       54 $val = $self->_call_hook($before, $val, $key) if $before;
112 15         40 $out = $self->_call_one($key, $val);
113 15 100       2578 $out = $self->_call_hook($after, $out, $key) if $after;
114             }
115              
116 26         345 return $out;
117             }
118              
119             sub _call_hook {
120 16     16   24 my ($self, $chain, $values, $fields) = @_;
121              
122 16 100       32 if( $self->{hook_as_hash} ){
123 8 100       17 if( ref($fields) eq 'ARRAY' ){
124 4         6 my $hash = {};
125 4         15 @$hash{ @$fields } = @$values;
126 4         17 $hash = $chain->call($hash);
127 4         292 $values = [ @$hash{ @$fields } ];
128             }
129             else {
130 4         9 my $hash = { $fields => $values };
131 4         12 $hash = $chain->call($hash);
132 4         292 $values = $hash->{ $fields };
133             }
134             }
135             else {
136 8         18 $values = $chain->call($values, $fields);
137             }
138              
139 16         644 return $values;
140             }
141              
142             sub _call_one {
143 47     47   80 my ($self, $field, $value, $opts) = @_;
144 47 100       97 return $value
145             unless my $chain = $self->chain($field, $opts);
146 37         114 return $chain->call($value);
147             }
148              
149              
150             sub chain {
151 64     64 1 549 my ($self, $name, $opts) = @_;
152 64   100     198 $opts ||= {};
153              
154             $self->dequeue
155 64 100       155 if $self->{queue};
156              
157 64 100       175 if( my $chain = $self->{fields}{$name} ){
158 54         175 return $chain;
159             }
160              
161             carp("No subs chained for '$name'")
162             if $self->{warn_no_field} eq 'always'
163 10 100 100     101 || ($self->{warn_no_field} eq 'single' && !$opts->{multi});
      66        
164              
165 10         1710 return;
166             }
167              
168              
169             sub dequeue {
170 14     14 1 3003 my ($self) = @_;
171              
172 14 50       46 return unless my $queue = $self->{queue};
173 14   100     59 my $dequeued = ($self->{dequeued} ||= []);
174              
175             # shift items off the queue until they've all been processed
176 14         55 while( my $item = shift @$queue ){
177             # save this item in case we need to reprocess the whole queue later
178 45         5708 CORE::push(@$dequeued, $item);
179              
180 45         68 my ($sub, $opts) = @$item;
181 45         152 my @chain_args = ($sub, @$opts{qw(args opts)});
182              
183 45 100       53 foreach my $hook ( @{ $opts->{hooks} || [] } ){
  45         195  
184 12   66     57 ($self->{hooks}->{ $hook } ||= $self->new_sub_chain())
185             ->append(@chain_args);
186             }
187              
188 45   100     2717 my $fields = $opts->{fields} || [];
189             # keep fields unique
190 45         75 my %seen = map { $_ => 1 } @$fields;
  30         95  
191             # add unique fields from groups (if there are any)
192 45 100       116 if( my $groups = $opts->{groups} ){
193 25         61 CORE::push(@$fields, grep { !$seen{$_}++ }
194 5         8 map { @$_ } values %{ $self->{groups}->groups(@$groups) }
  5         329  
  5         20  
195             );
196             }
197              
198 45         107 foreach my $field ( @$fields ){
199 55   66     6817 ($self->{fields}->{$field} ||= $self->new_sub_chain())
200             ->append(@chain_args);
201             }
202             }
203             # let 'queue' return false so we can do simple 'if queue' checks
204 14         2941 delete $self->{queue};
205              
206             # what would be a good return value?
207 14         28 return;
208             }
209              
210              
211             sub fields {
212 3     3 1 779 my ($self) = shift;
213 3         12 $self->{groups}->add_items(@_);
214             $self->reprocess_queue
215 3 100       68 if $self->{dequeued};
216 3         6 return $self;
217             }
218              
219              
220             sub group {
221 5     5 1 1677 my ($self) = shift;
222 5 50       15 croak("group() takes argument pairs. Did you mean groups()?")
223             if !@_;
224              
225 5         25 $self->{groups}->add(@_);
226             $self->reprocess_queue
227 5 100       303 if $self->{dequeued};
228 5         23 return $self;
229             }
230              
231              
232             sub groups {
233 6     6 1 1074 my ($self) = shift;
234 6 50       17 croak("groups() takes no arguments. Did you mean group()?")
235             if @_;
236              
237 6         24 return $self->{groups};
238             }
239              
240              
241             sub new_sub_chain {
242 40     40 1 56 my ($self) = @_;
243 40         222 return $self->{chain_class}->new($self->{chain_args});
244             }
245              
246             sub _normalize_spec {
247 37     37   47 my ($self, $opts) = @_;
248              
249             # Don't alter \%opts. Limit %norm to desired keys.
250 37         46 my %norm;
251 37         143 my %aliases = (
252             arguments => 'args',
253             options => 'opts',
254             field => 'fields',
255             group => 'groups',
256             hook => 'hooks',
257             );
258              
259 37         113 while( my ($alias, $name) = each %aliases ){
260             # store the alias in the actual key
261             # overwrite with actual key if specified
262 185         233 foreach my $key ( $alias, $name ){
263             $norm{$name} = $opts->{$key}
264 370 100       1164 if exists $opts->{$key};
265             }
266             }
267              
268             # allow a single string and convert it to an arrayref
269 37         51 foreach my $type ( qw(fields groups hooks) ){
270             $norm{$type} = [$norm{$type}]
271 111 100 100     418 if exists($norm{$type}) && !ref($norm{$type});
272             }
273              
274             # simplify code later by initializing these to refs
275 37   100     157 $norm{args} ||= [];
276 37   100     133 $norm{opts} ||= {};
277              
278 37         139 return \%norm;
279             }
280              
281              
282             sub reprocess_queue {
283 2     2 1 5 my ($self) = @_;
284 2 50       9 return unless my $dequeued = delete $self->{dequeued};
285              
286             # reset the queue and the stacks so that it will all be rebuilt
287 2 50       4 $self->{queue} = [@$dequeued, @{ $self->{queue} || [] } ];
  2         27  
288 2         6 $self->{fields} = {};
289 2         55 $self->{hooks} = {};
290             # but don't actually rebuild it until necessary
291             }
292              
293             1;
294              
295             # NOTE: Synopsis tested in t/synopsis.t
296              
297             __END__