File Coverage

blib/lib/Spark/Form.pm
Criterion Covered Total %
statement 152 160 95.0
branch 21 38 55.2
condition 1 5 20.0
subroutine 34 34 100.0
pod 19 19 100.0
total 227 256 88.6


line stmt bran cond sub pod time code
1             package Spark::Form;
2             our $VERSION = '0.2102';
3              
4              
5             # ABSTRACT: A simple yet powerful forms validation system that promotes reuse.
6              
7 28     28   405370 use Moose;
  28         9415536  
  28         172  
8 28     28   163931 use MooseX::AttributeHelpers;
  28         8972907  
  28         1208  
9 28     28   16202 use List::MoreUtils 'all';
  28         221653  
  28         179  
10 28     28   24659 use Data::Couplet ();
  28         34956417  
  28         840  
11 28     28   209 use Carp ();
  28         39  
  28         485  
12 28     28   98 use Scalar::Util qw( blessed );
  28         39  
  28         48127  
13              
14             with qw(MooseX::Clone);
15              
16             has _fields => (
17             isa => 'Data::Couplet',
18             is => 'ro',
19             required => 0,
20             default => sub { Data::Couplet->new },
21             traits => [qw(Clone)],
22             );
23              
24             has plugin_ns => (
25             isa => 'Str',
26             is => 'ro',
27             required => 0,
28             );
29              
30             has _errors => (
31             metaclass => 'Collection::Array',
32             isa => 'ArrayRef',
33             is => 'ro',
34             required => 0,
35             default => sub { [] },
36             provides => {
37             push => '_add_error',
38             elements => 'errors',
39             clear => '_clear_errors',
40             },
41             );
42              
43             has valid => (
44             isa => 'Bool',
45             is => 'rw',
46             required => 0,
47             default => 0,
48             );
49              
50             has '_printer' => (
51             isa => 'Maybe[Str]',
52             required => 0,
53             is => 'ro',
54             init_arg => 'printer',
55             );
56              
57             sub BUILD {
58 28     28 1 50 my ($self) = @_;
59 28         83 my @search_path = (
60              
61             #This will load anything from SparkX::Form::Field
62             'SparkX::Form::Field',
63             );
64 28 100       752 if ($self->plugin_ns) {
65 1         19 unshift @search_path, ($self->plugin_ns);
66             }
67              
68 28         11431 require Module::Pluggable;
69 28 50       171653 eval {
70 28         203 Module::Pluggable->import(
71             search_path => \@search_path,
72             sub_name => 'field_mods',
73             required => 1,
74             );
75             } or Carp::croak("Spark::Form: Could not instantiate Module::Pluggable, $@");
76              
77 28 100       2578 if (defined $self->_printer) {
78              
79 2         40 my $printer = $self->_printer;
80              
81 2 50       4 eval {
82              
83             #Load the module, else short circuit.
84             #There were strange antics with qq{} and this is tidier than the alternative
85 2 50       94 eval "require $printer; 1" or Carp::croak("Require of $printer failed, $@");
86              
87             #Apply the role (failure will short circuit). Return 1 so the 'or' won't trigger
88 2         46 $self->_printer->meta->apply($self);
89              
90 2         13531 1
91             } or Carp::croak("Could not apply printer $printer, $@");
92             }
93 28         648 return;
94             }
95              
96             sub _error {
97 41     41   49 my ($self, $error) = @_;
98              
99 41         820 $self->valid(0);
100 41         119 $self->_add_error($error);
101              
102 41         79 return $self;
103             }
104              
105             sub field_couplet {
106 1     1 1 5 my ($self) = @_;
107 1         22 return $self->_fields;
108             }
109              
110             sub add {
111 26     26 1 6543 my ($self, $item, @args) = @_;
112              
113             #Dispatch to the appropriate handler sub
114              
115             #1. Regular String. Should have a name and any optional args
116 26 100       83 unless (ref $item) {
117 1 50       3 Carp::croak('->add expects [Scalar, List where { items > 0 }] or [Ref].') unless (scalar @args);
118 1         4 $self->_add_by_type($item, @args);
119 0         0 return $self;
120             }
121              
122             #2. Array - loop. This will spectacularly fall over if you are using string-based creation as there's no way to pass multiple names yet
123 25 50       74 if (ref $item eq 'ARRAY') {
124 0         0 $self->add($_, @args) for @{$item};
  0         0  
125 0         0 return $self;
126             }
127              
128             #3. Custom field. Just takes any optional args
129 25 50       75 if ($self->_valid_custom_field($item)) {
130 25         71 $self->_add_custom_field($item, @args);
131 25         95 return $self;
132             }
133              
134             #Unknown thing
135 0   0     0 Carp::croak(q(Spark::Form: Don\'t know what to do with a ) . ref $item . q(/) . (blessed $item|| q()));
136             }
137              
138             sub get {
139 23     23 1 76 my ($self, $key) = @_;
140 23         560 return $self->_fields->value($key);
141             }
142              
143             sub get_at {
144 2     2 1 1424 my ($self, $index) = @_;
145 2         51 return $self->_fields->value_at($index);
146             }
147              
148             sub keys {
149 9     9 1 400 my ($self) = @_;
150 9         223 return $self->_fields->keys();
151             }
152              
153             sub fields {
154 61     61 1 2223 my ($self) = @_;
155 61         1312 return $self->_fields->values;
156             }
157              
158             sub remove {
159 6     6 1 14 my ($self, @keys) = @_;
160 6         140 $self->_fields->unset_key(@keys);
161              
162 6         1979 return $self;
163             }
164              
165             sub remove_at {
166 2     2 1 5 my ($self, @indices) = @_;
167 2         39 $self->_fields->unset_at(@indices);
168              
169 2         221 return $self;
170             }
171              
172             sub validate {
173 36     36 1 82 my ($self) = @_;
174              
175             #Clear out
176 36         819 $self->valid(1);
177 36         123 $self->_clear_errors();
178 36         130 foreach my $field ($self->fields) {
179 64         422 $field->validate;
180 64 100       1731 unless ($field->valid) {
181 30         126 $self->_error($_) foreach $field->errors;
182             }
183             }
184 36         712 return $self->valid;
185             }
186              
187             sub data {
188 33     33 1 2202 my ($self, $fields) = @_;
189 33         46 while (my ($k, $v) = each %{$fields}) {
  89         250  
190 56 50       1284 if ($self->_fields->value($k)) {
191 56         1506 $self->_fields->value($k)->value($v);
192             }
193             }
194              
195 33         51 return $self;
196             }
197              
198             sub _valid_custom_field {
199 25     25   35 my ($self, $thing) = @_;
200 25 0       38 return eval {
201 25         133 $thing->isa('Spark::Form::Field')
202             } or 0;
203             }
204              
205             sub _add_custom_field {
206 25     25   44 my ($self, $item, %opts) = @_;
207              
208             #And add it.
209 25         735 $self->_add($item, $item->name, %opts);
210              
211 25         55 return $self;
212             }
213              
214             sub _add_by_type {
215 1     1   4 my ($self, $type, $name, %opts) = @_;
216              
217             #Default name is type itself
218 1   33     3 $name ||= $type;
219              
220             #Create and add it
221 1         6 $self->_add($self->_create_type($type, $name, %opts), $name);
222              
223 0         0 return $self;
224             }
225              
226             sub _add {
227 25     25   37 my ($self, $field, $name) = @_;
228              
229 25 50       614 Carp::croak("Field name $name exists in form.") if $self->_fields->value($name);
230              
231             #Add it onto the ArrayRef
232 25         934 $self->_fields->set($name, $field);
233              
234 25         7053 return 1;
235             }
236              
237             sub _mangle_modname {
238 18     18   20 my ($self, $mod) = @_;
239              
240             #Strip one or the other. This is the cleanest way.
241             #It also doesn't matter that class may be null
242 18         19 my @namespaces = (
243             'SparkX::Form::Field',
244             'Spark::Form::Field',
245             );
246              
247 18 50       325 push @namespaces, $self->plugin_ns if $self->plugin_ns;
248              
249 18         17 foreach my $ns (@namespaces) {
250 18 50       60 last if $mod =~ s/^${ns}:://;
251             }
252              
253             #Regulate.
254 18         21 $mod =~ s/::/-/g;
255 18         15 $mod = lc $mod;
256              
257 18         38 return $mod;
258             }
259              
260             sub _find_matching_mod {
261 1     1   2 my ($self, $wanted) = @_;
262              
263             #Just try and find something that, when mangled, eq $wanted
264 1         3 foreach my $mod ($self->field_mods) {
265 18 50       2833 return $mod if $self->_mangle_modname($mod) eq $wanted;
266             }
267              
268             #Cannot find
269 1         203 return 0;
270             }
271              
272             sub _create_type {
273 1     1   2 my ($self, $type, $name, %opts) = @_;
274 1 50       3 my $mod = $self->_find_matching_mod($type) or Carp::croak("Could not find field mod: $type");
275 0 0       0 eval qq{ use $mod; 1 } or Carp::croak("Could not load $mod, $@");
276              
277 0         0 return $mod->new(name => $name, form => $self, %opts);
278             }
279              
280             sub clone_all {
281 9     9 1 839 my ($self) = @_;
282 9         38 my $new = $self->clone;
283 9         237290 $_->form($self) foreach $new->fields;
284              
285 9         27 return $new;
286             }
287              
288             sub clone_except_names {
289 4     4 1 9 my ($self, @fields) = @_;
290 4         12 my $new = $self->clone_all;
291 4         18 $new->remove($_) foreach @fields;
292              
293 4         21 return $new;
294             }
295              
296             #
297             # ->_except( \@superset , \@things_to_get_rid_of )
298             #
299              
300             sub _except {
301 2     2   6 my ($self, $input_list, $exclusion_list) = @_;
302 2         3 my %d;
303 2         3 @d{@{$exclusion_list}} = ();
  2         10  
304              
305             return grep {
306 8         16 !exists $d{$_}
307 2         3 } @{$input_list};
  2         5  
308             }
309              
310             sub clone_only_names {
311 1     1 1 1551 my ($self, @fields) = @_;
312 1         5 my @all = $self->keys;
313 1         712 my @excepted = $self->_except(\@all, \@fields);
314 1         4 return $self->clone_except_names(@excepted);
315             }
316              
317             sub clone_except_ids {
318 2     2 1 1027 my ($self, @ids) = @_;
319 2         5 my $new = $self->clone_all;
320 2         7 $new->remove_at(@ids);
321              
322 2         9 return $new;
323             }
324              
325             sub clone_only_ids {
326 1     1 1 1008 my ($self, @ids) = @_;
327 1         27 my @all = $self->_fields->indices;
328              
329 1         16 return $self->clone_except_ids($self->_except(\@all, \@ids));
330             }
331              
332             sub clone_if {
333 1     1 1 987 my ($self, $sub) = @_;
334 1         27 my (@all) = ($self->_fields->key_values_paired);
335 1         15 my $i = 0 - 1;
336              
337             # Filter out items that match
338             # coderef->( $current_index, $key, $value );
339             @all = grep {
340 1         2 $i++;
  4         10  
341 4         3 !$sub->($i, @{$_});
  4         6  
342             } @all;
343              
344 1         5 return $self->clone_except_names(map { $_->[0] } @all);
  1         4  
345             }
346              
347             sub clone_unless {
348 1     1 1 1134 my ($self, $sub) = @_;
349 1         27 my (@all) = $self->_fields->key_values_paired;
350 1         15 my $i = 0 - 1;
351              
352             # Filter out items that match
353             # coderef->( $current_index, $key, $value );
354              
355             @all = grep {
356 1         2 $i++;
  4         8  
357 4         4 $sub->($i, @{$_});
  4         8  
358             } @all;
359              
360 1         6 return $self->clone_except_names(map { $_->[0] } @all);
  1         3  
361             }
362              
363             sub compose {
364 1     1 1 438 my ($self, $other) = @_;
365 1         5 my $new = $self->clone_all;
366 1         4 my $other_new = $other->clone_all;
367 1         4 foreach my $key ($other_new->keys) {
368 2 50       277 unless ($new->get($key)) {
369 2         43 $new->add($other_new->get($key));
370             }
371             }
372 1         25 return $new;
373             }
374              
375             __PACKAGE__->meta->make_immutable;
376              
377             1;
378              
379              
380              
381              
382             =pod
383              
384             =head1 NAME
385              
386             Spark::Form - A simple yet powerful forms validation system that promotes reuse.
387              
388             =head1 VERSION
389              
390             version 0.2102
391              
392             =head1 SYNOPSIS
393              
394             use Spark::Form;
395             use CGI; #Because it makes for a quick and oversimplistic example
396             use Third::Party::Field;
397             $form = Spark::Form->new(plugin_ns => 'MyApp::Field');
398             # Add a couple of inbuilt modules
399             $form->add('email','email',confirm_field => 'email-confirm')
400             ->add('email','email-confirm')
401             ->add('password','password',regex => qr/^\S{6,}$/),
402             #This one will be autoloaded from MyApp::Field::Username
403             ->add('username','username')
404             # And this shows how you can use a third party field of any class name
405             ->add(Third::Party::Field->new(name => 'blah'));
406             #Pass in a HashRef of params to populate the virtual form with data
407             $form->data(CGI->new->params);
408             #And do the actual validation
409             if ($form->validate) {
410             print "You are now registered";
411             } else {
412             print join "\n", $form->errors;
413             }
414              
415             and over in MyApp/Field/Username.pm...
416              
417             package MyApp::Form::Field::Username;
418             use base Spark::Form::Field;
419              
420             sub _validate {
421              
422             my ($self,$v) = @_;
423              
424             if (length $v < 6 or length $v > 12) {
425             $self->error("Usernames must be 6-12 characters long");
426             } elsif ($v =~ /[^a-zA-Z0-9_-]/) {
427             $self->error("Usernames may contain only a-z,A-Z,0-9, _ and -");
428             } else {
429             $self->error(undef);
430             }
431             $self->valid(!!$self->error());
432             }
433              
434             =head1 INSTABILITY
435              
436             Periodically the API may break. I'll try to make sure it's obvious so it doesn't silently malfunction.
437              
438             By 0.5, we shouldn't have to do this.
439              
440             =head1 DEPENDENCIES
441              
442             Moose. I've dropped using Any::Moose. If you need the performance increase, perhaps it's time to start thinking about shifting off CGI.
443              
444             =head1 METHODS
445              
446             =head2 import (%options)
447              
448             Allows you to set some options for the forms class.
449              
450             =over 4
451              
452             =item class => String
453              
454             Optional, gives the basename for searching for form plugins.
455              
456             Given 'MyApp', it will try to load form plugins from MyApp::Form::Field::*
457              
458             =item source => String
459              
460             Optional, names a plugin to try and extract form data from.
461              
462             If unspecified, you will need to call $form->data(\%data);
463              
464             =back
465              
466             =head2 add ($thing,@rest)
467              
468             If $thing is a string, attempts to instantiate a plugin of that type and add it
469             to the form. Requires the second argument to be a string name for the field to identify it in the form. Rest will become %kwargs
470             If it is an ArrayRef, it loops over the contents (Useful for custom fields, will probably result in bugs for string field names).@rest will be passed in each iteration.
471             If it looks sufficiently like a field (implements Spark::Form::Field),
472             then it will add it to the list of fields. @rest will just become %kwargs
473              
474             Uses 'field name' to locate it from the data passed in.
475              
476             This is a B<streaming interface>, it returns the form itself.
477              
478             =head2 validate
479              
480             Validates the form. Sets C<valid> and then also returns the value.
481              
482             =head2 data
483              
484             Allows you to pass in a HashRef of data to populate the fields with before validation. Useful if you don't use a plugin to automatically populate the data.
485              
486             This is a B<streaming interface>, it returns the form itself.
487              
488             =head2 fields () => Fields
489              
490             Returns a list of Fields in the form in their current order
491              
492             =head2 BUILD
493              
494             Moose constructor. Test::Pod::Coverage made me do it.
495             Adds C<class> to the search path for field modules.
496              
497             =head2 get (Str)
498              
499             Returns the form field of that name
500              
501             =head2 get_at (Int)
502              
503             Returns the form field at that index (counting from 0)
504              
505             =head2 keys () :: Array
506              
507             Returns the field names
508              
509             =head2 field_couplet () :: Data::Couplet
510              
511             Returns the Data::Couplet used to store the fields. Try not to use this too much.
512              
513             =head2 remove (Array[Str]) :: Spark::Form
514              
515             Removes the field(s) bearing the given name(s) from the form object. Silently no-ops any that do not exist.
516              
517             =head2 remove_at (Array[Int]) :: Spark::Form
518              
519             Removes the field at the given ID(s) from the form object. Silently no-ops any that do not exist.
520              
521             WARNING: Things will get re-ordered when you do this. If you have a form with
522             IDs 0..3 and you remove (1, 3), then (0, 2) will remain but they will now be
523             (0, 1) as L<Data::Couplet> will move them to keep a consistent array.
524              
525             =head2 clone_all () :: Spark::Form
526              
527             Returns a new copy of the form with freshly instantiated fields.
528              
529             =head2 clone_except_names (Array[Str]) :: Spark::Form
530              
531             Clones, removing the fields with the specified names.
532              
533             =head2 clone_only_names (Array[Str]) :: Spark::Form
534              
535             Clones, removing the fields without the specified names.
536              
537             =head2 clone_except_ids (Array[Int]) :: Spark::Form
538              
539             Clones, removing the fields with the specified IDs.
540              
541             =head2 clone_only_ids (Array[Int]) :: Spark::Form
542              
543             Clones, removing the fields without the specified IDs.
544              
545             =head2 clone_if (SubRef[(Int, Str, Any) -> Bool]) :: Spark::Form
546              
547             Clones, removing items for which the sub returns false. Sub is passed (Id, Key, Value).
548              
549             =head2 clone_unless (SubRef[(Int, Str, Any) -> Bool]) :: Spark::Form
550              
551             Clones, removing items for which the sub returns true. Sub is passed (Id, Key, Value).
552              
553             =head2 compose (Spark::Form) :: Spark::Form
554              
555             Clones the current form object and copies fields from the supplied other form to the end of that form.
556             Where names clash, items on the current form take priority.
557              
558             =head1 Docs?
559              
560             L<http://sparkengine.org/docs/forms/>
561              
562             =head2 Source?
563              
564             L<http://github.com/jjl/Spark-Form/>
565              
566             =head1 THANKS
567              
568             Thanks to the Django Project, whose forms module gave some inspiration.
569              
570             =head1 SEE ALSO
571              
572             The FAQ: L<Spark::Form::FAQ>
573             L<Data::Couplet> used to hold the fields (see C<field_couplet>)
574              
575              
576              
577             =head1 AUTHOR
578              
579             James Laver L<http://jameslaver.com>
580              
581             =head1 COPYRIGHT AND LICENSE
582              
583             This software is copyright (c) 2009 by James Laver C<< <sprintf qw(%s@%s.%s cpan jameslaver com)> >>.
584              
585             This is free software; you can redistribute it and/or modify it under
586             the same terms as the Perl 5 programming language system itself.
587              
588             =cut
589              
590              
591              
592             __END__
593