File Coverage

blib/lib/Data/ValidateInterdependent.pm
Criterion Covered Total %
statement 118 119 99.1
branch 29 30 96.6
condition n/a
subroutine 19 19 100.0
pod 9 9 100.0
total 175 177 98.8


line stmt bran cond sub pod time code
1             package Data::ValidateInterdependent;
2 1     1   952 use utf8;
  1         12  
  1         4  
3 1     1   30 use v5.14;
  1         4  
4 1     1   5 use warnings;
  1         1  
  1         38  
5             our $VERSION = '0.000001';
6              
7 1     1   364 use Moo;
  1         8078  
  1         6  
8 1     1   1041 use Carp;
  1         3  
  1         1996  
9              
10             =encoding UTF-8
11              
12             =head1 NAME
13              
14             Data::ValidateInterdependent - safely validate interdependent parameters
15              
16             =head1 SYNOPSIS
17              
18             use Data::ValidateInterdependent;
19              
20             state $validator =
21             Data::ValidateInterdependent->new
22             # inject a constant value
23             ->const(generator => 'perl')
24             # take an input parameter without validation
25             ->param('description')
26             # create variables "x", "y", "z" from parameter "coords"
27             ->validate(['x', 'y', 'z'], '$coords', sub {
28             my ($coords) = @_;
29             die "Coords must contain 3 elements" unless @$coords == 3;
30             my ($x, $y, $z) = @$coords;
31             return { x => $x, y => $y, z => $z };
32             })
33             # create variable "title" from parameter "title"
34             # and from validated variables "x", "y", "z".
35             ->validate('title', ['$title, 'x', 'y', 'z'], sub {
36             my ($title, $x, $y, $z) = @_;
37             $title //= "Object at ($x, $y, $z)";
38             return { title => $title };
39             });
40              
41             my $variables = $validator->run(%config);
42              
43             =head1 DESCRIPTION
44              
45             The problem: you need to validate some configuration.
46             But validation of one field depends on other fields,
47             or default values are taken from other parts of the config.
48             These dependencies can be daunting.
49              
50             This module makes the dependencies between different validation steps
51             more explicit:
52             Each step declares which variables it provides,
53             and which variables or input parameters it consumes.
54             The idea of
55             L
56             allows us to check basic consistency properties when the validator is assembled:
57              
58             =over
59              
60             =item *
61              
62             The validator will provide all declared output variables.
63             Because there is no branching,
64             it is not possible to forget a variable.
65              
66             =item *
67              
68             All variables are declared before they are used.
69             It is not possible to accidentally read an unvalidated value.
70              
71             =item *
72              
73             Each variable is only initialized once.
74             It is not possible to accidentally overwrite a variable.
75              
76             =back
77              
78             =head2 Terminology
79              
80             A B is an unvalidated input value.
81             A parameter called C can be addressed with the symbol C<$name>,
82             i.e. with a prepended C<$> character.
83             If no such parameter exists, its value will be C.
84              
85             A B is a validated field that will be written exactly once.
86             A variable called C is addressed with the symbol C,
87             i.e. without any changes.
88              
89             A B is a callback that initializes one or more variables.
90             It receives a list with any number of parameters and variables.
91              
92             =head1 METHODS
93              
94             Unless explicitly noted,
95             all methods return the object itself
96             so that you can chain methods.
97              
98             =cut
99              
100             # the static environment
101             has _variables => (
102             is => 'ro',
103             default => sub { {} },
104             );
105              
106             has _unused_variables => (
107             is => 'ro',
108             default => sub { {} },
109             );
110              
111             has _rules => (
112             is => 'ro',
113             default => sub { [] },
114             );
115              
116             has _known_params => (
117             is => 'ro',
118             default => sub { {} },
119             );
120              
121             has _ignore_unknown => (
122             is => 'rw',
123             default => 0,
124             );
125              
126             sub _parse_params {
127 30     30   49 my ($spec) = @_;
128 30 100       75 return @$spec if ref $spec eq 'ARRAY';
129 17         35 return $spec;
130             }
131              
132             sub _declare_variable {
133 23     23   56 my ($self, @names) = @_;
134              
135 23         45 my $known = $self->_variables;
136 23         47 my $unused = $self->_unused_variables;
137 23         91 for my $var (@names) {
138 33 100       72 if ($known->{$var}) {
139 1         11 croak qq(Variable cannot be declared twice: $var);
140             }
141             else {
142 32         54 $known->{$var} = 1;
143 32         50 $unused->{$var} = 1; # all are unused initially
144             }
145             }
146              
147 22         42 return;
148             }
149              
150             sub _declare_usage {
151 16     16   36 my ($self, $name, @vars) = @_;
152              
153 16         40 my $known_variables = $self->_variables;
154 16         26 my $unused = $self->_unused_variables;
155              
156 16 100       35 if (my @unknown = grep { not $known_variables->{$_} } @vars) {
  9         35  
157 2         43 croak qq($name depends on undeclared variables: ), join q(, ) => sort @unknown;
158             }
159              
160 14         28 delete @$unused{@vars};
161              
162 14         24 return;
163             }
164              
165             sub _declare_param {
166 8     8   19 my ($self, @names) = @_;
167 8         19 my $known_params = $self->_known_params;
168              
169 8         24 $known_params->{$_} = 1 for @names;
170              
171 8         30 return;
172             }
173              
174             =head2 const
175              
176             $validator = $validator->const(name => $value, ...);
177              
178             Declare one or more variables with a constant value.
179              
180             In most cases this is not necessary
181             because you could use Perl variables
182             to make data accessible to all pipeline steps.
183              
184             Note that this method cannot provide default values for a variable,
185             since all variables are write-once.
186              
187             This method is functionally equivalent to:
188              
189             $validator->validate(['name', ...], [], sub {
190             return { name => $value, ... };
191             });
192              
193             =cut
194              
195             sub const {
196 4     4 1 13 my ($self, %values) = @_;
197             # TODO must not be empty
198              
199 4         24 _declare_variable($self, sort keys %values);
200 4         6 push @{ $self->_rules }, [const => \%values];
  4         15  
201 4         15 return $self;
202             }
203              
204             =head2 param
205              
206             $validator = $validator->param('name', { variable => 'parameter' }, ...);
207              
208             Declare variables that take their value directly from input parameters
209             without any validation.
210              
211             The arguments may be variable names,
212             in which case the value is taken from the parameter of the same name.
213             The arguments may also be a hash ref,
214             which maps variable names to parameters.
215             These names are not symbols,
216             so you must not include the C<$> for parameter symbols.
217              
218             Absolutely no validation will be performed.
219             If the parameter does not exist, the variable will be C.
220              
221             This method is functionally equivalent to:
222              
223             $validator->validate(['name', 'variable', ...], ['$name', '$parameter'], sub {
224             my ($name, $parameter, ...) = @_;
225             return { name => $name, variable => $parameter, ... };
226             });
227              
228             =cut
229              
230             sub param {
231 6     6 1 15 my ($self, @items) = @_;
232             # TODO must not be empty
233              
234 6         10 my %mapping;
235 6         40 for my $item (@items) {
236 9 100       31 $item = { $item => $item } if ref $item ne 'HASH';
237 9         34 @mapping{ keys %$item } = values %$item;
238             }
239              
240 6         27 _declare_param($self, sort values %mapping);
241 6         20 _declare_variable($self, sort keys %mapping);
242              
243 6         8 push @{ $self->_rules }, [param => \%mapping];
  6         20  
244              
245 6         32 return $self;
246             }
247              
248             =head2 validate
249              
250             $validator = $validator->validate($output, $input, sub { ... });
251              
252             Perform a validation step.
253              
254             B<$output> declares the variables which are assigned by this validation step.
255             It may either be a single variable name,
256             or an array ref with one or more variable names.
257              
258             B<$input> declares dependencies on other variables or input parameters.
259             It may either be a single symbol,
260             or an array ref of symbols.
261             The array ref may be empty.
262             A symbol can be the name of a variable,
263             or a C<$> followed by the name of a parameter.
264              
265             B is a callback that peforms the validation step.
266             The callback will be invoked with the values of all I<$input> symbols,
267             in the order in which they were listed.
268             Note that a parameter will have C value if it doesn't exist.
269             The callback must return a hash ref that contains all variables to be assigned.
270             The hash keys must match the declared I<$output> variables exactly.
271              
272             The returned hash ref will be modified.
273             If other code depends on this hash ref, return a copy instead.
274              
275             B when an existing variable was re-declared.
276             All variables are write-once.
277             You cannot reassign them.
278              
279             B when an undeclared variable was used.
280             You must declare all variables before you can use them.
281              
282             B Reading multiple inputs:
283              
284             # "x" and "y" are previously declared variables.
285             # "foo" is an input parameter.
286             $validator->validate('result', ['x', '$foo', 'y'], sub {
287             my ($x, $foo, $y) = @_;
288             $foo //= $y;
289             return { result => $x + $foo };
290             });
291              
292             =cut
293              
294             sub validate {
295 15     15 1 91 my ($self, $output, $input, $callback) = @_;
296 15         29 $output = [_parse_params($output)];
297 15         29 $input = [_parse_params($input)];
298              
299 15 100       36 if (not @$output) {
300 1         9 croak q(Validation rule must provide at least one variable);
301             }
302              
303 14         21 my @vars;
304             my @args;
305 14         26 for (@$input) {
306 8 100       22 if (/^\$/) {
307 1         7 push @args, s/^\$//r;
308             }
309             else {
310 7         14 push @vars, $_;
311             }
312             }
313              
314 14 100       31 _declare_param($self, @args) if @args;
315 14         82 _declare_usage($self, qq(Validation rule "@$output"), @vars);
316 13         33 _declare_variable($self, @$output);
317              
318 12         19 push @{ $self->_rules }, [rule => $output, $input, $callback];
  12         34  
319 12         41 return $self;
320             }
321              
322             =head2 run
323              
324             my $variables = $validator->run(%params);
325              
326             Run the validator with a given set of params.
327             A validator instance can be run multiple times.
328              
329             B<%params> is a hash of all input parameters.
330             The hash may be empty.
331              
332             B a hashref with all output variables.
333             If your validation rules assigned helper variables,
334             you may want to delete them from this hashref before further processing.
335              
336             B when unknown parameters were provided
337             (but see L).
338              
339             B when a rule callback did not return a suitable value:
340             either it was not a hash ref,
341             or the hash ref did not assign exactly the $output variables.
342              
343             =cut
344              
345             sub run {
346 17     17 1 238 my ($self, %params) = @_;
347              
348 17 100       90 unless ($self->_ignore_unknown) {
349 16         32 my $known_params = $self->_known_params;
350 16 100       49 if (my @unknown = grep { not $known_params->{$_} } keys %params) {
  11         34  
351 3         38 croak qq(Unknown parameters: ), join q(, ) => sort @unknown;
352             }
353             }
354              
355 14         23 my %variables;
356              
357             my $get_arg = sub {
358 4     4   7 my ($name) = @_;
359 4 100       15 return $params{$name} if $name =~ s/^\$//;
360 3         9 return $variables{$name};
361 14         78 };
362              
363             RULE:
364 14         26 for my $rule (@{ $self->_rules }) {
  14         48  
365 13         66 my ($type, @rule_args) = @$rule;
366              
367 13 100       34 if ($type eq 'const') {
368 2         6 my ($values) = @rule_args;
369 2         10 @variables{keys %$values} = values %$values;
370 2         9 next RULE;
371             }
372              
373 11 100       23 if ($type eq 'param') {
374 2         3 my ($mapping) = @rule_args;
375 2         7 @variables{ keys %$mapping } = @params{ values %$mapping };
376 2         5 next RULE;
377             }
378              
379 9 50       21 if ($type eq 'rule') {
380 9         18 my ($provided, $required, $callback) = @rule_args;
381              
382 9         20 my $result = $callback->(map { $get_arg->($_) } @$required);
  4         8  
383              
384 9         71 for my $var (@$provided) {
385 11 100       24 if (exists $result->{$var}) {
386 10         21 $variables{$var} = delete $result->{$var};
387             }
388             else {
389 1         14 croak qq(Validation rule "@$provided" must return parameter $var);
390             }
391             }
392              
393 8 100       27 if (my @unknown = keys %$result) {
394 1         21 croak qq(Validation rule "@$provided" returned unknown variables: ),
395             join q(, ) => sort @unknown;
396             }
397              
398 7         26 next RULE;
399             }
400              
401 0         0 die "Unknown rule type: $type";
402             }
403              
404 12         71 return \%variables;
405             }
406              
407             =head2 ignore_unknown
408              
409             $validator = $validator->ignore_unknown;
410              
411             Ignore unknown parameters.
412             If this flag is not set, the L method will die
413             when unknown parameters were provided.
414             A parameter is unknown when no validation rule or param assignment
415             reads from that parameter.
416              
417             =cut
418              
419             sub ignore_unknown {
420 1     1 1 3 my ($self) = @_;
421 1         5 $self->_ignore_unknown(1);
422 1         4 return $self;
423             }
424              
425             =head2 ignore_param
426              
427             $validator = $validator->ignore_param($name, ...);
428              
429             Ignore a specific parameter.
430              
431             =cut
432              
433             sub ignore_param {
434 1     1 1 4 my ($self, @names) = @_;
435 1         5 _declare_param($self, @names);
436 1         11 return $self;
437             }
438              
439             =head2 provided
440              
441             my @names = $validator->provided;
442              
443             Get a list of all provided variables.
444             The order is unspecified.
445              
446             =cut
447              
448             sub provided {
449 1     1 1 3 my ($self) = @_;
450 1         2 return keys %{ $self->_variables };
  1         6  
451             }
452              
453             =head2 unused
454              
455             my @names = $validator->unused;
456              
457             Get a list of all variables that are provided but not used.
458             The order is unspecified.
459              
460             =cut
461              
462             sub unused {
463 2     2 1 6 my ($self) = @_;
464 2         4 return keys %{ $self->_unused_variables };
  2         12  
465             }
466              
467             =head2 select
468              
469             $validator = $validator->select(@names);
470              
471             Mark variables as used, and ensure that these variables exist.
472              
473             This is convenient when the validator is assembled in different places,
474             and you want to make sure that certain variables are provided.
475              
476             The output variables may include variables that were not selected.
477             This method does not list all output variables,
478             but just ensures their presence.
479              
480             =cut
481              
482             sub select :method {
483 2     2 1 7 my ($self, @names) = @_;
484 2         7 _declare_usage($self, q(Select), @names);
485 1         2 return $self;
486             }
487              
488             =head1 SUPPORT
489              
490             Homepage: L
491              
492             Bugtracker: L
493              
494             =head1 AUTHOR
495              
496             amon – Lukas Atkinson (cpan: AMON)
497              
498             =head1 COPYRIGHT
499              
500             Copyright 2017 Lukas Atkinson
501              
502             This library is free software and may be distributed under the same terms
503             as perl itself. See http://dev.perl.org/licenses/.
504              
505             =cut
506              
507             1;