File Coverage

blib/lib/Mojolicious/Plugin/FormFields.pm
Criterion Covered Total %
statement 251 257 97.6
branch 65 78 83.3
condition 38 62 61.2
subroutine 51 52 98.0
pod 1 1 100.0
total 406 450 90.2


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::FormFields;
2              
3             # TODO: We're not much of a subclass now
4 14     14   14539 use Mojo::Base 'Mojolicious::Plugin::ParamExpand';
  14         31  
  14         116  
5              
6             our $VERSION = '0.04';
7              
8             sub register
9             {
10 14     14 1 829 my ($self, $app, $config) = @_;
11 14         40 my $ns = 'formfields.fields';
12              
13 14         56 $config->{separator} = Mojolicious::Plugin::FormFields::Field->separator;
14 14         145 $self->SUPER::register($app, $config);
15              
16             $app->helper(field => sub {
17 116     116   1513891 my $c = shift;
18 116   100     514 my $name = shift || '';
19 116   66     390 $c->stash->{$ns}->{$name} ||= Mojolicious::Plugin::FormFields::Field->new($c, $name, @_);
20 115         689 $c->stash->{$ns}->{$name};
21 14         1650 });
22              
23             $app->helper(fields => sub {
24 33     33   402208 my $c = shift;
25 33   50     139 my $name = shift || '';
26 33   33     102 $c->stash->{$ns}->{$name} ||= Mojolicious::Plugin::FormFields::ScopedField->new($c, $name, @_);
27 33         107 $c->stash->{$ns}->{$name};
28 14         1563 });
29              
30 14         1139 my $methods = $config->{methods};
31 14   50     138 my $helper = $methods->{valid} // 'valid';
32             $app->helper($helper => sub {
33 13     13   1138 my $c = shift;
34 13         26 my $valid = 1;
35 13         21 my $errors = {};
36              
37             # TODO: skip keys used by fields()
38 13         27 while(my ($name, $field) = each %{$c->stash->{$ns}}) {
  33         100  
39 20 100       270 if(!$field->valid) {
40 9         12 $valid = 0;
41 9         31 $errors->{$name} = $field->error;
42             }
43             }
44              
45 13         139 $c->stash->{"$ns.errors"} = $errors;
46 13         165 $valid;
47 14         103 });
48              
49 14   50     1174 $helper = $methods->{errors} // 'errors';
50             $app->helper($helper => sub {
51 7     7   582 my ($c, $name) = @_;
52 7   50     23 my $errors = $c->stash->{"$ns.errors"} // {};
53 7 50       99 $name ? { $name => $errors->{$name} } : $errors;
54 14         92 });
55             }
56              
57             package Mojolicious::Plugin::FormFields::Field;
58              
59 14     14   61017 use Mojo::Base -strict;
  14         29  
  14         107  
60 14     14   1094 use Scalar::Util;
  14         26  
  14         626  
61 14     14   75 use Carp ();
  14         25  
  14         237  
62 14     14   19322 use Validate::Tiny;
  14         72742  
  14         95  
63              
64             use overload
65             '@{}' => '_to_fields',
66             '""' => '_to_string',
67 184     184   2108 bool => sub { 1 },
68 14     14   1078 fallback => 1;
  14         34  
  14         188  
69              
70             my $SEPARATOR = '.';
71              
72             sub new
73             {
74 118     118   1555 my $class = shift;
75 118         220 my ($c, $name, $object) = @_;
76 118 100       709 Carp::croak 'field name required' unless $name;
77              
78 117         1081 my $self = bless {
79             c => $c,
80             name => $name,
81             object => $object,
82             checks => [],
83             filters => []
84             }, $class;
85              
86 117         2048 Scalar::Util::weaken $self->{c};
87 117         439 $self;
88             }
89              
90             sub checkbox
91             {
92 5     5   40 my $self = shift;
93              
94 5         8 my $value;
95 5 100       22 $value = shift if @_ % 2;
96 5   100     17 $value //= 1;
97              
98 5         10 my %options = @_;
99 5         19 $options{value} = $value;
100              
101 5         23 $self->input('checkbox', %options);
102             }
103              
104             sub file
105             {
106 3     3   33 my ($self, %options) = @_;
107 3   66     16 $options{id} //= _dom_id($self->{name});
108              
109 3         30 $self->{c}->file_field($self->{name}, %options);
110             }
111              
112             sub input
113             {
114 32     32   94 my ($self, $type, %options) = @_;
115 32         112 my $value = $self->_lookup_value;
116              
117 28   66     176 $options{id} //= _dom_id($self->{name});
118 28 50 66     181 $options{value} //= $value if defined $value;
119 28         61 $options{type} = $type;
120              
121 28 100 100     163 if($type eq 'checkbox' || $type eq 'radio') {
122 10 100 33     92 $options{checked} = 'checked'
      66        
123             if !exists $options{checked} && defined $value && $value eq $options{value};
124             }
125              
126 28         246 $self->{c}->input_tag($self->{name}, %options);
127             }
128              
129             sub hidden
130             {
131 8     8   44 my ($self, %options) = @_;
132 8         29 $self->input('hidden', %options);
133             }
134              
135             sub radio
136             {
137 6     6   75 my ($self, $value, %options) = @_;
138 6 100       253 Carp::croak 'value required' unless defined $value;
139              
140 5   66     36 $options{id} //= _dom_id($self->{name}, $value);
141 5         11 $options{value} = $value;
142              
143 5         24 $self->input('radio', %options);
144             }
145              
146             sub select
147             {
148 7     7   76 my $self = shift;
149 7 50       41 my $options = @_ % 2 ? shift : [];
150 7         18 my %attr = @_;
151 7   33     52 $attr{id} //= _dom_id($self->{name});
152              
153 7         16 my $c = $self->{c};
154 7         14 my $name = $self->{name};
155 7         12 my $field;
156              
157 7 100       20 if(defined $c->param($name)) {
158 2         278 $field = $c->select_field($name, $options, %attr);
159             }
160             else {
161             # Make select_field select the value
162 5         506 $c->param($name, $self->_lookup_value);
163 5         390 $field = $c->select_field($name, $options, %attr);
164 5         2756 $c->param($name, undef);
165             }
166              
167 7         1494 $field;
168             }
169              
170             sub password
171             {
172 4     4   36 my ($self, %options) = @_;
173 4   66     30 $options{id} //= _dom_id($self->{name});
174              
175 4         44 $self->{c}->password_field($self->{name}, %options);
176             }
177              
178             sub label
179             {
180 7     7   106 my $self = shift;
181              
182 7         13 my $text;
183 7 100       26 $text = pop if ref $_[-1] eq 'CODE';
184 7 100       26 $text = shift if @_ % 2; # step on CODE
185 7   66     29 $text //= _default_label($self->{name});
186              
187 7         18 my %options = @_;
188 7   33     46 $options{for} //= _dom_id($self->{name});
189              
190 7         58 $self->{c}->tag('label', %options, $text)
191             }
192              
193             sub text
194             {
195 13     13   120 my ($self, %options) = @_;
196 13         61 $self->input('text', %options);
197             }
198              
199             sub textarea
200             {
201 2     2   8 my ($self, %options) = @_;
202 2   33     15 $options{id} //= _dom_id($self->{name});
203              
204 2         4 my $size = delete $options{size};
205 2 50 33     20 if($size && $size =~ /^(\d+)[xX](\d+)$/) {
206 0         0 $options{rows} = $1;
207 0         0 $options{cols} = $2;
208             }
209              
210 2 50   2   36 $self->{c}->text_area($self->{name}, %options, sub { $self->_lookup_value || '' });
  2         453  
211             }
212              
213             sub each
214             {
215 1     1   19 my $self = shift;
216 1         4 my $block = pop;
217 1         5 my $fields = $self->_to_fields;
218              
219 1 50       7 return $fields unless ref($block) eq 'CODE';
220              
221 1         3 local $_;
222 1         6 $block->() for @$fields;
223              
224 1         697 return;
225             }
226              
227             sub check
228             {
229 4     4   29 my $self = shift;
230 4         8 push @{$self->{checks}}, $self->{name} => shift;
  4         20  
231 4         11 $self;
232             }
233              
234             sub filter
235             {
236 8     8   46 my $self = shift;
237 8 100       37 my $data = ref $_[0] eq 'CODE' ? shift : Validate::Tiny::filter(@_);
238 8         195 push @{$self->{filters}}, $self->{name} => $data;
  8         26  
239 8         23 $self;
240             }
241              
242             # Just a single value
243             sub error
244             {
245 20     20   51 my $self = shift;
246 20         110 $self->{result}->{error}->{$self->{name}};
247             }
248              
249 28     28   74 sub separator { $SEPARATOR; }
250              
251             sub valid
252             {
253 34     34   65 my $self = shift;
254 34 100       116 return $self->{result}->{success} if defined $self->{result};
255              
256 30         41 my $result;
257 30         62 my $name = $self->{name};
258 30         122 my $value = $self->{c}->param($name);
259 30         3649 my $field = { $name => $value };
260 30         219 my $rules = {
261             fields => [ $name ],
262             checks => $self->{checks},
263             filters => $self->{filters}
264             };
265              
266             # A bit of massaging For the is_equal() validation
267 30         67 my $eq = $self->{eq_to_field};
268 30 100       80 if($eq) {
269 4         17 $field->{$eq} = $self->{c}->param($eq);
270 4         307 push @{$rules->{fields}}, $eq;
  4         45  
271             }
272              
273 30         122 $result = Validate::Tiny::validate($field, $rules);
274 30 100       3522 $self->{c}->param($name, $result->{data}->{$name}) if @{$self->{filters}};
  30         131  
275 30         499 $self->{result} = $result;
276              
277 30         189 $result->{success};
278             }
279              
280             sub is_equal
281             {
282 4     4   10 my $self = shift;
283 4         11 $self->{eq_to_field} = $_[0];
284 4         7 push @{$self->{checks}}, $self->{name} => Validate::Tiny::is_equal(@_);
  4         28  
285             }
286              
287             # Avoid AUTOLOAD call
288 0     0   0 sub DESTROY { }
289              
290             our $AUTOLOAD;
291             sub AUTOLOAD
292             {
293 24     24   303 my $self = shift;
294 24         194 (my $method = $AUTOLOAD) =~ s/[^':]+:://g;
295              
296 24 50       110 if($method =~ /^is_/) {
297 24         163 my $check = Validate::Tiny->can($method);
298 24 50       74 die qq|Can't locate object method "$method" via package "${ \__PACKAGE__ }"| unless $check;
  0         0  
299              
300 24         36 push @{$self->{checks}}, $self->{name} => $check->(@_);
  24         141  
301             }
302             else {
303             # TODO: What's the use case for this?
304             # field('name')->trim instead of field('name')->filter('trim')?
305 0         0 push @{$self->{filters}}, $self->{name} => Validate::Tiny::filter($method);
  0         0  
306             }
307              
308 24         265 $self->{result} = undef; # reset previous validation
309 24         65 $self;
310             }
311              
312 5     5   58 sub _to_string { shift->_lookup_value; }
313              
314             sub _to_fields
315             {
316 7     7   79 my $self = shift;
317 7         32 my $value = $self->_lookup_value;
318              
319 7         15 my $fields = [];
320 7 100       30 return $fields unless ref($value) eq 'ARRAY';
321              
322 6         11 my $i = -1;
323 6         16 while(++$i < @$value) {
324 12         92 push @$fields, $self->{c}->fields($self->_path($i), $self->{object});
325             }
326              
327 6         82 $fields;
328             }
329              
330             sub _dom_id
331             {
332 46     46   116 my @name = @_;
333 46         331 s/[^\w]+/-/g for @name;
334 46         214 join '-', @name;
335             }
336              
337             sub _default_label
338             {
339 3     3   31 my $label = (split /\Q$SEPARATOR/, shift)[-1];
340 3         10 $label =~ s/[^-a-z0-9]+/ /ig;
341 3         15 ucfirst $label;
342             }
343              
344             sub _invalid_parameter
345             {
346 4     4   9 my ($field, $message) = @_;
347 4         1001 Carp::croak "Invalid parameter '$field': $message";
348             }
349              
350 80     80   583 sub _path { "$_[0]->{name}${SEPARATOR}$_[1]" }
351              
352             sub _lookup_value
353             {
354 54     54   87 my $self = shift;
355 54 100       208 return $self->{value} if defined $self->{value};
356              
357 50         101 my $name = $self->{name};
358 50         98 my $object = $self->{object};
359 50         518 my @path = split /\Q$SEPARATOR/, $name;
360              
361 50 100       152 if(!$object) {
362 26         128 $object = $self->{c}->stash($path[0]);
363 26 100       377 _invalid_parameter($name, "nothing in the stash for '$path[0]'") unless $object;
364             }
365              
366             # Remove the stash key for $object
367 49         79 shift @path;
368              
369 49         171 while(defined(my $accessor = shift @path)) {
370 61         283 my $isa = ref($object);
371              
372             # We don't handle the case where one of these return an array
373 61 100 100     560 if(Scalar::Util::blessed($object) && $object->can($accessor)) {
    100          
    100          
374 40         1005 $object = $object->$accessor;
375             }
376             elsif($isa eq 'HASH') {
377             # If blessed and !can() do we _really_ want to look inside?
378 7         26 $object = $object->{$accessor};
379             }
380             elsif($isa eq 'ARRAY') {
381 12 100       65 _invalid_parameter($name, "non-numeric index '$accessor' used to access an ARRAY")
382             unless $accessor =~ /^\d+$/;
383              
384 11         47 $object = $object->[$accessor];
385             }
386             else {
387 2   100     13 my $type = $isa || 'type that is not a reference';
388 2         13 _invalid_parameter($name, "cannot use '$accessor' on a $type");
389             }
390             }
391              
392 46         658 $self->{value} = $object;
393             }
394              
395             package Mojolicious::Plugin::FormFields::ScopedField;
396              
397 14     14   46640 use Mojo::Base -strict;
  14         34  
  14         154  
398 14     14   1670 use Carp ();
  14         29  
  14         3409  
399              
400             our @ISA = 'Mojolicious::Plugin::FormFields::Field';
401              
402             my $sep = __PACKAGE__->separator;
403              
404             sub new
405             {
406 33     33   557 my $class = shift;
407 33 50       98 Carp::croak 'object name required' unless $_[1]; # 0 arg is controller instance
408              
409 33         180 my $self = $class->SUPER::new(@_);
410 33         87 $self->{fields} = {};
411 33         85 $self->{errors} = {};
412 33 100       293 $self->{index} = $1 if $self->{name} =~ /\Q$sep\E(\d+)$/;
413              
414 33         209 $self;
415             }
416              
417 2     2   11 sub index { shift->{index} }
418              
419             # This is the caller's view of the object, which can differ from $self->{object}.
420             # For example, given 'user.orders.0.id' {object} will be user and object() will be user.orders.0
421 3     3   21 sub object { shift->_lookup_value }
422              
423             for my $m (qw(checkbox fields file hidden input label password radio select text textarea check filter is_equal)) {
424 14     14   76 no strict 'refs';
  14         30  
  14         9384  
425             *$m = sub {
426 37     37   15931 my $self = shift;
427 37         58 my $name = shift;
428 37 50       98 Carp::croak 'field name required' unless $name;
429              
430 37 100       104 return $self->_fields($name) if $m eq 'fields';
431              
432 33         97 my $field = $self->_field($name);
433 33         348 $self->{fields}->{$name} = 1;
434              
435 33 50       88 return $field->error if $m eq 'error';
436              
437             # TODO: compare things in different scopes, e.g., user.password => account.password
438 33 100       73 return $field->$m($self->_path(shift), @_) if $m eq 'is_equal';
439              
440 31         116 $field->$m(@_);
441             };
442             }
443              
444             sub errors
445             {
446 9     9   17 my ($self, $name) = @_;
447 9 100       51 $name ? $self->_field($name)->error : $self->{errors};
448             }
449              
450             sub valid
451             {
452 12     12   48 my ($self, $name) = @_;
453 12 50       48 return $self->_field($name)->valid if $name;
454              
455 12         28 $self->{errors} = {};
456              
457 12         25 my $valid = 1;
458 12         23 for my $name (keys %{$self->{fields}}) {
  12         66  
459 15         42 my $field = $self->_field($name);
460 15 100       173 unless($field->valid) {
461 7         14 $valid = 0;
462 7         22 $self->{errors}->{$name} = $field->error;
463             }
464             }
465              
466 12         57 $valid;
467             }
468              
469             our $AUTOLOAD;
470             sub AUTOLOAD
471             {
472 12     12   131 my $self = shift;
473 12         19 my $name = shift;
474 12 50       30 Carp::croak 'field name required' unless $name;
475              
476 12         108 (my $method = $AUTOLOAD) =~ s/[^':]+:://g;
477 12         51 $self->_field($name)->$method(@_);
478             # Some hacky shit going on here...
479 12         40 $self->{fields}->{$name} = 1;
480              
481 12         56 $self;
482             }
483              
484             sub _field
485             {
486 62     62   89 my ($self, $name) = @_;
487 62         378 $self->{c}->field($self->_path($name), $self->{object});
488             }
489              
490             sub _fields
491             {
492 4     4   8 my ($self, $name) = @_;
493 4         21 $self->{c}->fields($self->_path($name), $self->{object});
494             }
495              
496             1;
497              
498             __END__