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