File Coverage

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