File Coverage

blib/lib/Data/Transform/Stackable.pm
Criterion Covered Total %
statement 85 88 96.5
branch 15 24 62.5
condition 7 14 50.0
subroutine 17 17 100.0
pod 12 12 100.0
total 136 155 87.7


line stmt bran cond sub pod time code
1             # vim: ts=3 sw=3 expandtab
2             package Data::Transform::Stackable;
3 1     1   1662 use strict;
  1         2  
  1         46  
4              
5 1     1   615 use Data::Transform;
  1         3  
  1         24  
6 1     1   5 use Scalar::Util qw(blessed);
  1         2  
  1         40  
7              
8 1     1   4 use vars qw($VERSION @ISA);
  1         2  
  1         50  
9             $VERSION = '0.01';
10             @ISA = qw(Data::Transform);
11              
12 1     1   5 use Carp qw(croak);
  1         2  
  1         766  
13              
14             sub FILTERS () { 0 }
15              
16             =pod
17              
18             =head1 NAME
19              
20             Data::Transform::Stackable - combine multiple Data::Transform objects
21              
22             =head1 SYNOPSIS
23              
24             =head1 DESCRIPTION
25              
26             Data::Transform::Stackable combines multiple filters together in such a
27             way that they appear to be a single filter. All the usual L
28             methods work, but data is secretly passed through the stacked filters
29             before it is returned.
30              
31             Data added by get_one_start() will flow through the filter array in
32             increasing index order. Filter #0 will have first crack at it,
33             followed by filter #1 and so. The get_one() call will return an item
34             after it has passed through the last filter.
35              
36             put() passes data through the filters in descending index order. Data
37             will go through the filter with the highest index first, and put()
38             will return the results after data has passed through filter #0.
39              
40             =head1 PUBLIC FILTER METHODS
41              
42             Data::Transform::Stackable implements the L API. Only
43             differences and additions are documented here.
44              
45             =cut
46              
47             =head2 new
48              
49             By default, new() creates an empty filter stack that behaves like
50             Data::Transform::Stream. It may be given optional parameters to
51             initialize the stack with an array of filters.
52              
53             my $sudo_lines = Data::Transform::Stackable->new(
54             Filters => [
55             Data::Transform::Line->new(),
56             Data::Transform::Grep->new(
57             Put => sub { 1 }, # put all items
58             Get => sub { shift() =~ /sudo\[\d+\]/i },
59             ),
60             ]
61             );
62              
63             =cut
64              
65             sub new {
66 1     1 1 2 my $type = shift;
67 1 50       4 croak "$type must be given an even number of parameters" if @_ & 1;
68 1         3 my %params = @_;
69              
70 1 50       3 $params{Filters} = [ ] unless defined $params{Filters};
71             # Sanity check the filters
72 1 50       3 if ( ref $params{Filters} eq 'ARRAY') {
73              
74 1         4 my $self = bless [
75             $params{Filters}, # FILTERS
76             ], $type;
77              
78 1         3 return $self;
79             } else {
80 0         0 croak "Filters is not an ARRAY reference!";
81             }
82             }
83              
84             sub clone {
85 1     1 1 6 my $self = shift;
86              
87 1         3 my $clone = [
88             [ ], # FILTERS
89             ];
90 1         2 foreach my $filter (@{$self->[FILTERS]}) {
  1         3  
91 3         4 push (@{$clone->[FILTERS]}, $filter->clone());
  3         15  
92             }
93              
94 1         4 return bless $clone, ref $self;
95             }
96              
97             sub get_pending {
98 2     2 1 1016 my ($self) = @_;
99 2         2 my $data;
100 2         4 for (@{$self->[FILTERS]}) {
  2         5  
101 6 50 33     12 $_->put($data) if $data && @{$data};
  0         0  
102 6         23 $data = $_->get_pending;
103             }
104 2   50     22 return $data || [];
105             }
106              
107             sub get_one_start {
108 3     3 1 4 my ($self, $data) = @_;
109 3         17 $self->[FILTERS]->[0]->get_one_start($data);
110             }
111              
112             # RCC 2005-06-28: get_one() needs to strobe through all the filters
113             # regardless whether there's data to input to each. This is because a
114             # later filter in the chain may produce multiple things from one piece
115             # of input. If we stop even though there's no subsequent input, we
116             # may lose something.
117             #
118             # Keep looping through the filters we manage until get_one() returns a
119             # record, or until none of the filters exchange data.
120              
121             sub get_one {
122 6     6 1 7 my ($self) = @_;
123              
124 6         10 my $return = [ ];
125              
126 6         14 while (!@$return) {
127 8         8 my $exchanged = 0;
128              
129 8         9 foreach my $filter (@{$self->[FILTERS]}) {
  8         16  
130              
131             # If we have something to input to the next filter, do that.
132 24 100       48 if (@$return) {
133 7         24 $filter->get_one_start($return);
134 7         9 $exchanged++;
135             }
136              
137             # Get what we can from the current filter.
138 24         80 $return = $filter->get_one();
139 24 50 66     131 last if ( blessed $return->[0]
140             and $return->[0]->isa('Data::Transform::Meta::SENDBACK'));
141             }
142              
143 8 100       21 last unless $exchanged;
144             }
145              
146 6         17 return $return;
147             }
148              
149             # get() is inherited from Data::Transform.
150              
151             sub put {
152 2     2 1 1604 my ($self, $data) = @_;
153 2         4 foreach my $filter (reverse @{$self->[FILTERS]}) {
  2         5  
154 6         103 $data = $filter->put($data);
155 6 50       16 last unless @$data;
156             }
157 2         6 $data;
158             }
159              
160             =head2 filter_types
161              
162             filter_types() returns a list of class names for each filter in the
163             stack, in the stack's native order.
164              
165             =cut
166              
167             sub filter_types {
168 3     3 1 733 map { ref($_) } @{$_[0]->[FILTERS]};
  9         23  
  3         7  
169             }
170              
171             =head2 filters
172              
173             filters() returns a list of the filters inside the Stackable filter,
174             in the stack's native order.
175              
176             =cut
177              
178             sub filters {
179 6     6 1 2603 @{$_[0]->[FILTERS]};
  6         21  
180             }
181              
182             =head2 shift
183              
184             Behaves like Perl's built-in shift() for the filter stack. The 0th
185             filter is removed from the stack and returned. Any data remaining in
186             the filter's input buffer is passed to the new head of the stack, or
187             it is lost if the stack becomes empty. An application may also call
188             L on the returned filter to examine the
189             filter's input buffer.
190              
191             my $first_filter = $stackable->shift();
192             my $first_buffer = $first_filter->get_pending();
193              
194             =cut
195              
196             sub shift {
197 1     1 1 370 my ($self) = @_;
198 1         1 my $filter = shift @{$self->[FILTERS]};
  1         3  
199 1         4 my $pending = $filter->get_pending;
200 1 50       4 $self->[FILTERS]->[0]->put( $pending ) if $pending;
201 1         2 $filter;
202             }
203              
204             =head2 unshift FILTER[, FILTER]
205              
206             unshift() adds one or more new FILTERs to the beginning of the stack.
207             The newly unshifted FILTERs will process input first, and they will
208             handle output last.
209              
210             =cut
211              
212             sub unshift {
213 1     1 1 5 my ($self, @filters) = @_;
214              
215             # Sanity check
216 1         7 foreach my $elem ( @filters ) {
217 1 50 33     12 if ( ! defined $elem or ! UNIVERSAL::isa( $elem, 'Data::Transform' ) ) {
218 0         0 croak "Filter element is not a Data::Transform instance!";
219             }
220             }
221              
222 1         1 unshift(@{$self->[FILTERS]}, @filters);
  1         4  
223             }
224              
225             =head2 push FILTER[, FILTER]
226              
227             push() adds one or more new FILTERs to the end of the stack. The
228             newly pushed FILTERs will process input last, and they will handle
229             output first.
230              
231             # Reverse data read through the stack.
232             # rot13 encode data sent through the stack.
233             $stackable->push(
234             Data::Transform::Map->(
235             Get => sub { return scalar reverse shift() },
236             Put => sub { local $_ = shift(); tr[a-zA-Z][n-za-mN-ZA-M]; $_ },
237             )
238             );
239              
240             =cut
241              
242             sub push {
243 5     5 1 1639 my ($self, @filters) = @_;
244              
245             # Sanity check
246 5         8 foreach my $elem ( @filters ) {
247 5 100 66     37 if ( ! defined $elem or ! UNIVERSAL::isa( $elem, 'Data::Transform' ) ) {
248 4         485 croak "Filter element is not a Data::Transform instance!";
249             }
250             }
251              
252 1         2 push(@{$self->[FILTERS]}, @filters);
  1         4  
253             }
254              
255             =head2 pop
256              
257             Behaves like Perl's built-in pop() for the filter stack. The
258             highest-indexed filter is removed from the stack and returned. Any
259             data remaining in the filter's input buffer is lost, but an
260             application may always call L on the returned
261             filter.
262              
263             my $last_filter = $stackable->pop();
264             my $last_buffer = $last_filter->get_pending();
265              
266             =cut
267              
268             sub pop {
269 1     1 1 12 my ($self) = @_;
270 1         3 my $filter = pop @{$self->[FILTERS]};
  1         2  
271 1         5 my $pending = $filter->get_pending;
272 1 50       6 $self->[FILTERS]->[-1]->put( $pending ) if $pending;
273 1         3 $filter;
274             }
275              
276             1;
277              
278             __END__