File Coverage

lib/Data/Processor.pm
Criterion Covered Total %
statement 109 116 93.9
branch 30 42 71.4
condition 19 34 55.8
subroutine 23 24 95.8
pod 7 8 87.5
total 188 224 83.9


line stmt bran cond sub pod time code
1             package Data::Processor;
2              
3 19     19   1204557 use strict;
  19         188  
  19         473  
4 19     19   484 use 5.010_001;
  19         48  
5             our $VERSION = '1.0.9';
6              
7 19     19   86 use Carp;
  19         25  
  19         1233  
8 19     19   124 use Scalar::Util qw(blessed);
  19         50  
  19         829  
9 19     19   6185 use Data::Processor::Error::Collection;
  19         43  
  19         502  
10 19     19   6910 use Data::Processor::Validator;
  19         44  
  19         585  
11 19     19   114 use Data::Processor::Transformer;
  19         25  
  19         367  
12 19     19   6258 use Data::Processor::Generator;
  19         37  
  19         482  
13 19     19   5654 use Data::Processor::PodWriter;
  19         38  
  19         458  
14 19     19   5563 use Data::Processor::ValidatorFactory;
  19         38  
  19         26279  
15              
16             =head1 NAME
17              
18             Data::Processor - Transform Perl Data Structures, Validate Data against a Schema, Produce Data from a Schema, or produce documentation directly from information in the Schema.
19              
20             =head1 SYNOPSIS
21              
22             use Data::Processor;
23             my $schema = {
24             section => {
25             description => 'a section with a few members',
26             error_msg => 'cannot find "section" in config',
27             members => {
28             foo => {
29             # value restriction either with a regex..
30             value => qr{f.*},
31             description => 'a string beginning with "f"'
32             },
33             bar => {
34             # ..or with a validator callback.
35             validator => sub {
36             my $self = shift;
37             my $parent = shift;
38             # undef is "no-error" -> success.
39             no strict 'refs';
40             return undef
41             if $self->{value} == 42;
42             }
43             },
44             wuu => {
45             optional => 1
46             }
47             }
48             }
49             };
50              
51             my $p = Data::Processor->new($schema);
52              
53             my $data = {
54             section => {
55             foo => 'frobnicate',
56             bar => 42,
57             # "wuu" being optional, can be omitted..
58             }
59             };
60              
61             my $error_collection = $p->validate($data, verbose=>0);
62             # no errors :-)
63              
64             # in case of errors:
65             # ------------------
66             # print each error on one line.
67             say $error_collection;
68              
69             # same
70             for my $e ($error_collection->as_array){
71             say $e;
72             # do more..
73             }
74              
75             =head1 DESCRIPTION
76              
77             Data::Processor is a tool for transforming, verifying, and producing Perl data structures from / against a schema, defined as a Perl data structure.
78              
79             =head1 METHODS
80              
81             =head2 new
82              
83             my $processor = Data::Processor->new($schema);
84              
85             optional parameters:
86             - indent: count of spaces to insert when printing in verbose mode. Default 4
87             - depth: level at which to start. Default is 0.
88             - verbose: Set to a true value to print messages during processing.
89              
90             =cut
91             sub new{
92 34     34 1 71883 my $class = shift;
93 34         49 my $schema = shift;
94 34         62 my %p = @_;
95             my $self = {
96             schema => $schema // {},
97             errors => Data::Processor::Error::Collection->new(),
98             depth => $p{depth} // 0,
99             indent => $p{indent} // 4,
100             parent_keys => ['root'],
101             verbose => $p{verbose} // undef,
102 34   100     274 };
      50        
      50        
      50        
103 34         76 bless ($self, $class);
104 34         80 my $e = $self->validate_schema;
105 34 100       198 if ($e->count > 0){
106 2         5 croak "There is a problem with your schema:".join "\n", $e->as_array;
107             }
108 32         115 return $self;
109             }
110              
111             =head2 validate
112             Validate the data against a schema. The schema either needs to be present
113             already or be passed as an argument.
114              
115             my $error_collection = $processor->validate($data, verbose=>0);
116             =cut
117             sub validate{
118 43     43 1 5945 my $self = shift;
119 43         48 my $data = shift;
120 43         77 my %p = @_;
121              
122             $self->{validator}=Data::Processor::Validator->new(
123             $self->{schema} // $p{schema},
124             verbose => $p{verbose} // $self->{verbose} // undef,
125             errors => $self->{errors},
126             depth => $self->{depth},
127             indent => $self->{indent},
128             parent_keys => $self->{parent_keys},
129 43   33     372 );
      66        
      50        
130 43         100 return $self->{validator}->validate($data);
131             }
132              
133             =head2 validate_schema
134              
135             check that the schema is valid.
136             This method gets called upon creation of a new Data::Processor object.
137              
138             my $error_collection = $processor->validate_schema();
139              
140             =cut
141              
142             sub validate_schema {
143 38     38 1 54 my $self = shift;
144 38         148 my $vf = Data::Processor::ValidatorFactory->new;
145 38         182 my $bool = $vf->rx(qr(^[01]$),'Expected 0 or 1');
146 38         65 my $schemaSchema;
147             $schemaSchema = {
148             '.+' => {
149             regex => 1,
150             optional => 1,
151             description => 'content description for the key',
152             members => {
153             description => {
154             description => 'the description of this content of this key',
155             optional => 1,
156             validator => $vf->rx(qr(^.+$),'expected a description string'),
157             },
158             example => {
159             description => 'an example value for this key',
160             optional => 1,
161             validator => $vf->rx(qr(^.+$),'expected an example string'),
162             },
163             no_descend_into => {
164             optional => 1,
165             description => 'do not check inside this node',
166             },
167             regex => {
168             description => 'should this key be treated as a regular expression?',
169             optional => 1,
170             validator => $bool
171             },
172             value => {
173             description => 'a regular expression describing the expected value',
174             optional => 1,
175             validator => sub {
176 18 50   18   80 ref shift eq 'Regexp' ? undef : 'expected a regular expression value (qr/.../)'
177             }
178             },
179             error_msg => {
180             description => 'an error message for the case that the value regexp does not match',
181             optional => 1,
182             validator => $vf->rx(qr(^.+$),'expected an error message string'),
183             },
184             optional => {
185             description => 'is this key optional ?',
186             optional => 1,
187             validator => $bool,
188             },
189             default => {
190             description => 'the default value for this key',
191             optional => 1
192             },
193             array => {
194             description => 'is the value of this key expected to be an array? In array mode, value and validator will be applied to each element of the array.',
195             optional => 1,
196             validator => $bool
197             },
198             allow_empty => {
199             description => 'allow empty entries in an array',
200             optional => 1,
201             validator => sub {
202 1     1   3 my ($value, $parent) = @_;
203 1 50       11 return 'allow_empty can only be set for array' if !$parent->{array};
204 1         4 return $bool->($value);
205             }
206             },
207             order => {
208             description => 'numeric value to specify the validation order',
209             optional => 1,
210             validator => $vf->rx(qr(^\d+$), 'expected an integer'),
211             },
212             members => {
213             description => 'what keys do I expect in a hash hanging off this key',
214             optional => 1,
215             validator => sub {
216 23     23   30 my $value = shift;
217 23 50       66 if (ref $value ne 'HASH'){
218 0         0 return "expected a hash"
219             }
220 23         96 my $subVal=Data::Processor::Validator->new($schemaSchema,%$self);
221 23         170 my $e = $subVal->validate($value);
222 23 50       62 return ( $e->count > 0 ? join("\n", $e->as_array) : undef);
223             }
224             },
225             validator => {
226             description => 'a callback which gets called with (value,section) to validate the value. If it returns anything, this is treated as an error message',
227             optional => 1,
228             validator => sub {
229 18     18   31 my $v = shift;
230             # "0" is a valid package, but is "false"
231 18         53 my $blessed = blessed $v;
232 18 100       55 if (defined $blessed){
233 4 100       21 $v->can('validate') && return undef;
234 1         4 return 'validator object must implement method "validate()"';
235             }
236 14 100       54 ref $v eq 'CODE' ? undef : 'expected a callback';
237             },
238             example => 'sub { my ($value,$section) = @_; return $value <= 1 ? "value must be > 1" : undef}'
239             },
240             transformer => {
241             description => 'a callback which gets called on the value with (value,section) to transform the date into a format suitable for further processing. This is called BEFORE the validator. Die with C<{msg=>"error"}> if there is a problem!',
242             optional => 1,
243             validator => sub {
244 12 100   12   33 ref shift eq 'CODE' ? undef : 'expected a callback'
245             }
246             },
247 38         145 'x-.+' => {
248             optional => 1,
249             regex => 1,
250             description => 'metadata'
251             }
252             }
253             }
254             };
255 38         399 return Data::Processor::Validator->new($schemaSchema,%$self)->validate($self->{schema});
256             }
257              
258             =head2 merge_schema
259              
260             merges another schema into the schema (optionally at a specific node)
261              
262             my $error_collection = $processor->merge_schema($schema_2);
263              
264             merging rules:
265             - merging transformers will result in an error
266             - merge checks if all merged elements match existing elements
267             - non existing elements will be added from merging schema
268             - validators from existing and merging schema get combined
269              
270             =cut
271              
272             sub merge_schema {
273 6     6 1 724 my $self = shift;
274 6         7 my $schema = shift;
275 6         7 my $mergeNode = $self->{schema};
276              
277 6         6 for my $key (@{$_[0]}){
  6         11  
278 12 50       19 exists $mergeNode->{$key} || ($mergeNode->{$key} = {});
279 12         16 $mergeNode = $mergeNode->{$key};
280             }
281              
282 6         6 my $mergeSubSchema;
283             $mergeSubSchema = sub {
284 6     6   5 my $subSchema = shift;
285 6         7 my $otherSubSchema = shift;
286              
287             my $checkKey = sub {
288 36         29 my $elem = shift;
289 36         31 my $key = shift;
290              
291             #nothing to do if key value is not defined
292 36 100       84 return if !defined $otherSubSchema->{$elem}->{$key};
293              
294 1 50       4 if (!defined $subSchema->{$elem}->{$key}){
    50          
295 0         0 $subSchema->{$elem}->{$key} = $otherSubSchema->{$elem}->{$key};
296             }
297             elsif ($subSchema->{$elem}->{$key} ne $otherSubSchema->{$elem}->{$key}){
298 1         68 croak "merging element '$elem' : $key does not match";
299             }
300 6         12 };
301              
302 6         15 for my $elem (keys %$otherSubSchema){
303             #copy whole sub schema if element does not yet exist or is empty
304 6 50 33     11 if (!(exists $subSchema->{$elem} && %{$subSchema->{$elem}})){
  6         16  
305 0         0 $subSchema->{$elem} = $otherSubSchema->{$elem};
306 0         0 next;
307             }
308              
309             #merge members subtree recursively
310 6 50       19 if (exists $otherSubSchema->{$elem}->{members}) {
311 0 0       0 exists $subSchema->{$elem}->{members} || ($subSchema->{$elem}->{members} = {});
312 0         0 $mergeSubSchema->($subSchema->{$elem}->{members}, $otherSubSchema->{$elem}->{members});
313             }
314              
315             #check elements
316 6         8 for my $key (qw(description example default error_msg regex array value)){
317 36         38 $checkKey->($elem, $key);
318             }
319              
320             #special handler for transformer
321 5 100       8 if ($otherSubSchema->{$elem}->{transformer}) {
322             croak "merging element '$elem': merging conflicting transformers not allowed"
323             if $subSchema->{$elem}->{transformer}
324 2 100 66     209 && $subSchema->{$elem}->{transformer} != $otherSubSchema->{$elem}->{transformer};
325              
326 1         2 $subSchema->{$elem}->{transformer} = $otherSubSchema->{$elem}->{transformer};
327             }
328              
329             #special handler for optional: set it mandatory if at least one is not optional
330             delete $subSchema->{$elem}->{optional}
331 4 50 33     9 if !($subSchema->{$elem}->{optional} && $otherSubSchema->{$elem}->{optional});
332              
333             #special handler for validator: combine validator subs
334 4 100       11 if ($otherSubSchema->{$elem}->{validator}) {
335 3 100 66     13 if ((my $validator = $subSchema->{$elem}->{validator})
336             && $subSchema->{$elem}->{validator} != $otherSubSchema->{$elem}->{validator}) {
337              
338             $subSchema->{$elem}->{validator} = sub {
339 3   66     7 return $validator->(@_) // $otherSubSchema->{$elem}->{validator}->(@_);
340 2         14 };
341             }
342             else{
343             $subSchema->{$elem}->{validator}
344 1         10 = $otherSubSchema->{$elem}->{validator};
345             }
346             }
347             }
348 6         34 };
349              
350 6         15 $mergeSubSchema->($mergeNode, $schema);
351              
352 4         9 return $self->validate_schema;
353             }
354              
355             =head2 schema
356              
357             Returns the schema. Useful after schema merging.
358              
359             =cut
360              
361             sub schema{
362 0     0 1 0 return shift->{schema};
363             }
364              
365             =head2 transform_data
366              
367             Transform one key in the data according to rules specified
368             as callbacks that themodule calls for you.
369             Transforms the data in-place.
370              
371             my $validator = Data::Processor::Validator->new($schema, data => $data)
372             my $error_string = $processor->transform($key, $schema_key, $value);
373              
374             This is not tremendously useful at the moment, especially because validate()
375             transforms during validation.
376              
377             =cut
378             # XXX make this traverse a data tree and transform everything
379             # XXX across.
380             # XXX Before hacking something here, think about factoring traversal out of
381             # XXX D::P::Validator
382             sub transform_data{
383 3     3 1 12 my $self = shift;
384 3         4 my $key = shift;
385 3         4 my $schema_key = shift;
386 3         3 my $val = shift;
387              
388 3         20 return Data::Processor::Transformer->new()->transform($key, $schema_key, $val);
389             }
390              
391             =head2 make_data
392              
393             Writes a data template using the information found in the schema.
394              
395             my $data = $processor->make_data(data=>$data);
396              
397             =cut
398             sub make_data{
399 2     2 1 1534 my $self = shift;
400 2   66     7 my $entry_point = shift // $self->{schema};
401 2         5 return Data::Processor::Generator::make_data_template($entry_point);
402             }
403              
404             =head2 make_pod
405              
406             Write descriptive pod from the schema.
407              
408             my $pod_string = $processor->make_pod();
409              
410             =cut
411             sub pod_write{
412 1     1 0 4 my $self = shift;
413             return Data::Processor::PodWriter::pod_write(
414             $self->{schema},
415 1         3 "=head1 Schema Description\n\n"
416             );
417             }
418              
419             =head1 SCHEMA REFERENCE
420              
421              
422             =head2 Top-level keys and members
423              
424             The schema is described by a nested hash. At the top level, and within a
425             members definition, the keys are the same as the structure you are
426             describing. So for example:
427              
428             my $schema = {
429             coordinates => {
430             members => {
431             x => {
432             description => "the x coordinate",
433             },
434             y => {
435             description => "the y coordinate",
436             },
437             }
438             }
439             };
440              
441              
442             This schema describes a structure which might look like this:
443              
444             { coordinates => { x => 1, y => 2} }
445              
446             Obviously this can be nested all the way down:
447              
448             my $schema = {
449             house => {
450             members => {
451             bungalow => {
452             members => {
453             rooms => {
454             #...
455             }
456             }
457             }
458             }
459             }
460             };
461              
462             =head2 array
463              
464             To have a key point to an array of things, simply use the array key. So:
465              
466             my $schema = {
467             houses => {
468             array => 1,
469             }
470             };
471              
472             Would describe a structure like:
473              
474             { houses => [] }
475              
476             And of course you can nest within here so:
477              
478             my $schema = {
479             houses => {
480             array => 1,
481             members => {
482             name => {},
483             windows => {
484             array => 1,
485             }
486             },
487             },
488             };
489              
490             Might describe:
491              
492             {
493             houses => [
494             { name => 'bob',
495             windows => []},
496             { name => 'harry',
497             windows => []},
498             ]
499             }
500              
501             =head2 description
502              
503             The description key within a definition describes that value:
504              
505             my $schema = {
506             x => { description => 'The x coordinate' },
507             };
508              
509             =head2 error_msg
510              
511             The error_msg key can be set to provide extra context for when a value is not
512             found or fails the L test.
513              
514             =head2 optional
515              
516             Most values are required by default. To reverse this use the "optional" key:
517              
518             my $schema = {
519             x => {
520             optional => 1,
521             },
522             y => {
523             # required
524             },
525             };
526              
527             =head2 regex
528              
529             B
530              
531             If you set "regex" within a definition then it's key will be treated as a
532             regular expression.
533              
534             my $schema = {
535             'color_.+' => {
536             regex => 1
537             },
538             };
539             my $data = { color_red => 'red', color_blue => 'blue'};
540             Data::Processor->new($schema)->validate($data);
541              
542             =head2 transformer
543              
544             B
545              
546             Transformer maps to a sub ref which will be passed the value and the containing
547             structure. Your return value provides the new value.
548              
549             my $schema = {
550             x => {
551             transformer => sub{
552             my( $value, $section ) = @_;
553             $value = $value + 1;
554             return $value;
555             }
556             }
557             };
558             my $data = { x => 1 };
559             my $p = Data::Processor->new($schema);
560             my $val = Data::Processor::Validator->new( $schema, data => $data);
561             $p->transform_data('x', 'x', $val);
562             say $data->{x}; #will print 2
563              
564             If you wish to provide an error from the transformer you should die with a
565             hash reference with a key of "msg" mapping to your error:
566              
567              
568             my $schema = {
569             x => {
570             transformer => sub{
571             die { msg => "SOMETHING IS WRONG" };
572             }
573             },
574             };
575              
576             my $p = Data::Processor->new($schema);
577             my $data = { x => 1 };
578             my $val = Data::Processor::Validator->new( $schema, data => $data);
579             my $error = $p->transform_data('x', 'x', $val);
580              
581             say $error; # will print: error transforming 'x': SOMETHING IS WRONG
582              
583              
584             The transformer is called before any validator, so:
585              
586             my $schema = {
587             x => {
588             transformer => sub{
589             my( $value, $section ) = @_;
590             return $value + 1;
591             },
592             validator => sub{
593             my( $value ) = @_;
594             if( $value < 2 ){
595             return "too low"
596             }
597             },
598             },
599             };
600             my $p = Data::Processor->new( $schema );
601             my $data = { x => 1 };
602             my $errors = $p->validate();
603             say $errors->count; # will print 0
604             say $data->{x}; # will print 2
605              
606             =head2 value
607              
608             B
609              
610             To check a value against a regular expression you can use the I key
611             within a definition, mapped to a quoted regex:
612              
613             my $schema = {
614             x => {
615             value => qr{\d+}
616             }
617             };
618              
619             =head2 validator
620              
621             B
622              
623             To conduct extensive checks you can use I and provide a
624             callback. Your sub will be passed the value and it's container. If you return
625             anything it will be regarded as an error message, so to indicate a valid
626             value you return nothing:
627              
628             my $schema = {
629             bob => {
630             validator => sub{
631             my( $value, $section ) = @_;
632             if( $value ne 'bob' ){
633             return "Bob must equal bob!";
634             }
635             return;
636             },
637             },
638             };
639             my $p = Data::Processor->new($schema);
640             # would validate:
641             $p->validate({ bob => "bob" });
642             # would fail:
643             $p->validate({ bob => "harry"});
644              
645             See also L
646              
647             =head3 Validator objects
648              
649             Validator may also be an object, in this case the object must implement a
650             "validate" method.
651              
652             The "validate" method should return undef if valid, or an error message string if there is a problem.
653              
654             package FiveChecker;
655              
656             sub new {
657             bless {}, shift();
658             }
659              
660             sub validate{
661             my( $self, $val ) = @_;
662             $val == 5 or return "I wanted five!";
663             return;
664             }
665             package main;
666              
667             my $checker = FiveChecker->new;
668             my $schema = (
669             five => (
670             validator => $checker,
671             ),
672             );
673             my $dp = Data::Processor->new($schema);
674             $dp->validate({five => 6}); # fails
675             $dp->validate({five => 5}); # passes
676              
677             You can for example use MooseX::Types and Type::Tiny type constraints that are objects
678             which offer validate methods which work this way.
679              
680             use Types::Standard -all;
681              
682             # ... in schema ...
683             foo => {
684             validator => ArrayRef[Int],
685             description => 'an arrayref of integers'
686             },
687              
688             =head1 AUTHOR
689              
690             Matthias Bloch Ematthias.bloch@puffin.chE
691              
692             =head1 COPYRIGHT
693              
694             Copyright 2015- Matthias Bloch
695              
696             =head1 LICENSE
697              
698             This library is free software; you can redistribute it and/or modify
699             it under the same terms as Perl itself.
700              
701             =cut
702             1;
703             __END__