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   6054  
  12         24  
  12         69  
4             our $VERSION = '1.2018';
5              
6             use Moo::Role;
7 12     12   75 use Package::Stash;
  12         21  
  12         70  
8 12     12   4058 use namespace::clean;
  12         33  
  12         311  
9 12     12   58  
  12         28  
  12         65  
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 1472 }
33 56         765  
34             my ($self, $data, $code) = @_;
35             return $code->($data);
36             }
37 19     19 1 31  
38 19         211 my ($self, $fixer, $label) = @_;
39             my $perl = "";
40              
41             my $var = $fixer->var;
42 116     116 0 257 my $bind_var = $fixer->capture($self);
43 116         207 my $unit = $fixer->generate_var;
44              
45 116         2070 #---The subfixer is only provided for backwards compatibility
46 116         1095 # with older Bind implementations and is deprecated
47 116         396 my $sub_fixer = Catmandu::Fix->new(fixes => $self->__fixes__);
48             my $sub_fixer_var = $fixer->capture($sub_fixer);
49              
50             #---
51 116         1989  
52 116         2470 my $fix_stash = Package::Stash->new('Catmandu::Fix');
53             my $fix_emit_reject;
54             my $fix_emit_fixes;
55              
56 116         1113 # Allow Bind-s to overwrite the default reject behavior
57 116         260 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       608 }
62 12         69  
63             # Allow Bind-s to bind to all fixes in if-unless-else statements
64 12     3   79 unless ($self->does('Catmandu::Fix::Bind::Group')) {
  3         11  
65             $fix_emit_fixes = $fix_stash->get_symbol('&emit_fixes');
66             $fix_stash->add_symbol(
67             '&emit_fixes' => sub {
68 116 100       520 my ($this, $fixes) = @_;
69 33         1549 my $perl = '';
70              
71             $perl .= "my ${unit} = ${bind_var}->unit(${var});";
72 24     24   49  
73 24         37 for (my $i = 0; $i < @{$fixes}; $i++) {
74             my $fix = $fixes->[$i];
75 24         74 my $name = ref($fix);
76             my $var = $this->var;
77 24         43 my $original_code = $this->emit_fix($fix);
  36         86  
78 12         25 my $generated_code
79 12         29 = "sub { my ${var} = shift; $original_code ; ${var} }";
80 12         178 $perl
81 12         103 .= "${unit} = ${bind_var}->bind(${unit}, $generated_code, '$name',${sub_fixer_var});";
82 12         51 }
83              
84 12         66 if ($self->can('result')) {
85             $perl .= "${unit} = ${bind_var}->result(${unit});";
86             }
87              
88 24 100       83 if ($self->__return__) {
89 16         55 $perl .= "${var} = ${unit};";
90             }
91              
92 24 50       66 $perl;
93 0         0 }
94             );
95             }
96 24         83  
97             $perl .= "my ${unit} = ${bind_var}->unit(${var});";
98 33         511  
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         2516 my $generated_code = "sub { my ${var} = shift;";
102              
103             for my $fix (@{$self->__fixes__}) {
104 116 100       300 my $original_code = $fixer->emit_fix($fix);
105 83         1049 $generated_code .= "$original_code ;";
106             }
107 83         123  
  83         228  
108 79         242 $generated_code .= "${var} }";
109 79         245  
110             $perl
111             .= "${unit} = ${bind_var}->bind(${unit}, $generated_code,'::group::',${sub_fixer_var});";
112 83         181 }
113              
114 83         349 # 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         580 = "sub { my ${var} = shift; $original_code ; ${var} }";
  33         99  
121 34         86  
122 34         112 $perl
123 34         118 .= "${unit} = ${bind_var}->bind(${unit}, $generated_code,'$name',${sub_fixer_var});";
124             }
125             }
126 34         181  
127             if ($self->can('result')) {
128             $perl .= "${unit} = ${bind_var}->result(${unit});";
129             }
130              
131 116 100       642 if ($self->__return__) {
132 24         88 $perl .= "${var} = ${unit};";
133             }
134              
135 116 50       397 $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       330  
140             $perl;
141 116 100       749 }
142              
143             1;
144 116         498  
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