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.4';
56              
57 1     1   957 use strict;
  1         2  
  1         24  
58              
59 1     1   4 use Config::Neat::Array;
  1         2  
  1         16  
60 1     1   4 use Config::Neat::Inheritable;
  1         1  
  1         18  
61 1     1   4 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         1  
  1         66  
62 1     1   5 use File::Spec::Functions qw(rel2abs);
  1         1  
  1         35  
63 1     1   5 use File::Basename qw(dirname);
  1         1  
  1         35  
64 1     1   6 use Tie::IxHash;
  1         1  
  1         710  
65              
66             #
67             # Initialize object
68             #
69             sub new {
70 1     1 0 252 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 355 my ($self, $filename, $binmode) = @_;
83 1         4 my $c = Config::Neat::Inheritable->new();
84 1         3 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 6748 my ($self, $data) = @_;
97 20 50       51 die "Schema should be loaded prior to validation" unless defined $self->{schema};
98 20         56 return $self->validate_node($self->{schema}, $data, undef, undef, []);
99             }
100              
101             sub validate_node {
102 120     120 0 233 my ($self, $schema_node, $data_node, $parent_data, $parent_data_key, $path) = @_;
103              
104 120         563 my $pathstr = '/'.join('/', @$path);
105              
106 120 100       193 if (!$schema_node) {
107 1         9 die "Node '$pathstr' is not defined in the schema";
108             }
109              
110 119         192 my $schema_type = $self->get_node_type($schema_node);
111 119         184 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     405 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         83 $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         284 my $val = $schema_node->{''};
126 38 50       186 $schema_type = $schema_node->{''}->as_string if is_neat_array($val);
127 38 50       92 $schema_type = $schema_node->{''} if ref(\$val) eq 'SCALAR';
128             }
129              
130             # disambiguate fuzzy node schema types
131 119 100       382 if ($schema_type eq 'ARRAY_OR_HASH') {
132 17 100       38 $schema_type = ($data_type eq 'HASH') ? 'HASH' : 'ARRAY';
133             }
134              
135 119 100       164 if ($schema_type eq 'STRING_OR_HASH') {
136 2 100       4 $schema_type = ($data_type eq 'HASH') ? 'HASH' : 'STRING';
137             }
138              
139             # automatic casting from ARRAY to STRING
140 119 100 66     199 if ($schema_type eq 'STRING' and $data_type eq 'ARRAY') {
141 14         26 $parent_data->{$parent_data_key} = $data_node = $data_node->as_string;
142 14         128 $data_type = $schema_type;
143             }
144              
145             # automatic casting from ARRAY to BOOLEAN
146 119 100 66     199 if ($schema_type eq 'BOOLEAN' and $data_type eq 'ARRAY') {
147 11 100       22 die "'".$data_node->as_string."' is not a valid boolean number\n" unless $data_node->is_boolean;
148 10         19 $parent_data->{$parent_data_key} = $data_node = $data_node->as_boolean;
149 10         88 $data_type = $schema_type;
150             }
151              
152             # skip (don't validate) DATA nodes
153 118 100       175 return 1 if ($schema_type eq 'DATA');
154              
155 108 100       175 if ($schema_type eq 'LIST') {
156             # if this is not a simple array of scalars, wrap as an array
157 8 100 100     17 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         12 my $i = 0;
163 8         14 map { $h->{$i++} = $_ } @$data_node;
  19         193  
164 8         123 $parent_data->{$parent_data_key} = $data_node = $h;
165              
166 8         69 $data_type = 'HASH';
167 8         12 $schema_type = 'ARRAY';
168             }
169              
170             # see if automatic casting from HASH to ARRAY is possible
171 108         122 my $cast_to_array;
172              
173 108 100 100     210 if ($schema_type eq 'ARRAY' and $data_type eq 'HASH') {
174 15 100       38 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         17 $cast_to_array = 1;
176             }
177              
178 107 50 66     193 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       147 if ($data_type eq 'ARRAY') {
183             # flatten the array
184 24         42 $parent_data->{$parent_data_key} = $data_node->as_flat_array;
185             }
186              
187 107 100       341 if ($data_type eq 'HASH') {
188 59         124 foreach my $key (keys %$data_node) {
189 101         812 my @a = @$path;
190 101         138 push @a, $key;
191 101 50       170 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       163 die "Can't validate '/", join('/', @a), "', because schema contains no definition for it" if !is_hash($schema_node);
195 100   100     235 my $schema_subnode = $schema_node->{$key} || $schema_node->{'*'};
196 100         916 $self->validate_node($schema_subnode, $data_node->{$key}, $data_node, $key, \@a);
197             }
198             }
199             }
200              
201 99 100       168 if ($cast_to_array) {
202 13         29 my @a = values %$data_node;
203 13         309 $parent_data->{$parent_data_key} = Config::Neat::Array->new(\@a);
204             }
205              
206 99         352 return 1;
207             }
208              
209             sub get_node_type {
210 238     238 0 303 my ($self, $node) = @_;
211 238 100       430 return 'HASH' if ref($node) eq 'HASH';
212 106 50       161 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;