File Coverage

blib/lib/JSON/Schema/Modern/Utilities.pm
Criterion Covered Total %
statement 231 244 94.6
branch 113 130 86.9
condition 71 84 84.5
subroutine 39 42 92.8
pod 0 18 0.0
total 454 518 87.6


line stmt bran cond sub pod time code
1 36     36   272271 use strict;
  36         133  
  36         1277  
2 36     36   293 use warnings;
  36         174  
  36         1996  
3             package JSON::Schema::Modern::Utilities;
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Internal utilities for JSON::Schema::Modern
6              
7             our $VERSION = '0.571';
8              
9 36     36   862 use 5.020;
  36         157  
10 36     36   263 use strictures 2;
  36         386  
  36         1579  
11 36     36   7899 use stable 0.031 'postderef';
  36         716  
  36         314  
12 36     36   7045 use experimental 'signatures';
  36         148  
  36         220  
13 36     36   3301 use if "$]" >= 5.022, experimental => 're_strict';
  36         125  
  36         487  
14 36     36   3768 no if "$]" >= 5.031009, feature => 'indirect';
  36         168  
  36         367  
15 36     36   2096 no if "$]" >= 5.033001, feature => 'multidimensional';
  36         119  
  36         268  
16 36     36   1918 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  36         136  
  36         290  
17 36     36   1521 use B;
  36         118  
  36         2222  
18 36     36   291 use Carp 'croak';
  36         110  
  36         2747  
19 36     36   1338 use JSON::MaybeXS 1.004004 'is_bool';
  36         17093  
  36         3008  
20 36     36   1300 use Ref::Util 0.100 qw(is_ref is_plain_arrayref is_plain_hashref);
  36         3940  
  36         2393  
21 36     36   310 use Scalar::Util 'blessed';
  36         102  
  36         2077  
22 36     36   1596 use Storable 'dclone';
  36         6974  
  36         2313  
23 36     36   1275 use Feature::Compat::Try;
  36         762  
  36         540  
24 36     36   11917 use JSON::Schema::Modern::Error;
  36         135  
  36         1300  
25 36     36   1303 use JSON::Schema::Modern::Annotation;
  36         88  
  36         1226  
26 36     36   254 use namespace::clean;
  36         115  
  36         437  
27              
28 36     36   12384 use Exporter 'import';
  36         112  
  36         3258  
29              
30             our @EXPORT_OK = qw(
31             is_type
32             get_type
33             is_equal
34             is_elements_unique
35             jsonp
36             unjsonp
37             local_annotations
38             canonical_uri
39             E
40             A
41             abort
42             assert_keyword_exists
43             assert_keyword_type
44             assert_pattern
45             assert_uri_reference
46             assert_uri
47             annotate_self
48             sprintf_num
49             true
50             false
51             );
52              
53 36     36   325 use JSON::PP ();
  36         134  
  36         1842  
54 36     36   729 use constant { true => JSON::PP::true, false => JSON::PP::false };
  36         115  
  36         1619  
55              
56 72373     72373 0 482993 sub is_type ($type, $value) {
  72373         111380  
  72373         109013  
  72373         98901  
57 72373 100       146265 if ($type eq 'null') {
58 71         340 return !(defined $value);
59             }
60 72302 100       141238 if ($type eq 'boolean') {
61 8201         29046 return is_bool($value);
62             }
63 64101 100       125328 if ($type eq 'object') {
64 18243         86797 return is_plain_hashref($value);
65             }
66 45858 100       92872 if ($type eq 'array') {
67 11148         53268 return is_plain_arrayref($value);
68             }
69              
70 34710 100 100     106644 if ($type eq 'string' or $type eq 'number' or $type eq 'integer') {
      100        
71 34700 100       69418 return 0 if not defined $value;
72 34686         159175 my $flags = B::svref_2object(\$value)->FLAGS;
73              
74 34686 100       92536 if ($type eq 'string') {
75 24306   66     219934 return !is_ref($value) && $flags & B::SVf_POK && !($flags & (B::SVf_IOK | B::SVf_NOK));
76             }
77              
78 10380 100       24675 if ($type eq 'number') {
79 6892   100     59531 return ref($value) =~ /^Math::Big(?:Int|Float)$/
80             || !($flags & B::SVf_POK) && ($flags & (B::SVf_IOK | B::SVf_NOK));
81             }
82              
83 3488 50       7760 if ($type eq 'integer') {
84 3488   100     35853 return ref($value) =~ /^Math::Big(?:Int|Float)$/ && $value->is_int
85             || !($flags & B::SVf_POK) && ($flags & (B::SVf_IOK | B::SVf_NOK)) && int($value) == $value;
86             }
87             }
88              
89 10 100       52 if ($type =~ /^reference to (.+)$/) {
90 6   33     75 return !blessed($value) && ref($value) eq $1;
91             }
92              
93 4         26 return ref($value) eq $type;
94             }
95              
96 77807     77807 0 182891 sub get_type ($value) {
  77807         123466  
  77807         110201  
97 77807 100       246856 return 'object' if is_plain_hashref($value);
98 21689 100       64187 return 'boolean' if is_bool($value);
99 12886 100       93027 return 'null' if not defined $value;
100 12573 100       27611 return 'array' if is_plain_arrayref($value);
101              
102 11321 100       26781 return ref($value) =~ /^Math::Big(?:Int|Float)$/ ? ($value->is_int ? 'integer' : 'number')
    100          
    100          
    100          
103             : (blessed($value) ? '' : 'reference to ').ref($value)
104             if is_ref($value);
105              
106 10868         40957 my $flags = B::svref_2object(\$value)->FLAGS;
107 10868 100 100     47673 return 'string' if $flags & B::SVf_POK && !($flags & (B::SVf_IOK | B::SVf_NOK));
108 4748 100 66     30476 return int($value) == $value ? 'integer' : 'number'
    100          
109             if !($flags & B::SVf_POK) && ($flags & (B::SVf_IOK | B::SVf_NOK));
110              
111 2         10 return 'ambiguous type';
112             }
113              
114             # compares two arbitrary data payloads for equality, as per
115             # https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.4.2.2
116             # if provided with a state hashref with a 'path' key, any differences are recorded within
117 4453     4453 0 7320 sub is_equal ($x, $y, $state = undef) {
  4453         6975  
  4453         6758  
  4453         6928  
  4453         6320  
118 4453   100     18390 $state->{path} //= '';
119              
120 4453         10383 my @types = map get_type($_), $x, $y;
121              
122 4453 100       15643 if ($state->{scalarref_booleans}) {
123 1582 100       3819 ($x, $types[0]) = (0+!!$$x, 'boolean') if $types[0] eq 'reference to SCALAR';
124 1582 100       3460 ($y, $types[1]) = (0+!!$$y, 'boolean') if $types[1] eq 'reference to SCALAR';
125             }
126              
127 4453 100       12226 return 0 if $types[0] ne $types[1];
128 3560 100       7390 return 1 if $types[0] eq 'null';
129 3546 100       14136 return $x eq $y if $types[0] eq 'string';
130 1625 100       8699 return $x == $y if grep $types[0] eq $_, qw(boolean number integer);
131              
132 542         1010 my $path = $state->{path};
133 542 100       1204 if ($types[0] eq 'object') {
134 214 100       648 return 0 if keys %$x != keys %$y;
135 198 100       1053 return 0 if not is_equal([ sort keys %$x ], [ sort keys %$y ]);
136 192         776 foreach my $property (sort keys %$x) {
137 230         595 $state->{path} = jsonp($path, $property);
138 230 100       656 return 0 if not is_equal($x->{$property}, $y->{$property}, $state);
139             }
140 106         740 return 1;
141             }
142              
143 328 50       808 if ($types[0] eq 'array') {
144 328 100       850 return 0 if @$x != @$y;
145 320         881 foreach my $idx (0 .. $x->$#*) {
146 364         1066 $state->{path} = $path.'/'.$idx;
147 364 100       960 return 0 if not is_equal($x->[$idx], $y->[$idx], $state);
148             }
149 232         1140 return 1;
150             }
151              
152 0         0 return 0; # should never get here
153             }
154              
155             # checks array elements for uniqueness. short-circuits on first pair of matching elements
156             # if second arrayref is provided, it is populated with the indices of identical items
157 2849     2849 0 5049 sub is_elements_unique ($array, $equal_indices = undef) {
  2849         4866  
  2849         4948  
  2849         4277  
158 2849         9071 foreach my $idx0 (0 .. $array->$#*-1) {
159 985         2773 foreach my $idx1 ($idx0+1 .. $array->$#*) {
160 1390 100       4822 if (is_equal($array->[$idx0], $array->[$idx1], { scalarref_booleans => 1 })) {
161 211 50       2386 push @$equal_indices, $idx0, $idx1 if defined $equal_indices;
162 211         970 return 0;
163             }
164             }
165             }
166 2638         9188 return 1;
167             }
168              
169             # shorthand for creating and appending json pointers
170             # the first argument is a a json pointer; remaining arguments are path segments to be encoded and
171             # appended
172             sub jsonp {
173 45526 100   45526 0 581472 return join('/', shift, map s/~/~0/gr =~ s!/!~1!gr, map +(is_plain_arrayref($_) ? @$_ : $_), grep defined, @_);
174             }
175              
176             # splits a json pointer apart into its path segments
177 0     0 0 0 sub unjsonp ($path) {
  0         0  
  0         0  
178 0         0 return map s!~0!~!gr =~ s!~1!/!gr, split m!/!, $path;
179             }
180              
181             # get all annotations produced for the current instance data location (that are visible to this
182             # schema location) - remember these are hashrefs, not Annotation objects
183 1200     1200 0 2022 sub local_annotations ($state) {
  1200         2147  
  1200         1923  
184 1200         4914 grep $_->{instance_location} eq $state->{data_path}, $state->{annotations}->@*;
185             }
186              
187             # shorthand for finding the canonical uri of the present schema location
188             # last argument can be an arrayref, usually coming from $state->{_schema_path_suffix}
189 41279     41279 0 66337 sub canonical_uri ($state, @extra_path) {
  41279         61758  
  41279         77390  
  41279         59892  
190 41279 100 100     173299 return $state->{initial_schema_uri} if not @extra_path and not length($state->{schema_path});
191 23870 100 100     86009 splice(@extra_path, -1, 1, $extra_path[-1]->@*) if @extra_path and is_plain_arrayref($extra_path[-1]);
192 23870         84602 my $uri = $state->{initial_schema_uri}->clone;
193 23870 100 100     1861990 my $fragment = ($uri->fragment//'').(@extra_path ? jsonp($state->{schema_path}, @extra_path) : $state->{schema_path});
194 23870 100       132554 undef $fragment if not length($fragment);
195 23870         70299 $uri->fragment($fragment);
196 23870         179963 $uri;
197             }
198              
199             # shorthand for creating error objects
200             # uses these keys from $state:
201             # - initial_schema_uri
202             # - keyword
203             # - data_path
204             # - traversed_schema_path
205             # - schema_path
206             # - _schema_path_suffix
207             # - errors
208 11206     11206 0 45630 sub E ($state, $error_string, @args) {
  11206         17898  
  11206         19843  
  11206         20716  
  11206         16935  
209 11206 50       27801 croak 'E called in void context' if not defined wantarray;
210              
211             # sometimes the keyword shouldn't be at the very end of the schema path
212             my $uri = canonical_uri($state, $state->{keyword}, $state->{_schema_path_suffix})
213 11206         40182 ->to_abs($state->{effective_base_uri});
214              
215             my $keyword_location = $state->{traversed_schema_path}
216 11206         2770579 .jsonp($state->{schema_path}, $state->{keyword}, delete $state->{_schema_path_suffix});
217              
218 11206 100 100     41809 undef $uri if $uri eq '' and $keyword_location eq ''
      100        
      100        
      100        
219             or ($uri->fragment // '') eq $keyword_location and $uri->clone->fragment(undef) eq '';
220              
221             push $state->{errors}->@*, JSON::Schema::Modern::Error->new(
222             keyword => $state->{keyword},
223             instance_location => $state->{data_path},
224             keyword_location => $keyword_location,
225             defined $uri ? ( absolute_keyword_location => $uri ) : (),
226             error => @args ? sprintf($error_string, @args) : $error_string,
227 11206 100       4577182 $state->{exception} ? ( exception => $state->{exception} ) : (),
    100          
    100          
228             );
229              
230 11206         794694 return 0;
231             }
232              
233             # shorthand for creating annotations
234             # uses these keys from $state:
235             # - initial_schema_uri
236             # - keyword
237             # - data_path
238             # - traversed_schema_path
239             # - schema_path
240             # - _schema_path_suffix
241             # - annotations
242             # - collect_annotations
243 10764     10764 0 17131 sub A ($state, $annotation) {
  10764         16880  
  10764         17562  
  10764         16488  
244 10764 100 100     42115 return 1 if not $state->{collect_annotations} or $state->{spec_version} eq 'draft7';
245              
246             # we store the absolute uri in unresolved form until needed,
247             # and perform the rest of the calculations later.
248              
249             my $uri = [ canonical_uri($state, $state->{keyword}, $state->{_schema_path_suffix}),
250 2466         8982 $state->{effective_base_uri} ];
251              
252             my $keyword_location = $state->{traversed_schema_path}
253 2466         8732 .jsonp($state->{schema_path}, $state->{keyword}, delete $state->{_schema_path_suffix});
254              
255             push $state->{annotations}->@*, {
256             keyword => $state->{keyword},
257             instance_location => $state->{data_path},
258             keyword_location => $keyword_location,
259             # we calculate absolute_keyword_location when instantiating the Annotation object for Result
260             _uri => $uri,
261             annotation => $annotation,
262 2466 100       15299 $state->{_unknown} ? ( unknown => 1 ) : (),
263             };
264              
265 2466         7257 return 1;
266             }
267              
268             # creates an error object, but also aborts evaluation immediately
269             # only this error is returned, because other errors on the stack might not actually be "real"
270             # errors (consider if we were in the middle of evaluating a "not" or "if").
271             # Therefore this is only appropriate during the evaluation phase, not the traverse phase.
272 55     55 0 297 sub abort ($state, $error_string, @args) {
  55         109  
  55         120  
  55         123  
  55         92  
273 55         783 ()= E({ %$state, exception => 1 }, $error_string, @args);
274 55 50       416 croak 'abort() called during traverse' if $state->{traverse};
275 55         719 die pop $state->{errors}->@*;
276             }
277              
278 0     0 0 0 sub assert_keyword_exists ($state, $schema) {
  0         0  
  0         0  
  0         0  
279 0 0       0 croak 'assert_keyword_exists called in void context' if not defined wantarray;
280 0 0       0 return E($state, '%s keyword is required', $state->{keyword}) if not exists $schema->{$state->{keyword}};
281 0         0 return 1;
282             }
283              
284 39919     39919 0 60223 sub assert_keyword_type ($state, $schema, $type) {
  39919         60484  
  39919         56461  
  39919         62358  
  39919         55024  
285 39919 50       87667 croak 'assert_keyword_type called in void context' if not defined wantarray;
286 39919         84132 my $value = $schema->{$state->{keyword}};
287 39919         60714 my $thing = 'value';
288             ($value, $thing) = is_plain_hashref($value) ? ($value->{$state->{_schema_path_suffix}}, 'value at "'.$state->{_schema_path_suffix}.'"')
289             : is_plain_arrayref($value) ? ($value->[$state->{_schema_path_suffix}], 'item '.$state->{_schema_path_suffix})
290             : die 'unknown type'
291 39919 0       87633 if exists $state->{_schema_path_suffix};
    50          
    100          
292 39919 100       85692 return 1 if is_type($type, $value);
293 18 100       143 E($state, '%s %s is not a%s %s', $state->{keyword}, $thing, ($type =~ /^[aeiou]/ ? 'n' : ''), $type);
294             }
295              
296 2289     2289 0 4390 sub assert_pattern ($state, $pattern) {
  2289         3789  
  2289         3823  
  2289         3528  
297 2289 50       5614 croak 'assert_pattern called in void context' if not defined wantarray;
298             try {
299 0     0   0 local $SIG{__WARN__} = sub { die @_ };
300             qr/$pattern/;
301             }
302 2289         5230 catch ($e) { return E($state, $e); };
303 2286         21433 return 1;
304             }
305              
306             # this is only suitable for checking URIs within schemas themselves
307 4596     4596 0 8066 sub assert_uri_reference ($state, $schema) {
  4596         6954  
  4596         6810  
  4596         6812  
308 4596 50       9898 croak 'assert_uri_reference called in void context' if not defined wantarray;
309              
310 4596         10107 my $string = $schema->{$state->{keyword}};
311             return E($state, '%s value is not a valid URI reference', $state->{keyword})
312             # see also uri-reference format sub
313 4596 100 66     16450 if fc(Mojo::URL->new($string)->to_unsafe_string) ne fc($string)
      100        
      100        
      100        
      100        
314             or $string =~ /[^[:ascii:]]/
315             or $string =~ /#/
316             and $string !~ m{#$} # empty fragment
317             and $string !~ m{#[A-Za-z][A-Za-z0-9_:.-]*$} # plain-name fragment
318             and $string !~ m{#/(?:[^~]|~[01])*$}; # json pointer fragment
319              
320 4566         1637998 return 1;
321             }
322              
323             # this is only suitable for checking URIs within schemas themselves
324 5823     5823 0 10956 sub assert_uri ($state, $schema, $override = undef) {
  5823         9897  
  5823         8862  
  5823         10213  
  5823         8537  
325 5823 50       12249 croak 'assert_uri called in void context' if not defined wantarray;
326              
327 5823   66     20409 my $string = $override // $schema->{$state->{keyword}};
328 5823         19428 my $uri = Mojo::URL->new($string);
329              
330 5823 50 66     510028 return E($state, '"%s" is not a valid URI', $string)
      100        
      66        
      33        
      33        
      66        
331             # see also uri format sub
332             if fc($uri->to_unsafe_string) ne fc($string)
333             or $string =~ /[^[:ascii:]]/
334             or not $uri->is_abs
335             or $string =~ /#/
336             and $string !~ m{#$} # empty fragment
337             and $string !~ m{#[A-Za-z][A-Za-z0-9_:.-]*$} # plain-name fragment
338             and $string !~ m{#/(?:[^~]|~[01])*$}; # json pointer fragment
339              
340 5815         1203659 return 1;
341             }
342              
343             # produces an annotation whose value is the same as that of the current keyword
344 1595     1595 0 2467 sub annotate_self ($state, $schema) {
  1595         2446  
  1595         2460  
  1595         2499  
345             A($state, is_ref($schema->{$state->{keyword}}) ? dclone($schema->{$state->{keyword}})
346 1595 100       11107 : $schema->{$state->{keyword}});
347             }
348              
349 1101     1101 0 2164 sub sprintf_num ($value) {
  1101         2055  
  1101         1820  
350             # use original value as stored in the NV, without losing precision
351 1101 100       6922 ref($value) =~ /^Math::Big(?:Int|Float)$/ ? $value->bstr : sprintf('%s', $value);
352             }
353              
354             1;
355              
356             __END__
357              
358             =pod
359              
360             =encoding UTF-8
361              
362             =head1 NAME
363              
364             JSON::Schema::Modern::Utilities - Internal utilities for JSON::Schema::Modern
365              
366             =head1 VERSION
367              
368             version 0.571
369              
370             =head1 SYNOPSIS
371              
372             use JSON::Schema::Modern::Utilities qw(func1 func2..);
373              
374             =head1 DESCRIPTION
375              
376             This class contains internal utilities to be used by L<JSON::Schema::Modern>.
377              
378             =for Pod::Coverage is_type get_type is_equal is_elements_unique jsonp unjsonp local_annotations
379             canonical_uri E A abort assert_keyword_exists assert_keyword_type assert_pattern assert_uri_reference assert_uri
380             annotate_self sprintf_num
381              
382             =for stopwords OpenAPI
383              
384             =head1 SUPPORT
385              
386             Bugs may be submitted through L<https://github.com/karenetheridge/JSON-Schema-Modern/issues>.
387              
388             I am also usually active on irc, as 'ether' at C<irc.perl.org> and C<irc.libera.chat>.
389              
390             You can also find me on the L<JSON Schema Slack server|https://json-schema.slack.com> and L<OpenAPI Slack
391             server|https://open-api.slack.com>, which are also great resources for finding help.
392              
393             =head1 AUTHOR
394              
395             Karen Etheridge <ether@cpan.org>
396              
397             =head1 COPYRIGHT AND LICENCE
398              
399             This software is copyright (c) 2020 by Karen Etheridge.
400              
401             This is free software; you can redistribute it and/or modify it under
402             the same terms as the Perl 5 programming language system itself.
403              
404             =cut