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; |