File Coverage

lib/Data/Processor.pm
Criterion Covered Total %
statement 106 114 92.9
branch 26 40 65.0
condition 14 28 50.0
subroutine 23 24 95.8
pod 7 8 87.5
total 176 214 82.2


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