File Coverage

lib/Data/Processor.pm
Criterion Covered Total %
statement 106 114 92.9
branch 27 42 64.2
condition 14 28 50.0
subroutine 23 24 95.8
pod 7 8 87.5
total 177 216 81.9


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