File Coverage

blib/lib/Dancer2/Plugin/FormValidator.pm
Criterion Covered Total %
statement 64 73 87.6
branch 8 12 66.6
condition n/a
subroutine 20 21 95.2
pod 3 4 75.0
total 95 110 86.3


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