File Coverage

blib/lib/JSON/Schema/Draft201909/Utilities.pm
Criterion Covered Total %
statement 161 163 98.7
branch 77 82 93.9
condition 71 89 79.7
subroutine 34 35 97.1
pod 0 15 0.0
total 343 384 89.3


line stmt bran cond sub pod time code
1 22     22   256756 use strict;
  22         74  
  22         787  
2 22     22   149 use warnings;
  22         53  
  22         1235  
3             package JSON::Schema::Draft201909::Utilities;
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Internal utilities for JSON::Schema::Draft201909
6              
7             our $VERSION = '0.028';
8              
9 22     22   544 use 5.016;
  22         90  
10 22     22   143 no if "$]" >= 5.031009, feature => 'indirect';
  22         50  
  22         245  
11 22     22   1146 no if "$]" >= 5.033001, feature => 'multidimensional';
  22         71  
  22         136  
12 22     22   1025 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  22         53  
  22         140  
13 22     22   2114 use strictures 2;
  22         3910  
  22         950  
14 22     22   4852 use B;
  22         56  
  22         1438  
15 22     22   160 use Carp 'croak';
  22         54  
  22         1418  
16 22     22   1239 use JSON::MaybeXS 1.004001 'is_bool';
  22         17261  
  22         1646  
17 22     22   1289 use Ref::Util 0.100 qw(is_ref is_plain_arrayref is_plain_hashref);
  22         3992  
  22         1440  
18 22     22   1699 use Storable 'dclone';
  22         8196  
  22         1780  
19 22     22   1395 use Feature::Compat::Try;
  22         856  
  22         242  
20 22     22   8719 use JSON::Schema::Draft201909::Error;
  22         60  
  22         815  
21 22     22   1260 use JSON::Schema::Draft201909::Annotation;
  22         63  
  22         679  
22 22     22   196 use namespace::clean;
  22         57  
  22         221  
23              
24 22     22   7567 use Exporter 'import';
  22         63  
  22         1619  
25              
26             our @EXPORT_OK = qw(
27             is_type
28             get_type
29             is_equal
30             is_elements_unique
31             jsonp
32             local_annotations
33             canonical_schema_uri
34             E
35             A
36             abort
37             assert_keyword_type
38             assert_pattern
39             assert_uri_reference
40             assert_uri
41             annotate_self
42             true
43             false
44             );
45              
46 22     22   2011 use JSON::PP ();
  22         29745  
  22         1171  
47 22     22   174 use constant { true => JSON::PP::true, false => JSON::PP::false };
  22         57  
  22         128  
48              
49             sub is_type {
50 22872     22872 0 288545 my ($type, $value) = @_;
51              
52 22872 100       52463 if ($type eq 'null') {
53 102         531 return !(defined $value);
54             }
55 22770 100       47095 if ($type eq 'boolean') {
56 2756         10342 return is_bool($value);
57             }
58 20014 100       40941 if ($type eq 'object') {
59 5629         27331 return is_plain_hashref($value);
60             }
61 14385 100       30713 if ($type eq 'array') {
62 3542         16396 return is_plain_arrayref($value);
63             }
64              
65 10843 100 100     40416 if ($type eq 'string' or $type eq 'number' or $type eq 'integer') {
      100        
66 10842 100 100     45950 return 0 if not defined $value or is_ref($value);
67 10173         52605 my $flags = B::svref_2object(\$value)->FLAGS;
68              
69 10173 100       29439 if ($type eq 'string') {
70 6665   66     48642 return $flags & B::SVf_POK && !($flags & (B::SVf_IOK | B::SVf_NOK));
71             }
72              
73 3508 100       8938 if ($type eq 'number') {
74 2041   66     16100 return !($flags & B::SVf_POK) && ($flags & (B::SVf_IOK | B::SVf_NOK));
75             }
76              
77 1467 50       3622 if ($type eq 'integer') {
78 1467   100     13241 return !($flags & B::SVf_POK) && ($flags & (B::SVf_IOK | B::SVf_NOK))
79             && int($value) == $value;
80             }
81             }
82              
83 1         161 croak sprintf('unknown type "%s"', $type);
84             }
85              
86             # only the core six types are reported (integers are numbers)
87             # use is_type('integer') to differentiate numbers from integers.
88             sub get_type {
89 22217     22217 0 100079 my ($value) = @_;
90              
91 22217 100       53214 return 'null' if not defined $value;
92 22159 100       71909 return 'object' if is_plain_hashref($value);
93 5034 100       11567 return 'array' if is_plain_arrayref($value);
94 4684 100       13624 return 'boolean' if is_bool($value);
95              
96 2123 100       13658 croak sprintf('unsupported reference type %s', ref $value) if is_ref($value);
97              
98 2114         7072 my $flags = B::svref_2object(\$value)->FLAGS;
99 2114 100 100     9182 return 'string' if $flags & B::SVf_POK && !($flags & (B::SVf_IOK | B::SVf_NOK));
100 712 100 66     3465 return 'number' if !($flags & B::SVf_POK) && ($flags & (B::SVf_IOK | B::SVf_NOK));
101              
102 1         11 croak sprintf('ambiguous type for %s',
103             JSON::MaybeXS->new(allow_nonref => 1, canonical => 1, utf8 => 0)->encode($value));
104             }
105              
106             # compares two arbitrary data payloads for equality, as per
107             # https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.4.2.3
108             # if provided with a state hashref, any differences are recorded within
109             sub is_equal {
110 1495     1495 0 3357 my ($x, $y, $state) = @_;
111 1495   100     6793 $state->{path} //= '';
112              
113 1495         3645 my @types = map get_type($_), $x, $y;
114 1495 100       5569 return 0 if $types[0] ne $types[1];
115 1130 100       2449 return 1 if $types[0] eq 'null';
116 1124 100       4216 return $x eq $y if $types[0] eq 'string';
117 507 100 100     2767 return $x == $y if $types[0] eq 'boolean' or $types[0] eq 'number';
118              
119 216         399 my $path = $state->{path};
120 216 100       469 if ($types[0] eq 'object') {
121 88 100       293 return 0 if keys %$x != keys %$y;
122 82 100       387 return 0 if not is_equal([ sort keys %$x ], [ sort keys %$y ]);
123 76         269 foreach my $property (sort keys %$x) {
124 94         208 $state->{path} = jsonp($path, $property);
125 94 100       243 return 0 if not is_equal($x->{$property}, $y->{$property}, $state);
126             }
127 38         241 return 1;
128             }
129              
130 128 50       282 if ($types[0] eq 'array') {
131 128 100       324 return 0 if @$x != @$y;
132 124         198 foreach my $idx (0 .. $#{$x}) {
  124         382  
133 148         432 $state->{path} = $path.'/'.$idx;
134 148 100       358 return 0 if not is_equal($x->[$idx], $y->[$idx], $state);
135             }
136 90         407 return 1;
137             }
138              
139 0         0 return 0; # should never get here
140             }
141              
142             # checks array elements for uniqueness. short-circuits on first pair of matching elements
143             # if second arrayref is provided, it is populated with the indices of identical items
144             sub is_elements_unique {
145 882     882 0 2156 my ($array, $equal_indices) = @_;
146 882         1641 foreach my $idx0 (0 .. $#{$array}-1) {
  882         2994  
147 357         715 foreach my $idx1 ($idx0+1 .. $#{$array}) {
  357         886  
148 574 100       1500 if (is_equal($array->[$idx0], $array->[$idx1])) {
149 62 50       294 push @$equal_indices, $idx0, $idx1 if defined $equal_indices;
150 62         258 return 0;
151             }
152             }
153             }
154 820         3819 return 1;
155             }
156              
157             # shorthand for creating and appending json pointers
158             sub jsonp {
159 21294     21294 0 224743 return join('/', shift, map s/~/~0/gr =~ s!/!~1!gr, grep defined, @_);
160             }
161              
162             # get all annotations produced for the current instance data location (that are visible to this
163             # schema location)
164             sub local_annotations {
165 194     194 0 520 my ($state) = @_;
166 194         383 grep $_->instance_location eq $state->{data_path}, @{$state->{annotations}};
  194         966  
167             }
168              
169             # shorthand for finding the canonical uri of the present schema location
170             sub canonical_schema_uri {
171 13168     13168 0 37933 my ($state, @extra_path) = @_;
172              
173 13168         45617 my $uri = $state->{initial_schema_uri}->clone;
174 13168   100     1046020 $uri->fragment(($uri->fragment//'').jsonp($state->{schema_path}, @extra_path));
175 13168 100       90937 $uri->fragment(undef) if not length($uri->fragment);
176 13168         99587 $uri;
177             }
178              
179             # shorthand for creating error objects
180             sub E {
181 3694     3694 0 26158 my ($state, $error_string, @args) = @_;
182              
183             # sometimes the keyword shouldn't be at the very end of the schema path
184 3694         14740 my $uri = canonical_schema_uri($state, $state->{keyword}, $state->{_schema_path_suffix});
185              
186             my $keyword_location = $state->{traversed_schema_path}
187 3694         13618 .jsonp($state->{schema_path}, $state->{keyword}, delete $state->{_schema_path_suffix});
188              
189 3694 100 100     14362 undef $uri if $uri eq '' and $keyword_location eq ''
      100        
      100        
      100        
190             or ($uri->fragment // '') eq $keyword_location and $uri->clone->fragment(undef) eq '';
191              
192 3694         105577 push @{$state->{errors}}, JSON::Schema::Draft201909::Error->new(
193             keyword => $state->{keyword},
194             instance_location => $state->{data_path},
195 3694 100       1241569 keyword_location => $keyword_location,
    100          
196             defined $uri ? ( absolute_keyword_location => $uri ) : (),
197             error => @args ? sprintf($error_string, @args) : $error_string,
198             );
199              
200 3694         429478 return 0;
201             }
202              
203             # shorthand for creating annotations
204             sub A {
205 2767     2767 0 6475 my ($state, $annotation) = @_;
206 2767 100       11457 return 1 if not $state->{collect_annotations};
207              
208 387         1554 my $uri = canonical_schema_uri($state, $state->{keyword}, $state->{_schema_path_suffix});
209              
210             my $keyword_location = $state->{traversed_schema_path}
211 387         1387 .jsonp($state->{schema_path}, $state->{keyword}, delete $state->{_schema_path_suffix});
212              
213 387 100 33     1455 undef $uri if $uri eq '' and $keyword_location eq ''
      50        
      100        
      66        
214             or ($uri->fragment // '') eq $keyword_location and $uri->clone->fragment(undef) eq '';
215              
216 387         10014 push @{$state->{annotations}}, JSON::Schema::Draft201909::Annotation->new(
217             keyword => $state->{keyword},
218             instance_location => $state->{data_path},
219 387 100       134506 keyword_location => $keyword_location,
220             defined $uri ? ( absolute_keyword_location => $uri ) : (),
221             annotation => $annotation,
222             );
223              
224 387         36165 return 1;
225             }
226              
227             # creates an error object, but also aborts evaluation immediately
228             # only this error is returned, because other errors on the stack might not actually be "real"
229             # errors (consider if we were in the middle of evaluating a "not" or "if")
230             sub abort {
231 233     233 0 943 my ($state, $error_string, @args) = @_;
232 233         861 E($state, $error_string, @args);
233 233         481 die pop @{$state->{errors}};
  233         4206  
234             }
235              
236             sub assert_keyword_type {
237 10542     10542 0 22737 my ($state, $schema, $type) = @_;
238 10542 100       28629 return 1 if is_type($type, $schema->{$state->{keyword}});
239 6 50       40 E($state, '%s value is not a%s %s', $state->{keyword}, ($type =~ /^[aeiou]/ ? 'n' : ''), $type);
240             }
241              
242             sub assert_pattern {
243 695     695 0 1929 my ($state, $pattern) = @_;
244             try {
245 0     0   0 local $SIG{__WARN__} = sub { die @_ };
246             qr/$pattern/;
247             }
248 695         1824 catch ($e) { return E($state, $e); };
249 692         3380 return 1;
250             }
251              
252             sub assert_uri_reference {
253 769     769 0 1898 my ($state, $schema) = @_;
254              
255 769         2203 my $ref = $schema->{$state->{keyword}};
256              
257             return E($state, '%s value is not a valid URI reference', $state->{keyword})
258             # see also uri-reference format sub
259 769 100 66     2979 if fc(Mojo::URL->new($ref)->to_unsafe_string) ne fc($ref)
      100        
      100        
      100        
      100        
260             or $ref =~ /[^[:ascii:]]/
261             or $ref =~ /#/
262             and $ref !~ m{#$} # empty fragment
263             and $ref !~ m{#[A-Za-z][A-Za-z0-9_:.-]*$} # plain-name fragment
264             and $ref !~ m{#/(?:[^~]|~[01])*$}; # json pointer fragment
265              
266 757         294982 return 1;
267             }
268              
269             sub assert_uri {
270 88     88 0 207 my ($state, $schema, $override) = @_;
271              
272 88   66     278 my $string = $override // $schema->{$state->{keyword}};
273 88         285 my $uri = Mojo::URL->new($string);
274              
275 88 50 66     8429 return E($state, '"%s" is not a valid URI', $string)
      100        
      33        
      33        
      0        
      66        
276             # see also uri format sub
277             if fc($uri->to_unsafe_string) ne fc($string)
278             or $string =~ /[^[:ascii:]]/
279             or not $uri->is_abs
280             or $string =~ /#/
281             and $string !~ m{#$} # empty fragment
282             and $string !~ m{#[A-Za-z][A-Za-z0-9_:.-]*$} # plain-name fragment
283             and $string !~ m{#/(?:[^~]|~[01])*$}; # json pointer fragment
284              
285 82         16881 return 1;
286             }
287              
288             # produces an annotation whose value is the same as that of the current keyword
289             sub annotate_self {
290 521     521 0 1015 my ($state, $schema) = @_;
291             A($state, is_ref($schema->{$state->{keyword}}) ? dclone($schema->{$state->{keyword}})
292 521 100       4934 : $schema->{$state->{keyword}});
293             }
294              
295             1;
296              
297             __END__
298              
299             =pod
300              
301             =encoding UTF-8
302              
303             =head1 NAME
304              
305             JSON::Schema::Draft201909::Utilities - Internal utilities for JSON::Schema::Draft201909
306              
307             =head1 VERSION
308              
309             version 0.028
310              
311             =head1 SYNOPSIS
312              
313             use JSON::Schema::Draft201909::Utilities qw(func1 func2..);
314              
315             =head1 DESCRIPTION
316              
317             This class contains internal utilities to be used by L<JSON::Schema::Draft201909>.
318              
319             =for Pod::Coverage is_type get_type is_equal is_elements_unique jsonp local_annotations
320             canonical_schema_uri E A abort assert_keyword_type assert_pattern assert_uri_reference assert_uri
321             annotate_self
322              
323             =head1 SUPPORT
324              
325             Bugs may be submitted through L<https://github.com/karenetheridge/JSON-Schema-Draft201909/issues>.
326              
327             I am also usually active on irc, as 'ether' at C<irc.perl.org> and C<irc.libera.chat>.
328              
329             =head1 AUTHOR
330              
331             Karen Etheridge <ether@cpan.org>
332              
333             =head1 COPYRIGHT AND LICENCE
334              
335             This software is copyright (c) 2020 by Karen Etheridge.
336              
337             This is free software; you can redistribute it and/or modify it under
338             the same terms as the Perl 5 programming language system itself.
339              
340             =cut