File Coverage

blib/lib/Catmandu/Fix/Bind.pm
Criterion Covered Total %
statement 68 70 97.1
branch 16 18 88.8
condition n/a
subroutine 9 9 100.0
pod 2 3 66.6
total 95 100 95.0


line stmt bran cond sub pod time code
1              
2             use Catmandu::Sane;
3 12     12   6396  
  12         26  
  12         82  
4             our $VERSION = '1.2019';
5              
6             use Moo::Role;
7 12     12   87 use Package::Stash;
  12         23  
  12         71  
8 12     12   4075 use namespace::clean;
  12         33  
  12         327  
9 12     12   69  
  12         28  
  12         75  
10             with 'Catmandu::Logger';
11              
12             requires 'unit';
13             requires 'bind';
14              
15             has __return__ => (is => 'rw', default => sub {[0]});
16             has __fixes__ => (is => 'rw', default => sub {[]});
17              
18             around bind => sub {
19             my ($orig, $self, $prev, @args) = @_;
20             my $next = $orig->($self, $prev, @args);
21              
22             if ($self->can('plus')) {
23             return $self->plus($prev, $next);
24             }
25             else {
26             return $next;
27             }
28             };
29              
30             my ($self, $data) = @_;
31             return $data;
32 56     56 1 1376 }
33 56         777  
34             my ($self, $data, $code) = @_;
35             return $code->($data);
36             }
37 19     19 1 29  
38 19         189 my ($self, $fixer, $label) = @_;
39             my $perl = "";
40              
41             my $var = $fixer->var;
42 116     116 0 223 my $bind_var = $fixer->capture($self);
43 116         171 my $unit = $fixer->generate_var;
44              
45 116         1964 #---The subfixer is only provided for backwards compatibility
46 116         969 # with older Bind implementations and is deprecated
47 116         324 my $sub_fixer = Catmandu::Fix->new(fixes => $self->__fixes__);
48             my $sub_fixer_var = $fixer->capture($sub_fixer);
49              
50             #---
51 116         1846  
52 116         2178 my $fix_stash = Package::Stash->new('Catmandu::Fix');
53             my $fix_emit_reject;
54             my $fix_emit_fixes;
55              
56 116         946 # Allow Bind-s to overwrite the default reject behavior
57 116         241 if ($self->can('reject')) {
58             $fix_emit_reject = $fix_stash->get_symbol('&emit_reject');
59             $fix_stash->add_symbol(
60             '&emit_reject' => sub {"return ${bind_var}->reject(${var});";});
61 116 100       487 }
62 12         68  
63             # Allow Bind-s to bind to all fixes in if-unless-else statements
64 12     3   74 unless ($self->does('Catmandu::Fix::Bind::Group')) {
  3         12  
65             $fix_emit_fixes = $fix_stash->get_symbol('&emit_fixes');
66             $fix_stash->add_symbol(
67             '&emit_fixes' => sub {
68 116 100       386 my ($this, $fixes) = @_;
69 33         1364 my $perl = '';
70              
71             $perl .= "my ${unit} = ${bind_var}->unit(${var});";
72 24     24   47  
73 24         38 for (my $i = 0; $i < @{$fixes}; $i++) {
74             my $fix = $fixes->[$i];
75 24         67 my $name = ref($fix);
76             my $var = $this->var;
77 24         42 my $original_code = $this->emit_fix($fix);
  36         82  
78 12         22 my $generated_code
79 12         25 = "sub { my ${var} = shift; $original_code ; ${var} }";
80 12         171 $perl
81 12         101 .= "${unit} = ${bind_var}->bind(${unit}, $generated_code, '$name',${sub_fixer_var});";
82 12         48 }
83              
84 12         63 if ($self->can('result')) {
85             $perl .= "${unit} = ${bind_var}->result(${unit});";
86             }
87              
88 24 100       79 if ($self->__return__) {
89 16         42 $perl .= "${var} = ${unit};";
90             }
91              
92 24 50       69 $perl;
93 0         0 }
94             );
95             }
96 24         72  
97             $perl .= "my ${unit} = ${bind_var}->unit(${var});";
98 33         371  
99             # If this is a Bind::Group, then all fixes are executed as one block in a bind
100             if ($self->does("Catmandu::Fix::Bind::Group")) {
101 116         2056 my $generated_code = "sub { my ${var} = shift;";
102              
103             for my $fix (@{$self->__fixes__}) {
104 116 100       282 my $original_code = $fixer->emit_fix($fix);
105 83         975 $generated_code .= "$original_code ;";
106             }
107 83         105  
  83         187  
108 79         216 $generated_code .= "${var} }";
109 79         216  
110             $perl
111             .= "${unit} = ${bind_var}->bind(${unit}, $generated_code,'::group::',${sub_fixer_var});";
112 83         151 }
113              
114 83         282 # If this isn't a Bind::Group, then bind will be executed for each seperate fix
115             else {
116             for my $fix (@{$self->__fixes__}) {
117             my $name = ref($fix);
118             my $original_code = $fixer->emit_fix($fix);
119             my $generated_code
120 33         552 = "sub { my ${var} = shift; $original_code ; ${var} }";
  33         120  
121 34         75  
122 34         107 $perl
123 34         124 .= "${unit} = ${bind_var}->bind(${unit}, $generated_code,'$name',${sub_fixer_var});";
124             }
125             }
126 34         163  
127             if ($self->can('result')) {
128             $perl .= "${unit} = ${bind_var}->result(${unit});";
129             }
130              
131 116 100       468 if ($self->__return__) {
132 24         80 $perl .= "${var} = ${unit};";
133             }
134              
135 116 50       337 $fix_stash->add_symbol('&emit_reject' => $fix_emit_reject)
136 0         0 if $fix_emit_reject;
137             $fix_stash->add_symbol('&emit_fixes' => $fix_emit_fixes)
138             if $fix_emit_fixes;
139 116 100       300  
140             $perl;
141 116 100       628 }
142              
143             1;
144 116         449  
145              
146             =pod
147              
148             =head1 NAME
149              
150             Catmandu::Fix::Bind - a wrapper for Catmandu::Fix-es
151              
152             =head1 SYNOPSIS
153              
154             package Catmandu::Fix::Bind::demo;
155             use Moo;
156             with 'Catmandu::Fix::Bind';
157              
158             sub bind {
159             my ($self,$data,$code,$name) = @_;
160             warn "executing $name";
161             $code->($data);
162             }
163              
164             # in your fix script you can now write
165             do
166             demo()
167              
168             fix1()
169             fix2()
170             fix3()
171             end
172              
173             # this will execute all the fixes as expected, and print to STDERR the following messages
174              
175             executing fix1
176             executing fix2
177             executing fix3
178              
179             =head1 DESCRIPTION
180              
181             Bind is a package that wraps Catmandu::Fix-es and other Catmandu::Bind-s together. This gives
182             the programmer further control on the excution of fixes. With Catmandu::Fix::Bind you can simulate
183             the 'before', 'after' and 'around' modifiers as found in Moo or Dancer.
184              
185             To wrap Fix functions, the Fix language introduces the 'do' statement:
186              
187             do BIND
188             FIX1
189             FIX2
190             FIX3
191             end
192              
193             where BIND is a implementation of Catmandu::Fix::Bind and FIX1,...,FIXn are Catmandu::Fix functions.
194              
195             In the example above the BIND will wrap FIX1, FIX2 and FIX3. BIND will first wrap the record data
196             using its 'unit' method and send the data sequentially to each FIX which can make inline changes
197             to the record data. In pseudo-code this will look like:
198              
199             $bind_data = $bind->unit($data);
200             $bind_data = $bind->bind($bind_data, $fix1);
201             $bind_data = $bind->bind($bind_data, $fix2);
202             $bind_data = $bind->bind($bind_data, $fix3);
203             return $data;
204              
205             An alternative form exists, 'doset' which will overwrite the record data with results of the last
206             fix.
207              
208             doset BIND
209             FIX1
210             FIX2
211             FIX3
212             end
213              
214             Will result in a pseudo code like:
215              
216             $bind_data = $bind->unit($data);
217             $bind_data = $bind->bind($bind_data, $fix1);
218             $bind_data = $bind->bind($bind_data, $fix2);
219             $bind_data = $bind->bind($bind_data, $fix3);
220             return $bind_data;
221              
222             A Catmandu::Fix::Bind needs to implement two methods: 'unit' and 'bind'.
223              
224             =head1 METHODS
225              
226             =head2 unit($data)
227              
228             The unit method receives a Perl $data HASH and should return it, possibly converted to a new type.
229             The 'unit' method is called before all Fix methods are executed. A trivial, but verbose, implementation
230             of 'unit' is:
231              
232             sub unit {
233             my ($self,$data) = @_;
234             my $wrapped_data = $data;
235             return $wrapped_data;
236             }
237              
238             =head2 bind($wrapped_data,$code)
239              
240             The bind method is executed for every Catmandu::Fix method in the fix script. It receives the $wrapped_data
241             (wrapped by 'unit'), the fix method as anonymous subroutine and the name of the fix. It should return data
242             with the same type as returned by 'unit'.
243             A trivial, but verbose, implementaion of 'bind' is:
244              
245             sub bind {
246             my ($self,$wrapped_data,$code) = @_;
247             my $data = $wrapped_data;
248             $data = $code->($data);
249             # we don't need to wrap it again because the $data and $wrapped_data have the same type
250             $data;
251             }
252              
253             =head1 REQUIREMENTS
254              
255             Bind modules are simplified implementations of Monads. They should answer the formal definition of Monads, codified
256             in 3 monadic laws:
257              
258             =head2 left unit: unit acts as a neutral element of bind
259              
260             my $monad = Catmandu::Fix::Bind->demo();
261              
262             # bind(unit(data), coderef) == unit(coderef(data))
263             $monad->bind( $monad->unit({foo=>'bar'}) , $coderef) == $monad->unit($coderef->({foo=>'bar'}));
264              
265             =head2 right unit: unit act as a neutral element of bind
266              
267             # bind(unit(data), unit) == unit(data)
268             $monad->bind( $monad->unit({foo=>'bar'}) , sub { $monad->unit(shift) } ) == $monad->unit({foo=>'bar'});
269              
270             =head2 associative: chaining bind blocks should have the same effect as nesting them
271              
272             # bind(bind(unit(data),f),g) == bind(unit(data), sub { return bind(unit(f(data)),g) } )
273             my $f = sub { my $data = shift; $data->{demo} = 1 ; $data };
274             my $g = sub { my $data = shift; $data->{demo} += 1 ; $data};
275              
276             $monad->bind( $monad->bind( $monad->unit({}) , f ) , g ) ==
277             $monad->bind( $monad->unit({}) , sub { my $data = shift; $monad->bind($monad->unit($f->($data)), $g ); $data; });
278              
279             =head1 SEE ALSO
280              
281             L<Catmandu::Fix::Bind::identity>, L<Catmandu::Fix::Bind::benchmark>
282              
283             =cut