File Coverage

blib/lib/JSON/Validator/Util.pm
Criterion Covered Total %
statement 131 132 99.2
branch 87 92 94.5
condition 45 49 91.8
subroutine 23 23 100.0
pod 8 9 88.8
total 294 305 96.3


line stmt bran cond sub pod time code
1             package JSON::Validator::Util;
2 49     49   319 use Mojo::Base -strict;
  49         98  
  49         287  
3              
4 49     49   5048 use Carp ();
  49         93  
  49         619  
5 49     49   204 use Data::Dumper ();
  49         89  
  49         754  
6 49     49   201 use Exporter 'import';
  49         87  
  49         1293  
7 49     49   247 use JSON::Validator::Error;
  49         81  
  49         357  
8 49     49   1249 use List::Util;
  49         88  
  49         2383  
9 49     49   21079 use Mojo::Collection;
  49         183256  
  49         1854  
10 49     49   305 use Mojo::JSON;
  49         89  
  49         2250  
11 49     49   20093 use Mojo::Loader;
  49         1342926  
  49         2188  
12 49     49   354 use Mojo::Util;
  49         99  
  49         1531  
13 49     49   268 use Scalar::Util 'blessed';
  49         102  
  49         13824  
14              
15             our @EXPORT_OK
16             = qw(E data_checksum data_section data_type is_type schema_extract json_pointer prefix_errors schema_type);
17              
18 701     701 0 50639 sub E { JSON::Validator::Error->new(@_) }
19              
20             sub data_checksum {
21 2782     2782 1 10986 Mojo::Util::md5_sum(Data::Dumper->new([@_])->Sortkeys(1)->Useqq(1)->Dump);
22             }
23              
24             sub data_section {
25 18     18 1 45 my ($class, $file, $params) = @_;
26 18         61 state $skip_re
27             = qr{(^JSON::Validator|^Mojo::Base$|^Mojolicious$|\w+::_Dynamic)};
28              
29 18 100       83 my @classes = $class ? ([$class]) : ();
30 18 100       51 unless (@classes) {
31 4         6 my $i = 0;
32 4         14 while ($class = caller($i++)) {
33 22 100       95 push @classes, [$class] unless $class =~ $skip_re;
34             }
35             }
36              
37 18         44 for my $group (@classes) {
38             push @$group,
39 49     49   374 grep { !/$skip_re/ } do { no strict 'refs'; @{"$group->[0]\::ISA"} };
  49         113  
  49         82179  
  21         141  
  6         52  
  21         28  
  21         114  
40 21         70 for my $class (@$group) {
41 22 100       155 next unless my $text = Mojo::Loader::data_section($class, $file);
42             return Mojo::Util::encode($params->{encoding}, $text)
43 17 50       1586 if $params->{encoding};
44 0         0 return $text;
45             }
46             }
47              
48 1 50       9 return undef unless $params->{confess};
49              
50 1 50       3 my $err = Mojo::JSON::encode_json([map { @$_ == 1 ? $_->[0] : $_ } @classes]);
  3         11  
51 1         263 Carp::confess(qq(Could not find "$file" in __DATA__ section of $err.));
52             }
53              
54             sub data_type {
55 1317     1317 1 2960 my $ref = ref $_[0];
56 1317         2319 my $blessed = blessed $_[0];
57 1317 100       3354 return 'object' if $ref eq 'HASH';
58 1000 100 100     2632 return lc $ref if $ref and !$blessed;
59 873 100       1758 return 'null' if !defined $_[0];
60 855 100 100     1824 return 'boolean' if $blessed and ("$_[0]" eq "1" or !"$_[0]");
      100        
61              
62 814 100       1413 if (is_type($_[0], 'NUM')) {
63 193 100 100     368 return 'integer' if grep { ($_->{type} // '') eq 'integer' } @{$_[1] || []};
  249 100       1222  
  193         733  
64 102         474 return 'number';
65             }
66              
67 621   100     2676 return $blessed || 'string';
68             }
69              
70             sub is_type {
71 111977     111977 1 140142 my $type = $_[1];
72              
73 111977 100       168928 if ($type eq 'BOOL') {
74 21499   100     67313 return blessed $_[0]
75             && ($_[0]->isa('JSON::PP::Boolean') || "$_[0]" eq "1" || !$_[0]);
76             }
77              
78             # NUM
79 90478 100       120248 if ($type eq 'NUM') {
80             return
81 1976   66     13810 B::svref_2object(\$_[0])->FLAGS & (B::SVp_IOK | B::SVp_NOK)
82             && 0 + $_[0] eq $_[0]
83             && $_[0] * 0 == 0;
84             }
85              
86             # Class or data type
87 88502 100       289806 return blessed $_[0] ? $_[0]->isa($type) : ref $_[0] eq $type;
88             }
89              
90             sub schema_extract {
91 12     12 1 64 my ($data, $p, $cb) = @_;
92 12 50       43 $p = [ref $p ? @$p : length $p ? split('/', $p, -1) : $p];
    100          
93 12 100 66     68 shift @$p if @$p and defined $p->[0] and !length $p->[0];
      100        
94 12         39 _schema_extract($data, $p, '', $cb);
95             }
96              
97             sub json_pointer {
98 2692     2692 1 4580 local $_ = $_[1];
99 2692         5088 s!~!~0!g;
100 2692         3444 s!/!~1!g;
101 2692         10001 "$_[0]/$_";
102             }
103              
104             sub prefix_errors {
105 31     31 1 98 my ($type, @errors_with_index) = @_;
106 31         47 my @errors;
107              
108 31         71 for my $e (@errors_with_index) {
109 42         88 my $index = shift @$e;
110             push @errors, map {
111 42         96 my $msg = sprintf '/%s/%s %s', $type, $index, $_->message;
  44         135  
112 44         220 $msg =~ s!(\d+)\s/!$1/!g;
113 44         150 E $_->path, $msg;
114             } @$e;
115             }
116              
117 31         130 return @errors;
118             }
119              
120             sub schema_type {
121 2137 100   2137 1 5256 return $_[0]->{type} if $_[0]->{type};
122 1996 100       3484 return _guessed_right(object => $_[1]) if $_[0]->{additionalProperties};
123 1980 100       3445 return _guessed_right(object => $_[1]) if $_[0]->{patternProperties};
124 1961 100       3313 return _guessed_right(object => $_[1]) if $_[0]->{properties};
125 1858 100       3023 return _guessed_right(object => $_[1]) if $_[0]->{propertyNames};
126 1857 100       3075 return _guessed_right(object => $_[1]) if $_[0]->{required};
127             return _guessed_right(object => $_[1])
128             if defined $_[0]->{maxProperties}
129 1855 100 100     5414 or defined $_[0]->{minProperties};
130 1847 100       3240 return _guessed_right(array => $_[1]) if $_[0]->{additionalItems};
131 1845 100       3226 return _guessed_right(array => $_[1]) if $_[0]->{items};
132 1829 100       3049 return _guessed_right(array => $_[1]) if $_[0]->{uniqueItems};
133             return _guessed_right(array => $_[1])
134             if defined $_[0]->{maxItems}
135 1816 100 100     5014 or defined $_[0]->{minItems};
136 1800 100       3283 return _guessed_right(string => $_[1]) if $_[0]->{pattern};
137             return _guessed_right(string => $_[1])
138             if defined $_[0]->{maxLength}
139 1760 100 100     4649 or defined $_[0]->{minLength};
140 1731 100       2740 return _guessed_right(number => $_[1]) if $_[0]->{multipleOf};
141             return _guessed_right(number => $_[1])
142             if defined $_[0]->{maximum}
143 1719 100 100     4380 or defined $_[0]->{minimum};
144 1686 100       2902 return 'const' if exists $_[0]->{const};
145 1669         3891 return '';
146             }
147              
148             # _guessed_right($type, $data);
149             sub _guessed_right {
150 310 100   310   865 return $_[0] if !defined $_[1];
151 273 100       955 return $_[0] if $_[0] eq data_type $_[1], [{type => $_[0]}];
152 19         113 return '';
153             }
154              
155             sub _schema_extract {
156 33     33   54 my ($data, $path, $pos, $cb) = @_, my $tied;
157              
158 33         57 while (@$path) {
159 44         56 my $p = shift @$path;
160              
161 44 100       75 unless (defined $p) {
162 9         12 my $i = 0;
163             return Mojo::Collection->new(
164             map {
165 21         53 _schema_extract($_->[0], [@$path], json_pointer($pos, $_->[1]), $cb)
166 17         30 } ref $data eq 'ARRAY' ? map { [$_, $i++] }
167 9 50       26 @$data : ref $data eq 'HASH' ? map { [$data->{$_}, $_] }
  4 100       9  
168             sort keys %$data : [$data, '']
169             );
170             }
171              
172 35         54 $p =~ s!~1!/!g;
173 35         39 $p =~ s/~0/~/g;
174 35 100       52 $pos = json_pointer $pos, $p if $cb;
175              
176 35 100 100     127 if (ref $data eq 'HASH' and exists $data->{$p}) {
    100 66        
      66        
177 26         37 $data = $data->{$p};
178             }
179             elsif (ref $data eq 'ARRAY' and $p =~ /^\d+$/ and @$data > $p) {
180 2         3 $data = $data->[$p];
181             }
182             else {
183 7         32 return undef;
184             }
185              
186 28 100 100     81 $data = $tied->schema if ref $data eq 'HASH' and $tied = tied %$data;
187             }
188              
189 17 100       28 return $cb->($data, $pos) if $cb;
190 15         44 return $data;
191             }
192              
193             1;
194              
195             =encoding utf8
196              
197             =head1 NAME
198              
199             JSON::Validator::Util - Utility functions for JSON::Validator
200              
201             =head1 DESCRIPTION
202              
203             L is a package containing utility functions for
204             L. Each of the L can be imported.
205              
206             =head1 FUNCTIONS
207              
208             =head2 data_checksum
209              
210             $str = data_checksum $any;
211              
212             Will create a checksum for any data structure stored in C<$any>.
213              
214             =head2 data_section
215              
216             $str = data_section "Some::Module", "file.json";
217             $str = data_section "Some::Module", "file.json", {encode => 'UTF-8'};
218              
219             Same as L, but will also look up the file in any
220             inherited class.
221              
222             =head2 data_type
223              
224             $str = data_type $any;
225             $str = data_type $any, [@schemas];
226             $str = data_type $any, [{type => "integer", ...}];
227              
228             Returns the JSON type for C<$any>. C<$str> can be array, boolean, integer,
229             null, number object or string. Note that a list of schemas need to be provided
230             to differentiate between "integer" and "number".
231              
232             =head2 is_type
233              
234             $bool = is_type $any, $class;
235             $bool = is_type $any, $type; # $type = "ARRAY", "BOOL", "HASH", "NUM" ...
236              
237             Checks if C<$any> is a, or inherits from, C<$class> or C<$type>. Two special
238             types can be checked:
239              
240             =over 2
241              
242             =item * BOOL
243              
244             Checks if C<$any> is a boolean value. C<$any> is considered boolean if it is an
245             object inheriting from L or is another object that
246             stringifies to "1" or "0".
247              
248             =item * NUM
249              
250             Checks if C<$any> is indeed a number.
251              
252             =back
253              
254             =head2 json_pointer
255              
256             $str = json_pointer $path, $append;
257              
258             Will concat C<$append> on to C<$path>, but will also escape the two special
259             characters "~" and "/" in C<$append>.
260              
261             =head2 prefix_errors
262              
263             @errors = prefix_errors $prefix, @errors;
264              
265             Consider this internal for now.
266              
267             =head2 schema_extract
268              
269             $data = schema_extract $any, $json_pointer;
270             $data = schema_extract $any, "/x/cool_beans/y";
271             $collection = schema_extract $any, ["x", undef, "y"];
272             schema_extract $any, $json_pointer, sub { my ($data, $json_pointer) = @_ };
273              
274             The basic usage is to extract data from C<$any>, using a C<$json_pointer> -
275             L. It can however be used in a
276             more complex way by passing in an array-ref, instead of a plain string. The
277             array-ref can contain C values, will result in extracting any element
278             on that point, regardsless of value. In that case a L will
279             be returned.
280              
281             A callback can also be given. This callback will be called each time the
282             C<$json_pointer> matches some data, and will pass in the C<$json_pointer> at
283             that place.
284              
285             In addition, if the C<$json_pointer> points to a L at any
286             point, the "$ref" will be followed, while if you used L,
287             it would return either the L or C.
288              
289             Even though L has special capabilities for handling a
290             JSON-Schema, it can be used for any data-structure, just like
291             L.
292              
293             =head2 schema_type
294              
295             $str = schema_type $hash_ref;
296             $str = schema_type $hash_ref, $any;
297              
298             Looks at C<$hash_ref> and tries to figure out what kind of type the schema
299             represents. C<$str> can be "array", "const", "number", "object", "string", or
300             fallback to empty string if the correct type could not be figured out.
301              
302             C<$any> can be provided to double check the type, so if C<$hash_ref> describes
303             an "object", but C<$any> is an array-ref, then C<$str> will become an empty
304             string. Example:
305              
306             # $str = "";
307             $str = schema {additionalProperties => false}, [];
308              
309             # $str = "object"
310             $str = schema {additionalProperties => false};
311             $str = schema {additionalProperties => false}, {};
312              
313             Note that this process is relatively slow, so it will make your validation
314             faster if you specify "type". Both of the two below is valid, but the one with
315             "type" will be faster.
316              
317             {"type": "object", "properties": {}} # Faster
318             {"properties": {}} # Slower
319              
320             =head1 SEE ALSO
321              
322             L.
323              
324             =cut