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