File Coverage

blib/lib/Dancer2/Plugin/FormValidator.pm
Criterion Covered Total %
statement 74 84 88.1
branch 9 14 64.2
condition n/a
subroutine 21 22 95.4
pod 3 4 75.0
total 107 124 86.2


line stmt bran cond sub pod time code
1              
2             use 5.24.0;
3 10     10   6660834 use strict;
  10         143  
4 10     10   47 use warnings;
  10         14  
  10         180  
5 10     10   35  
  10         16  
  10         220  
6             use Dancer2::Plugin;
7 10     10   5123 use Dancer2::Core::Hook;
  10         307225  
  10         83  
8 10     10   62623 use Dancer2::Plugin::FormValidator::Config;
  10         20  
  10         205  
9 10     10   4485 use Dancer2::Plugin::FormValidator::Factory::Extensions;
  10         36  
  10         314  
10 10     10   4082 use Dancer2::Plugin::FormValidator::Registry;
  10         29  
  10         306  
11 10     10   3538 use Dancer2::Plugin::FormValidator::Input;
  10         34  
  10         295  
12 10     10   3750 use Dancer2::Plugin::FormValidator::Processor;
  10         36  
  10         301  
13 10     10   3947 use Types::Standard qw(InstanceOf);
  10         31  
  10         373  
14 10     10   65  
  10         17  
  10         72  
15             our $VERSION = '1.04';
16              
17             plugin_keywords qw(validate validated errors);
18              
19             has validator_config => (
20             is => 'ro',
21             isa => InstanceOf['Dancer2::Plugin::FormValidator::Config'],
22             lazy => 1,
23             builder => sub {
24             return Dancer2::Plugin::FormValidator::Config->new(
25 10     10   67461 config => $_[0]->config,
26             );
27             }
28             );
29              
30             has registry => (
31             is => 'ro',
32             isa => InstanceOf['Dancer2::Plugin::FormValidator::Registry'],
33             lazy => 1,
34             default => sub {
35             my $factory = Dancer2::Plugin::FormValidator::Factory::Extensions->new(
36             plugin => $_[0],
37             extensions => $_[0]->config->{extensions} // {},
38             );
39              
40             return Dancer2::Plugin::FormValidator::Registry->new(
41             extensions => $factory->build,
42             );
43             }
44             );
45              
46             # Var for saving last success validation valid input.
47             has valid => (
48             is => 'rwp',
49             clearer => 1,
50             );
51              
52             $_[0]->_register_hooks;
53             return;
54 11     11 0 55214 }
55 11         75  
56             my ($self, %args) = @_;
57              
58             # We need to delete old data in session if it wasn't collected.
59 11     11 1 405358 $self->_clear_session;
60              
61             # We need to unset value of this var (if there was something).
62 11         172 $self->clear_valid;
63              
64             # Arguments.
65 11         168 # Arguments.
66             my $profile = $args{profile};
67             my $input = $args{input};
68             my $lang = $args{lang};
69 11         66  
70 11         31 if (not defined $input) {
71 11         23 my $request = $self->app->request;
72              
73 11 50       41 if ($request->is_get) {
74 11         77 $input = $request->query_parameters->as_hashref_mixed;
75             }
76 11 100       60 elsif($request->is_post) {
    50          
77 1         23 $input = $request->body_parameters->as_hashref_mixed;
78             }
79             }
80 10         240  
81             if (defined $lang) {
82             $self->_validator_language($lang);
83             }
84 11 100       838  
85 1         5 my $processor = Dancer2::Plugin::FormValidator::Processor->new(
86             input => Dancer2::Plugin::FormValidator::Input->new(input => $input),
87             profile => $profile,
88 11         135 config => $self->validator_config,
89             registry => $self->registry,
90             );
91              
92             my $result = $processor->run;
93              
94             if ($result->success != 1) {
95 11         45413 $self->_set_session({
96             messages => $result->messages,
97 11 100       23849 old => $input,
98 8         109 });
99             return undef;
100             }
101             else {
102 8         401 $self->_set_valid($result->valid);
103              
104             return $self->valid;
105 3         32 }
106             }
107 3         149  
108             return $_[0]->valid;
109             }
110              
111             if (my $session = $_[0]->_get_session) {
112 2     2 1 27 return $session->{messages}
113             }
114             return undef;
115             }
116 7 50   7 1 127  
117             # Register Dancer2 hook to add custom template tokens: errors, old.
118 7         120 my ($self) = @_;
119 0         0  
120             $self->app->add_hook(
121             Dancer2::Core::Hook->new(
122             name => 'before_template_render',
123             code => sub {
124 11     11   27 my ($tokens) = @_;
125              
126             my $errors = {};
127             my $old = {};
128              
129             if (my $session = $self->_get_session) {
130 0     0   0 $errors = $session->{messages};
131             $old = $session->{old};
132 0         0 }
133 0         0  
134             $tokens->{errors} = $errors;
135 0 0       0 $tokens->{old} = $old;
136 0         0  
137 0         0 return;
138             },
139             )
140 0         0 );
141 0         0  
142             return;
143 0         0 }
144              
145             # Set validator to language to $lang.
146 11         295 my ($self, $lang) = @_;
147              
148 11         876289 $self->validator_config->language($lang);
149             return;
150             }
151              
152             my ($self, $value) = @_;
153 1     1   4  
154             $self->app->session->write(
155 1         16 $self->validator_config->session_namespace,
156 1         47 $value,
157             );
158              
159             return;
160 8     8   39 }
161              
162 8         256 my ($self) = @_;
163              
164             my $session = $self->app->session->read(
165             $self->validator_config->session_namespace,
166             );
167 8         1055  
168             $self->_clear_session;
169              
170             return $session;
171 7     7   22 }
172              
173 7         141 my ($self) = @_;
174              
175             $self->app->session->delete(
176             $self->validator_config->session_namespace,
177 7         324 );
178              
179 7         36 return;
180             }
181              
182             1;
183 18     18   60  
184             # ABSTRACT: Dancer2 validation framework.
185 18         357  
186             =pod
187              
188             =encoding UTF-8
189 18         2282  
190             =head1 NAME
191              
192             Dancer2::Plugin::FormValidator - neat and easy to start form validation plugin for Dancer2.
193              
194             =head1 VERSION
195              
196             version 1.04
197              
198             =head1 SYNOPSIS
199              
200             ### If you need a simple and easy validation in your project,
201             ### This module is what you need.
202              
203             use Dancer2;
204             use Dancer2::Plugin::FormValidator;
205              
206             ### First create form validation profile class.
207              
208             package RegisterForm {
209             use Moo;
210             with 'Dancer2::Plugin::FormValidator::Role::Profile';
211              
212             ### Here you need to declare fields => validators.
213              
214             sub profile {
215             return {
216             username => [ qw(required alpha_num length_min:4 length_max:32) ],
217             email => [ qw(required email length_max:127) ],
218             password => [ qw(required length_max:40) ],
219             password_cnf => [ qw(required same:password) ],
220             confirm => [ qw(required accepted) ],
221             };
222             }
223             }
224              
225             ### Now you can use it in your Dancer2 project.
226              
227             post '/form' => sub {
228             if (validate profile => RegisterForm->new) {
229             my $valid_hash_ref = validated;
230              
231             save_user_input($valid_hash_ref);
232             redirect '/success_page';
233             }
234              
235             redirect '/form';
236             };
237              
238             The html result could be like:
239              
240             =begin html
241              
242             <p>
243             <img alt="Screenshot register form" src="https://raw.githubusercontent.com/AlexP007/dancer2-plugin-formvalidator/main/assets/screenshot_register.png" width="500px">
244             </p>
245              
246             =end html
247              
248             =head1 DESCRIPTION
249              
250             This is micro-framework that provides validation in your Dancer2 application.
251             It consists of dsl's keywords: validate, validated, errors.
252             It has a set of built-in validators that can be extended by compatible modules (extensions).
253             Also proved runtime switching between languages, so you can show proper error messages to users.
254              
255             This module has a minimal set of dependencies and does not require the mandatory use of DBIc or Moose.
256              
257             Uses simple and declarative approach to validate forms.
258              
259             =head2 Validator
260              
261             First, you need to create class which will implements
262             at least one main role: Dancer2::Plugin::FormValidator::Role::Profile.
263              
264             This role requires profile method which should return a I<HashRef> Data::FormValidator accepts:
265              
266             package RegisterForm
267              
268             use Moo;
269             with 'Dancer2::Plugin::FormValidator::Role::Profile';
270              
271             sub profile {
272             return {
273             username => [ qw(required alpha_num_ascii length_min:4 length_max:32) ],
274             email => [ qw(required email length_max:127) ],
275             password => [ qw(required length_max:40) ],
276             password_cnf => [ qw(required same:password) ],
277             confirm => [ qw(required accepted) ],
278             };
279             };
280              
281             =head3 Profile method
282              
283             Profile method should always return a I<HashRef[ArrayRef]> where keys are input fields names
284             and values are ArrayRef with list of validators.
285              
286             =head2 Application
287              
288             Then you need to set basic configuration:
289              
290             use Dancer2;
291              
292             set plugins => {
293             FormValidator => {
294             session => {
295             namespace => '_form_validator' # This is required field
296             },
297             },
298             };
299              
300             Now you can validate POST parameters in your controller:
301              
302             use Dancer2;
303             use Dancer2::Plugin::FormValidator;
304             use RegisterForm;
305              
306             post '/register' => sub {
307             if (my $valid_hash_ref = validate profile => RegisterForm->new) {
308             if (login($valid_hash_ref)) {
309             redirect '/success_page';
310             }
311             }
312              
313             redirect '/register';
314             };
315              
316             get '/register' => sub {
317             template 'app/register' => {
318             title => 'Register page',
319             };
320             };
321              
322             =head2 Template
323              
324             In you template you have access to: $errors - this is I<HashRef[ArrayRef]> with fields names as keys
325             and error messages values and $old - contains old input values.
326              
327             Template app/register:
328              
329             <div class="w-3/4 max-w-md bg-white shadow-lg py-4 px-6">
330             <form method="post" action="/register">
331             <div class="py-2">
332             <label class="block font-normal text-gray-400" for="name">
333             Name
334             </label>
335             <input
336             type="text"
337             id="name"
338             name="name"
339             value="<: $old[name] :>"
340             class="border border-2 w-full h-5 px-4 py-5 mt-1 rounded-md
341             hover:outline-none focus:outline-none focus:ring-1 focus:ring-indigo-100"
342             >
343             <: for $errors[name] -> $error { :>
344             <small class="pl-1 text-red-400"><: $error :></small>
345             <: } :>
346             </div>
347             <div class="py-2">
348             <label class="block font-normal text-gray-400" for="email">
349             Email
350             </label>
351             <input
352             type="text"
353             id="email"
354             name="email"
355             value="<: $old[email] :>"
356             class="border border-2 w-full h-5 px-4 py-5 mt-1 rounded-md
357             hover:outline-none focus:outline-none focus:ring-1 focus:ring-indigo-100"
358             >
359             <: for $errors[email] -> $error { :>
360             <small class="pl-1 text-red-400"><: $error :></small>
361             <: } :>
362              
363             <!-- Other fields -->
364             ...
365             ...
366             ...
367             <!-- Other fields end -->
368              
369             </div>
370             <button
371             type="submit"
372             class="mt-4 bg-sky-600 text-white py-2 px-6 rounded-md hover:bg-sky-700"
373             >
374             Register
375             </button>
376             </form>
377             </div>
378              
379             =head1 CONFIGURATION
380              
381             ...
382             plugins:
383             FormValidator:
384             session:
385             namespace: '_form_validator' # this is default
386             messages:
387             language: en # this is default
388             ucfirst: 1 # this is default
389             validators:
390             required:
391             en: %s is needed from config # custom en message
392             de: %s ist erforderlich # custom de message
393             ...
394             extensions:
395             dbic:
396             provider: Dancer2::Plugin::FormValidator::Extension::DBIC
397             ...
398             ...
399              
400             =head2 session
401              
402             =head3 namespace
403              
404             Session storage key where this module stores data, like: errors or old vars.
405              
406             =head2 messages
407              
408             =head3 language
409              
410             Default language for error messages.
411              
412             =head3 ucfirst
413              
414             Apply ucfirst function to messages or not.
415              
416             =head3 validators
417              
418             Key => values, where key is validator name and value is messages
419             dictionary for different languages.
420              
421             =head2 extensions
422              
423             Key => values, where key is extension short name and values is its configuration.
424              
425             =head1 DSL KEYWORDS
426              
427             =head3 validate
428              
429             validate(Hash %args): HashRef|undef
430              
431             Accept arguments as hash:
432              
433             (
434             profile => Object implementing Dancer2::Plugin::FormValidator::Role::Profile # required
435             input => HashRef of values to validate, default is body_parameters->as_hashref_mixed
436             lang => Accepts two-lettered language id, default is 'en'
437             )
438              
439             Profile is required, input and lang is optional.
440              
441             Returns valid input I<HashRef> if validation succeed, otherwise returns undef.
442              
443             ### You can use HashRef returned from validate.
444              
445             if (my $valid_hash_ref = validate profile => RegisterForm->new) {
446             # Success, data is valid.
447             }
448              
449              
450             ### Or more declarative approach with validated keyword.
451              
452             if (validate profile => RegisterForm->new) {
453             # Success, data is valid.
454             my $valid_hash_ref = validated;
455              
456             # Do some operations...
457             }
458             else {
459             # Error, data is invalid.
460             my $errors = errors; # errors keyword returns error messages.
461              
462             # Redirect or show errors...
463             }
464              
465             =head3 validated
466              
467             validated(): HashRef|undef
468              
469             No arguments.
470             Returns valid input I<HashRef> if validate succeed.
471             I<Undef> value will be returned after first call within one validation process.
472              
473             my $valid_hash_ref = validated;
474              
475             =head3 errors
476              
477             errors(): HashRef
478              
479             No arguments.
480             Returns I<HashRef[ArrayRef]> if validation failed.
481              
482             my $errors_hash_multi = errors;
483              
484             =head1 Validators
485              
486             =head3 accepted
487              
488             accepted(): Bool
489              
490             Validates that field B<exists> and one of the listed: (yes on 1).
491              
492             field => [ qw(accepted) ]
493              
494             =head3 alpha
495              
496             alpha(Str $encoding = 'a'): Bool
497              
498             Validate that string only contain of alphabetic symbols.
499             By default encoding is ascii, i.e B</^[[:alpha:]]+$/a>.
500              
501             field => [ qw(alpha) ]
502              
503             To set encoding to unicode you need to pass 'u' argument:
504              
505             field => [ qw(alpha:u) ]
506              
507             Then the validation rule will be B</^[[:alpha:]]+$/>.
508              
509             =head3 alpha_num
510              
511             alpha_num(Str $encoding = 'a'): Bool
512              
513             Validate that string only contain of alphabetic symbols, underscore and numbers 0-9.
514             By default encoding is ascii, i.e. B</^\w+$/a>.
515              
516             field => [ qw(alpha_num) ]
517              
518             To set encoding to unicode you need to pass 'u' argument:
519              
520             field => [ qw(alpha_num:u) ]
521              
522             Rule will be B</^\w+$/>.
523              
524             =head3 boolean
525              
526             boolean(): Bool
527              
528             Validate that field is 0 or 1 scalar value.
529              
530             field => [ qw(boolean) ]
531              
532             =head3 email
533              
534             email(): Bool
535              
536             Validate that field is valid email(B<rfc822>).
537              
538             field => [ qw(email) ]
539              
540             =head3 email_dns
541              
542             email_dns(): Bool
543              
544             Validate that field is valid email(B<rfc822>) and dns exists.
545              
546             field => [ qw(email_dns) ]
547              
548             =head3 enum
549              
550             enum(Array @values): Bool
551              
552             Validate that field is one of listed values.
553              
554             field => [ qw(enum:value1,value2) ]
555              
556             =head3 integer
557              
558             integer(): Bool
559              
560             Validate that field is integer.
561              
562             field => [ qw(integer) ]
563              
564             =head3 length_max
565              
566             length_max(Int $num): Bool
567              
568             Validate that string length <= num.
569              
570             field => [ qw(length_max:32) ]
571              
572             =head3 length_min
573              
574             length_min(Int $num): Bool
575              
576             Validate that string length >= num.
577              
578             field => [ qw(length_max:4) ]
579              
580             =head3 max
581              
582             max(Int $num): Bool
583              
584             Validate that field is number <= num.
585              
586             field => [ qw(max:32) ]
587              
588             =head3 min
589              
590             min(Int $num): Bool
591              
592             Validate that field is number >= num.
593              
594             field => [ qw(min:4) ]
595              
596             =head3 numeric
597              
598             numeric(): Bool
599              
600             Validate that field is number.
601              
602             field => [ qw(numeric) ]
603              
604             =head3 required
605              
606             required(): Bool
607              
608             Validate that field exists and not empty string.
609              
610             field => [ qw(required) ]
611              
612             =head3 required_with
613              
614             required_with(Str $field_name): Bool
615              
616             Validate that field exists and not empty string if another field is exists and not empty.
617              
618             field_1 => [ qw(required) ]
619             field_2 => [ qw(required_with:field_1) ]
620              
621             =head3 same
622              
623             same(Str $field_name): Bool
624              
625             Validate that field is exact value as another.
626              
627             field_1 => [ qw(required) ]
628             field_2 => [ qw(required same:field_1) ]
629              
630             =head1 CUSTOM MESSAGES
631              
632             To define custom error messages for fields/validators your Validator should implement
633             Role: Dancer2::Plugin::FormValidator::Role::ProfileHasMessages.
634              
635             package Validator {
636             use Moo;
637             with 'Dancer2::Plugin::FormValidator::Role::ProfileHasMessages';
638              
639             sub profile {
640             return {
641             name => [qw(required)],
642             email => [qw(required email)],
643             };
644             }
645              
646             sub messages {
647             return {
648             name => {
649             required => {
650             en => 'Specify your %s',
651             },
652             },
653             email => {
654             required => {
655             en => '%s is needed',
656             },
657             email => {
658             en => '%s please use valid email',
659             }
660             }
661             };
662             }
663             }
664              
665             =head1 HOOKS
666              
667             There is hook_before method available, which allows your Profile object to make
668             decisions depending on the input data. You could use it with Moo around modifier:
669              
670             around hook_before => sub {
671             my ($orig, $self, $profile, $input) = @_;
672              
673             # If there is specific input value.
674             if ($input->{name} eq 'Secret') {
675             # Delete all validators for field 'surname'.
676             delete $profile->{surname};
677             }
678              
679             return $orig->($self, $profile, $input);
680             };
681              
682             =head1 EXTENSIONS
683              
684             =head2 Writing custom extensions
685              
686             You can extend the set of validators by writing extensions:
687              
688             package Extension {
689             use Moo;
690             with 'Dancer2::Plugin::FormValidator::Role::Extension';
691              
692             sub validators {
693             return {
694             is_true => 'IsTrue', # Full class name
695             email => 'Email', # Full class name
696             restrict => 'Restrict', # Full class name
697             }
698             }
699             }
700              
701             Extension should implement Role: Dancer2::Plugin::FormValidator::Role::Extension.
702              
703             B<Hint:> you could reassign built-in validator with your custom one.
704              
705             Custom validators:
706              
707             package IsTrue {
708             use Moo;
709             with 'Dancer2::Plugin::FormValidator::Role::Validator';
710              
711             sub message {
712             return {
713             en => '%s is not a true value',
714             };
715             }
716              
717             sub validate {
718             my ($self, $field, $input) = @_;
719              
720             if (exists $input->{$field}) {
721             if ($input->{$field} == 1) {
722             return 1;
723             }
724             else {
725             return 0;
726             }
727             }
728              
729             return 1;
730             }
731             }
732              
733             Validator should implement Role: Dancer2::Plugin::FormValidator::Role::Validator.
734              
735             Config:
736              
737             set plugins => {
738             FormValidator => {
739             session => {
740             namespace => '_form_validator'
741             },
742             extensions => {
743             extension => {
744             provider => 'Extension',
745             }
746             }
747             },
748             };
749              
750             =head2 Extensions modules
751              
752             There is a set of ready-made extensions available on cpan:
753              
754             =over 4
755              
756             =item *
757             L<Dancer2::Plugin::FormValidator::Extension::Password|https://metacpan.org/pod/Dancer2::Plugin::FormValidator::Extension::Password>
758             - for validating passwords.
759              
760             =item *
761             L<Dancer2::Plugin::FormValidator::Extension::DBIC|https://metacpan.org/pod/Dancer2::Plugin::FormValidator::Extension::DBIC>
762             - for checking fields existence in table rows.
763              
764             =back
765              
766             =head1 ROLES
767              
768             =over 4
769              
770             =item *
771             Dancer2::Plugin::FormValidator::Role::Profile - for profile classes.
772              
773             =item *
774             Dancer2::Plugin::FormValidator::Role::HasMessages - for classes, that implements custom error messages.
775              
776             =item *
777             Dancer2::Plugin::FormValidator::Role::ProfileHasMessages - brings together Profile and HasMassages.
778              
779             =item *
780             Dancer2::Plugin::FormValidator::Role::Extension - for extension classes.
781              
782             =item *
783             Dancer2::Plugin::FormValidator::Role::Validator - for custom validators.
784              
785             =back
786              
787             =head1 HINTS
788              
789             If you don't want to create separated classes for your validation logic,
790             you could create one base class and reuse it in your project.
791              
792             ### Validator class
793              
794             package Validator {
795             use Moo;
796             with 'Dancer2::Plugin::FormValidator::Role::Profile';
797              
798             has profile_hash => (
799             is => 'ro',
800             required => 1,
801             );
802              
803             sub profile {
804             return $_[0]->profile_hash;
805             }
806             }
807              
808             ### Application
809              
810             use Dancer2
811              
812             my $validator = Validator->new(profile_hash =>
813             {
814             email => [qw(required email)],
815             }
816             );
817              
818             post '/subscribe' => sub {
819             if (not validate profile => $validator) {
820             to_json errors;
821             }
822             };
823              
824             =head1 BUGS AND LIMITATIONS
825              
826             If you find one, please let me know.
827              
828             =head1 SOURCE CODE REPOSITORY
829              
830             L<https://github.com/AlexP007/dancer2-plugin-formvalidator|https://github.com/AlexP007/dancer2-plugin-formvalidator>.
831              
832             =head1 AUTHOR
833              
834             Alexander Panteleev <alexpan at cpan dot org>.
835              
836             =head1 LICENSE AND COPYRIGHT
837              
838             This software is copyright (c) 2022 by Alexander Panteleev.
839             This is free software; you can redistribute it and/or modify it under
840             the same terms as the Perl 5 programming language system itself.
841              
842             =cut