File Coverage

lib/Config/Neat/Schema.pm
Criterion Covered Total %
statement 86 92 93.4
branch 47 56 83.9
condition 16 21 76.1
subroutine 12 13 92.3
pod 0 6 0.0
total 161 188 85.6


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.302';
56              
57 1     1   844 use strict;
  1         2  
  1         25  
58              
59 1     1   4 use Config::Neat::Array;
  1         2  
  1         25  
60 1     1   4 use Config::Neat::Inheritable;
  1         3  
  1         19  
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         8  
  1         55  
62 1     1   9 use File::Spec::Functions qw(rel2abs);
  1         2  
  1         37  
63 1     1   4 use File::Basename qw(dirname);
  1         2  
  1         28  
64 1     1   5 use Tie::IxHash;
  1         1  
  1         591  
65              
66             #
67             # Initialize object
68             #
69             sub new {
70 1     1 0 300 my ($class, $data) = @_;
71              
72 1         3 my $self = {
73             schema => $data
74             };
75              
76 1         3 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 365 my ($self, $filename, $binmode) = @_;
83 1         3 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 7772 my ($self, $data) = @_;
97 20 50       74 die "Schema should be loaded prior to validation" unless defined $self->{schema};
98 20         78 return $self->validate_node($self->{schema}, $data, undef, undef, []);
99             }
100              
101             sub validate_node {
102 120     120 0 317 my ($self, $schema_node, $data_node, $parent_data, $parent_data_key, $path) = @_;
103              
104 120         724 my $pathstr = '/'.join('/', @$path);
105              
106 120 100       296 if (!$schema_node) {
107 1         11 die "Node '$pathstr' is not defined in the schema";
108             }
109              
110 119         262 my $schema_type = $self->get_node_type($schema_node);
111 119         246 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     551 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         116 $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         362 my $val = $schema_node->{''};
126 38 50       292 $schema_type = $schema_node->{''}->as_string if is_neat_array($val);
127 38 50       139 $schema_type = $schema_node->{''} if ref(\$val) eq 'SCALAR';
128             }
129              
130             # disambiguate fuzzy node schema types
131 119 100       520 if ($schema_type eq 'ARRAY_OR_HASH') {
132 17 100       50 $schema_type = ($data_type eq 'HASH') ? 'HASH' : 'ARRAY';
133             }
134              
135 119 100       276 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     319 if ($schema_type eq 'STRING' and $data_type eq 'ARRAY') {
141 14         34 $parent_data->{$parent_data_key} = $data_node = $data_node->as_string;
142 14         158 $data_type = $schema_type;
143             }
144              
145             # automatic casting from ARRAY to BOOLEAN
146 119 100 66     306 if ($schema_type eq 'BOOLEAN' and $data_type eq 'ARRAY') {
147 11 100       26 die "'".$data_node->as_string."' is not a valid boolean number\n" unless $data_node->is_boolean;
148 10         26 $parent_data->{$parent_data_key} = $data_node = $data_node->as_boolean;
149 10         100 $data_type = $schema_type;
150             }
151              
152             # skip (don't validate) DATA nodes
153 118 100       272 return 1 if ($schema_type eq 'DATA');
154              
155 108 100       239 if ($schema_type eq 'LIST') {
156             # if this is not a simple array of scalars, wrap as an array
157 8 100 100     26 if (is_simple_array($data_node) or !is_any_array($data_node)) {
158 2         4 $data_node = [$data_node];
159             }
160             # then, convert an array to an ixhash with sequential keys
161 8         26 my $h = new_ixhash;
162 8         17 my $i = 0;
163 8         19 map { $h->{$i++} = $_ } @$data_node;
  19         280  
164 8         165 $parent_data->{$parent_data_key} = $data_node = $h;
165              
166 8         95 $data_type = 'HASH';
167 8         16 $schema_type = 'ARRAY';
168             }
169              
170             # see if automatic casting from HASH to ARRAY is possible
171 108         154 my $cast_to_array;
172              
173 108 100 100     365 if ($schema_type eq 'ARRAY' and $data_type eq 'HASH') {
174 15 100       50 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         27 $cast_to_array = 1;
176             }
177              
178 107 50 66     294 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       227 if ($data_type eq 'ARRAY') {
183             # flatten the array
184 24         69 $parent_data->{$parent_data_key} = $data_node->as_flat_array;
185             }
186              
187 107 100       474 if ($data_type eq 'HASH') {
188 59         177 foreach my $key (keys %$data_node) {
189 101         1068 my @a = @$path;
190 101         175 push @a, $key;
191 101 50       212 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       242 die "Can't validate '/", join('/', @a), "', because schema contains no definition for it" if !is_hash($schema_node);
195 100   66     315 my $schema_subnode = $schema_node->{$key} || $schema_node->{'*'};
196 100         1151 $self->validate_node($schema_subnode, $data_node->{$key}, $data_node, $key, \@a);
197             }
198             }
199             }
200              
201 99 100       249 if ($cast_to_array) {
202 13         48 my @a = values %$data_node;
203 13         432 $parent_data->{$parent_data_key} = Config::Neat::Array->new(\@a);
204             }
205              
206 99         462 return 1;
207             }
208              
209             sub get_node_type {
210 238     238 0 418 my ($self, $node) = @_;
211 238 100       609 return 'HASH' if ref($node) eq 'HASH';
212 106 50       238 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;