File Coverage

lib/Config/Neat/Schema.pm
Criterion Covered Total %
statement 86 92 93.4
branch 47 56 83.9
condition 17 21 80.9
subroutine 12 13 92.3
pod 0 6 0.0
total 162 188 86.1


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Config::Neat::Schema - Validate Config::Neat files against schema
4              
5             =head1 SYNOPSIS
6              
7             File 01.nconf:
8              
9             foo {
10             bar baz etc
11              
12             etc {
13             pwd 1 2
14             }
15             }
16             abc def
17              
18             File schema.nconf:
19              
20             foo
21             {
22             bar ARRAY
23             etc
24             {
25             * ARRAY
26             pwd STRING
27             }
28             }
29             data DATA
30              
31             if file 01.nconf is validated against schema.nconf, it will:
32             1) convert arrays to strings for the known nodes with 'STRING' type
33             2) die or warn (depending on the settings) when an unknown node is found
34             (in the example above, 'abc').
35              
36             '*' as the name of the node means 'node with any name'. If such catch-all rule
37             is not specified, all possible node values need to be specified explicitly.
38              
39             Possible type specifiers are: HASH (this is default if not specified),
40             ARRAY, STRING, ARRAY_OR_HASH, STRING_OR_HASH, or DATA. 'DATA' nodes may contain
41             any arbitrary data structure and are not validated.
42              
43             =head1 COPYRIGHT
44              
45             Copyright (C) 2012-2015 Igor Afanasyev
46              
47             =head1 SEE ALSO
48              
49             L
50              
51             =cut
52              
53             package Config::Neat::Schema;
54              
55             our $VERSION = '1.401';
56              
57 1     1   1160 use strict;
  1         2  
  1         32  
58              
59 1     1   6 use Config::Neat::Array;
  1         1  
  1         20  
60 1     1   5 use Config::Neat::Inheritable;
  1         2  
  1         23  
61 1     1   5 use Config::Neat::Util qw(new_ixhash is_hash is_any_hash is_any_array is_simple_array is_neat_array hash_has_sequential_keys);
  1         2  
  1         86  
62 1     1   7 use File::Spec::Functions qw(rel2abs);
  1         2  
  1         45  
63 1     1   5 use File::Basename qw(dirname);
  1         2  
  1         46  
64 1     1   7 use Tie::IxHash;
  1         1  
  1         852  
65              
66             #
67             # Initialize object
68             #
69             sub new {
70 1     1 0 304 my ($class, $data) = @_;
71              
72 1         3 my $self = {
73             schema => $data
74             };
75              
76 1         2 bless $self, $class;
77 1         2 return $self;
78             }
79              
80             # Given file name, will read and store the schema file
81             sub load {
82 1     1 0 425 my ($self, $filename, $binmode) = @_;
83 1         5 my $c = Config::Neat::Inheritable->new();
84 1         4 return $self->{schema} = $c->parse_file($filename, $binmode);
85             }
86              
87             # Store loaded data as current schema
88             sub set {
89 0     0 0 0 my ($self, $data) = @_;
90 0         0 $self->{schema} = $data;
91             }
92              
93             # Validates provided data structure (parsed config file) against the previously loaded schema
94             # with expanded '@inherit' blocks
95             sub validate {
96 20     20 0 7975 my ($self, $data) = @_;
97 20 50       65 die "Schema should be loaded prior to validation" unless defined $self->{schema};
98 20         63 return $self->validate_node($self->{schema}, $data, undef, undef, []);
99             }
100              
101             sub validate_node {
102 120     120 0 312 my ($self, $schema_node, $data_node, $parent_data, $parent_data_key, $path) = @_;
103              
104 120         732 my $pathstr = '/'.join('/', @$path);
105              
106 120 100       242 if (!$schema_node) {
107 1         13 die "Node '$pathstr' is not defined in the schema";
108             }
109              
110 119         210 my $schema_type = $self->get_node_type($schema_node);
111 119         208 my $data_type = $self->get_node_type($data_node);
112              
113             #print "::[$pathstr] schema_type=[$schema_type], data_type=[$data_type]\n";
114             #use Data::Dumper; print Dumper($data_node);
115              
116 119 50 66     472 if ($schema_type eq 'STRING') {
    100          
    100          
117             # the node itself is already a scalar and contains the type definition
118 0         0 $schema_type = $schema_node;
119             } elsif ($schema_type eq 'ARRAY') {
120             # the string representation of the node contains the type definition
121 46         104 $schema_type = $schema_node->as_string;
122             } elsif ($schema_type eq 'HASH' and defined $schema_node->{''}) {
123             # if it's a hash, the the string representation of the node's default parameter
124             # may contain the type definition override
125 38         346 my $val = $schema_node->{''};
126 38 50       237 $schema_type = $schema_node->{''}->as_string if is_neat_array($val);
127 38 50       119 $schema_type = $schema_node->{''} if ref(\$val) eq 'SCALAR';
128             }
129              
130             # disambiguate fuzzy node schema types
131 119 100       471 if ($schema_type eq 'ARRAY_OR_HASH') {
132 17 100       38 $schema_type = ($data_type eq 'HASH') ? 'HASH' : 'ARRAY';
133             }
134              
135 119 100       209 if ($schema_type eq 'STRING_OR_HASH') {
136 2 100       5 $schema_type = ($data_type eq 'HASH') ? 'HASH' : 'STRING';
137             }
138              
139             # automatic casting from ARRAY to STRING
140 119 100 66     245 if ($schema_type eq 'STRING' and $data_type eq 'ARRAY') {
141 14         30 $parent_data->{$parent_data_key} = $data_node = $data_node->as_string;
142 14         163 $data_type = $schema_type;
143             }
144              
145             # automatic casting from ARRAY to BOOLEAN
146 119 100 66     243 if ($schema_type eq 'BOOLEAN' and $data_type eq 'ARRAY') {
147 11 100       27 die "'".$data_node->as_string."' is not a valid boolean number\n" unless $data_node->is_boolean;
148 10         25 $parent_data->{$parent_data_key} = $data_node = $data_node->as_boolean;
149 10         114 $data_type = $schema_type;
150             }
151              
152             # skip (don't validate) DATA nodes
153 118 100       225 return 1 if ($schema_type eq 'DATA');
154              
155 108 100       196 if ($schema_type eq 'LIST') {
156             # if this is not a simple array of scalars, wrap as an array
157 8 100 100     27 if (is_simple_array($data_node) or !is_any_array($data_node)) {
158 2         5 $data_node = [$data_node];
159             }
160             # then, convert an array to an ixhash with sequential keys
161 8         22 my $h = new_ixhash;
162 8         17 my $i = 0;
163 8         21 map { $h->{$i++} = $_ } @$data_node;
  19         232  
164 8         151 $parent_data->{$parent_data_key} = $data_node = $h;
165              
166 8         89 $data_type = 'HASH';
167 8         17 $schema_type = 'ARRAY';
168             }
169              
170             # see if automatic casting from HASH to ARRAY is possible
171 108         137 my $cast_to_array;
172              
173 108 100 100     265 if ($schema_type eq 'ARRAY' and $data_type eq 'HASH') {
174 15 100       44 die "Can't cast '$pathstr' to ARRAY, since it is a HASH containing non-sequential keys" unless hash_has_sequential_keys($data_node);
175 14         24 $cast_to_array = 1;
176             }
177              
178 107 50 66     217 if ($schema_type ne $data_type && !$cast_to_array) {
179 0         0 die "'$pathstr' is $data_type, while it is expected to be $schema_type";
180             }
181              
182 107 100       197 if ($data_type eq 'ARRAY') {
183             # flatten the array
184 24         65 $parent_data->{$parent_data_key} = $data_node->as_flat_array;
185             }
186              
187 107 100       405 if ($data_type eq 'HASH') {
188 59         161 foreach my $key (keys %$data_node) {
189 101         1038 my @a = @$path;
190 101         181 push @a, $key;
191 101 50       206 if ($key eq '') {
192             # TODO: check if the default parameter for the hash is allowed, and if it is a string or array
193             } else {
194 101 100       233 die "Can't validate '/", join('/', @a), "', because schema contains no definition for it" if !is_hash($schema_node);
195 100   100     296 my $schema_subnode = $schema_node->{$key} || $schema_node->{'*'};
196 100         1192 $self->validate_node($schema_subnode, $data_node->{$key}, $data_node, $key, \@a);
197             }
198             }
199             }
200              
201 99 100       220 if ($cast_to_array) {
202 13         36 my @a = values %$data_node;
203 13         431 $parent_data->{$parent_data_key} = Config::Neat::Array->new(\@a);
204             }
205              
206 99         452 return 1;
207             }
208              
209             sub get_node_type {
210 238     238 0 371 my ($self, $node) = @_;
211 238 100       532 return 'HASH' if ref($node) eq 'HASH';
212 106 50       236 return 'ARRAY' if is_any_array($node);
213 0 0         return 'STRING' if ref(\$node) eq 'SCALAR';
214 0           return 'UNKNOWN';
215             }
216              
217             1;