File Coverage

blib/lib/MVC/Neaf/X/Form.pm
Criterion Covered Total %
statement 49 50 98.0
branch 13 14 92.8
condition 5 9 55.5
subroutine 10 10 100.0
pod 5 5 100.0
total 82 88 93.1


line stmt bran cond sub pod time code
1             package MVC::Neaf::X::Form;
2              
3 6     6   69282 use strict;
  6         21  
  6         175  
4 6     6   47 use warnings;
  6         14  
  6         302  
5             our $VERSION = '0.2901';
6              
7             =head1 NAME
8              
9             MVC::Neaf::X::Form - Form validator for Not Even A Framework
10              
11             =head1 CAUTION
12              
13             This module should be moved into a separate distribution or (ideally)
14             merged with an existing module with similar functionality.
15              
16             Possible candidates include L, L,
17             L, and more.
18              
19             =head1 DESCRIPTION
20              
21             Ths module provides hashref validation mechanism that allows for
22             showing per-value errors,
23             post-validation user-defined checks,
24             and returning the original content for resubmission.
25              
26             =head1 SINOPSYS
27              
28             use MVC::Neaf::X::Form;
29              
30             # At the start of the application
31             my $validator = MVC::Neaf::X::Form->new( \%profile );
32              
33             # Much later, multiple times
34             my $form = $validator->validate( \%user_input );
35              
36             if ($form->is_valid) {
37             do_intended_stuff( $form->data ); # a hashref
38             } else {
39             display_errors( $form->error ); # a hashref
40             show_form_for_resubmission( $form->raw ); # also a hashref
41             };
42              
43             As you can see, nothing here has anything to do with http or html,
44             it just so happens that the above pattern is common in web applications.
45              
46             =head1 METHODS
47              
48             =cut
49              
50 6     6   463 use parent qw(MVC::Neaf::X);
  6         313  
  6         49  
51 6     6   2902 use MVC::Neaf::X::Form::Data;
  6         20  
  6         3664  
52              
53             =head2 new( \%profile )
54              
55             Receives a validation profile, returns a validator object.
56              
57             In the default implementation,
58             %profile must be a hash with keys corresponding to the data being validated,
59             and values in the form of either regexp, [ regexp ], or [ required => regexp ].
60              
61             Regular expressions are accepted in qr(...) and string format, and will be
62             compiled to only match the whole line.
63              
64             B One may need to pass qr(...)s in order to allow multiline data
65             (e.g. in textarea).
66              
67             B Format may be subject to extention with extra options.
68              
69             =cut
70              
71             sub new {
72             # TODO 0.90 other constructor forms e.g. with options
73 3     3 1 162 my ($class, $profile) = @_;
74              
75 3         17 my $self = bless {
76             known_keys => [ keys %$profile ],
77             }, $class;
78              
79 3         13 $self->{rules} = $self->make_rules( $profile );
80 3         196 return $self;
81             };
82              
83             =head2 make_rules( \%profile )
84              
85             Preprocesses the validation profile before doing actual validation.
86              
87             Returns an object or reference to be stored in the C property.
88              
89             This method is called from new() and is to be overridden in a subclass.
90              
91             =cut
92              
93             sub make_rules {
94 2     2 1 5 my ($self, $profile) = @_;
95              
96 2         4 my %regexp;
97             my %required;
98              
99 2         6 foreach (keys %$profile) {
100 6         12 my $spec = $profile->{$_};
101 6 100       16 if (ref $spec eq 'ARRAY') {
102 3 100 33     21 if (@$spec == 1) {
    50          
103 1         3 $regexp{$_} = _mkreg( $spec->[-1] );
104             } elsif (@$spec == 2 and lc $spec->[0] eq 'required') {
105 2         5 $regexp{$_} = _mkreg( $spec->[-1] );
106 2         11 $required{$_}++;
107             } else {
108 0         0 $self->my_croak("Invalid validation profile for value $_");
109             };
110             } else {
111             # plain or regexp
112 3         7 $regexp{$_} = _mkreg( $spec );
113             };
114             };
115              
116 2         23 return { regexp => \%regexp, required => \%required };
117             };
118              
119             sub _mkreg {
120 6     6   9 my $str = shift;
121 6         204 return qr/^$str$/;
122             };
123              
124             =head2 validate( \%data )
125              
126             Returns a MVC::Neaf::X::Form::Data object with methods:
127              
128             =over
129              
130             =item * is_valid - true if validation passed.
131              
132             =item * data - data that passed validation as hash
133             (MAY be incomplete, must check is_valid() before usage).
134              
135             =item * error - errors encountered.
136             May be extended if called with 2 args.
137             (E.g. failed to load an otherwise correct item from DB).
138             This also affects is_valid.
139              
140             =item * raw - user params as is. Only the known keys end up in this hash.
141             Useful to send data back for resubmission.
142              
143             =back
144              
145             =cut
146              
147             sub validate {
148 9     9 1 1164 my ($self, $data) = @_;
149              
150 9         17 my $raw;
151             defined $data->{$_} and $raw->{$_} = $data->{$_}
152 9   66     20 for $self->known_keys;
153              
154 9         34 my ($clean, $error) = $self->do_validate( $raw );
155              
156 9         528 return MVC::Neaf::X::Form::Data->new(
157             raw => $raw, data=>$clean, error => $error,
158             );
159             };
160              
161             =head2 do_validate( $raw_data )
162              
163             Returns a pair of hashes: the cleaned data and errors.
164              
165             This is called by validate() and is to be overridden in subclasses.
166              
167             =cut
168              
169             sub do_validate {
170 6     6 1 11 my ($self, $data) = @_;
171              
172 6         12 my $rex = $self->{rules}{regexp};
173 6         10 my $must = $self->{rules}{required};
174 6         8 my (%clean, %error);
175 6         11 foreach ( $self->known_keys ) {
176 18 100       38 if (!defined $data->{$_}) {
177 8 100       18 $error{$_} = 'REQUIRED' if $must->{$_};
178 8         15 next;
179             };
180              
181 10 100 66     80 if ($data->{$_} =~ $rex->{$_}) {
    100          
182 7         16 $clean{$_} = $data->{$_};
183             } elsif (length $data->{$_} or $must->{$_}) {
184             # Silently skip empty values if they don't match RE
185             # so that /foo?bar= and /foo work the same
186             # (unless EXPLICITLY told NOT to)
187 2         5 $error{$_} = 'BAD_FORMAT';
188             };
189             };
190              
191 6         20 return (\%clean, \%error);
192             };
193              
194             =head2 known_keys()
195              
196             Returns list of data keys subject to validation.
197              
198             All other keys present in the input SHOULD be ignored.
199              
200             =cut
201              
202             sub known_keys {
203 15     15 1 24 my $self = shift;
204 15         20 return @{ $self->{known_keys} };
  15         77  
205             };
206              
207             =head1 LICENSE AND COPYRIGHT
208              
209             This module is part of L suite.
210              
211             Copyright 2016-2023 Konstantin S. Uvarin C.
212              
213             This program is free software; you can redistribute it and/or modify it
214             under the terms of either: the GNU General Public License as published
215             by the Free Software Foundation; or the Artistic License.
216              
217             See L for more information.
218              
219             =cut
220              
221             1;