File Coverage

blib/lib/JSON/Schema/Modern/Result.pm
Criterion Covered Total %
statement 118 127 92.9
branch 28 42 66.6
condition 20 35 57.1
subroutine 33 38 86.8
pod 7 12 58.3
total 206 254 81.1


line stmt bran cond sub pod time code
1 34     34   832 use strict;
  34         86  
  34         1262  
2 34     34   214 use warnings;
  34         87  
  34         1906  
3             package JSON::Schema::Modern::Result;
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Contains the result of a JSON Schema evaluation
6              
7             our $VERSION = '0.572';
8              
9 34     34   760 use 5.020;
  34         137  
10 34     34   200 use Moo;
  34         77  
  34         238  
11 34     34   13797 use strictures 2;
  34         367  
  34         1527  
12 34     34   7890 use stable 0.031 'postderef';
  34         615  
  34         287  
13 34     34   6363 use experimental 'signatures';
  34         90  
  34         150  
14 34     34   3210 use if "$]" >= 5.022, experimental => 're_strict';
  34         109  
  34         397  
15 34     34   4018 no if "$]" >= 5.031009, feature => 'indirect';
  34         82  
  34         278  
16 34     34   1766 no if "$]" >= 5.033001, feature => 'multidimensional';
  34         87  
  34         208  
17 34     34   1595 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  34         156  
  34         220  
18 34     34   1395 use MooX::TypeTiny;
  34         92  
  34         322  
19 34     34   28994 use Types::Standard qw(ArrayRef InstanceOf Enum Bool);
  34         92  
  34         357  
20 34     34   84630 use JSON::Schema::Modern::Annotation;
  34         102  
  34         1161  
21 34     34   279 use JSON::Schema::Modern::Error;
  34         85  
  34         763  
22 34     34   169 use JSON::PP ();
  34         73  
  34         867  
23 34     34   184 use List::Util 1.50 qw(any uniq all);
  34         852  
  34         2748  
24 34     34   227 use Scalar::Util qw(refaddr blessed);
  34         72  
  34         1688  
25 34     34   210 use Safe::Isa;
  34         75  
  34         3548  
26 34     34   233 use namespace::clean;
  34         109  
  34         198  
27              
28             use overload
29 26065     26065   1306922 'bool' => sub { $_[0]->valid },
30             '&' => \&combine,
31 0     0   0 '0+' => sub { Scalar::Util::refaddr($_[0]) },
32 0     0   0 '""' => sub { $_[0]->stringify },
33 34     34   16423 fallback => 1;
  34         96  
  34         693  
34              
35             has valid => (
36             is => 'ro',
37             isa => InstanceOf['JSON::PP::Boolean'],
38             coerce => sub { $_[0] ? JSON::PP::true : JSON::PP::false },
39             );
40 0     0 0 0 sub result { goto \&valid } # backcompat only
41              
42             has exception => (
43             is => 'ro',
44             isa => InstanceOf['JSON::PP::Boolean'],
45             coerce => sub { $_[0] ? JSON::PP::true : JSON::PP::false },
46             lazy => 1,
47             default => sub { any { $_->exception } $_[0]->errors },
48             );
49              
50             has $_.'s' => (
51             is => 'bare',
52             isa => ArrayRef[InstanceOf['JSON::Schema::Modern::'.ucfirst]],
53             lazy => 1,
54             default => sub { [] },
55             coerce => do {
56             my $type = $_;
57             sub ($arrayref) {
58             return $arrayref if all { blessed $_ } $arrayref->@*;
59             return [ map +(('JSON::Schema::Modern::'.ucfirst $type)->new($_)), $arrayref->@* ];
60             },
61             },
62             ) foreach qw(error annotation);
63              
64 18968   100 18968 1 217995 sub errors { ($_[0]->{errors}//[])->@* }
65 5964   50 5964 0 155700 sub error_count { scalar ($_[0]->{errors}//[])->@* }
66 24   100 24 1 1994 sub annotations { ($_[0]->{annotations}//[])->@* }
67 7418   100 7418 0 177304 sub annotation_count { scalar ($_[0]->{annotations}//[])->@* }
68              
69             # strict_basic can only be used with draft2019-09.
70 34     34   24716 use constant OUTPUT_FORMATS => [qw(flag basic strict_basic detailed verbose terse data_only)];
  34         89  
  34         39354  
71              
72             has output_format => (
73             is => 'rw',
74             isa => Enum(OUTPUT_FORMATS),
75             default => 'basic',
76             );
77              
78             has formatted_annotations => (
79             is => 'ro',
80             isa => Bool,
81             default => 1,
82             );
83              
84 13341     13341 0 430164 sub BUILD ($self, $) {
  13341         22307  
  13341         19970  
85 13341 50 66     60480 warn 'result is false but there are no errors' if not $self->valid and not $self->error_count;
86             }
87              
88 13390     13390 1 85092 sub format ($self, $style, $formatted_annotations = undef) {
  13390         22302  
  13390         22725  
  13390         23482  
  13390         20248  
89 13390   66     76856 $formatted_annotations //= $self->formatted_annotations;
90              
91 13390 100       41732 if ($style eq 'flag') {
    100          
    100          
    100          
    50          
92 1         10 return +{ valid => $self->valid };
93             }
94             elsif ($style eq 'basic') {
95             return +{
96 13384 100 100     60412 valid => $self->valid,
    100          
97             $self->valid
98             ? ($formatted_annotations && $self->annotation_count ? (annotations => [ map $_->TO_JSON, $self->annotations ]) : ())
99             : (errors => [ map $_->TO_JSON, $self->errors ]),
100             };
101             }
102             # note: strict_basic will NOT be supported after draft 2019-09!
103             elsif ($style eq 'strict_basic') {
104             return +{
105 1 0 0     7 valid => $self->valid,
    50          
106             $self->valid
107             ? ($formatted_annotations && $self->annotation_count ? (annotations => [ map _map_uris($_->TO_JSON), $self->annotations ]) : ())
108             : (errors => [ map _map_uris($_->TO_JSON), $self->errors ]),
109             };
110             }
111             elsif ($style eq 'terse') {
112 2         6 my (%instance_locations, %keyword_locations);
113              
114             my @errors = grep {
115 2         7 my ($keyword, $error) = ($_->keyword, $_->error);
  29         71  
116              
117 29   66     333 my $keep = 0+!!(
118             not $keyword
119             or (
120             not grep $keyword eq $_, qw(allOf anyOf if then else dependentSchemas contains propertyNames)
121             and ($keyword ne 'oneOf' or $error ne 'no subschemas are valid')
122             and ($keyword ne 'prefixItems' or $error eq 'item not permitted')
123             and ($keyword ne 'items' or $error eq 'item not permitted' or $error eq 'additional item not permitted')
124             and ($keyword ne 'additionalItems' or $error eq 'additional item not permitted')
125             and (not grep $keyword eq $_, qw(properties patternProperties)
126             or $error eq 'property not permitted')
127             and ($keyword ne 'additionalProperties' or $error eq 'additional property not permitted'))
128             and ($keyword ne 'dependentRequired' or $error ne 'not all dependencies are satisfied')
129             );
130              
131 29 100       68 ++$instance_locations{$_->instance_location} if $keep;
132 29 100       70 ++$keyword_locations{$_->keyword_location} if $keep;
133              
134 29         48 $keep;
135             }
136             $self->errors;
137              
138 2 50 33     27 die 'uh oh, have no errors left to report' if not $self->valid and not @errors;
139              
140             return +{
141 2 0 0     32 valid => $self->valid,
    50          
142             $self->valid
143             ? ($formatted_annotations && $self->annotation_count ? (annotations => [ map $_->TO_JSON, $self->annotations ]) : ())
144             : (errors => [ map $_->TO_JSON, @errors ]),
145             };
146             }
147             elsif ($style eq 'data_only') {
148 2 50       6 return 'valid' if not $self->error_count;
149             # Note: this output is going to be confusing when coming from a schema with a 'oneOf', 'not',
150             # etc. Perhaps generating the strings with indentation levels, as derived from a nested format,
151             # might be more readable.
152 2         7 return join("\n", uniq(map $_->stringify, $self->errors));
153             }
154              
155 0         0 die 'unsupported output format';
156             }
157              
158 0 0   0 1 0 sub count { $_[0]->valid ? $_[0]->annotation_count : $_[0]->error_count }
159              
160 5     5 1 6107 sub combine ($self, $other, $swap) {
  5         12  
  5         8  
  5         9  
  5         7  
161 5 100       17 die 'wrong type for & operation' if not $other->$_isa(__PACKAGE__);
162              
163 4 100       96 return $self if refaddr($other) == refaddr($self);
164              
165 3   66     17 return ref($self)->new(
      33        
166             valid => $self->valid && $other->valid,
167             annotations => [
168             $self->annotations,
169             $other->annotations,
170             ],
171             errors => [
172             $self->errors,
173             $other->errors,
174             ],
175             output_format => $self->output_format,
176             formatted_annotations => $self->formatted_annotations || $other->formatted_annotations,
177             );
178             }
179              
180              
181 0     0 0 0 sub stringify ($self) {
  0         0  
  0         0  
182 0         0 return $self->format('data_only');
183             }
184              
185 13388     13388 1 83697 sub TO_JSON ($self) {
  13388         22143  
  13388         20347  
186 13388 50       295216 die 'cannot produce JSON output for data_only format' if $self->output_format eq 'data_only';
187 13388         293133 $self->format($self->output_format);
188             }
189              
190 12980     12980 1 124003 sub dump ($self) {
  12980         21939  
  12980         20229  
191 12980         56836 my $encoder = JSON::MaybeXS->new(utf8 => 0, convert_blessed => 1, canonical => 1, indent => 1, space_after => 1);
192 12980 50       317291 $encoder->indent_length(2) if $encoder->can('indent_length');
193 12980         56577 $encoder->encode($self);
194             }
195              
196             # turns the JSON pointers in instance_location, keyword_location into a URI fragments,
197             # for strict draft-201909 adherence
198 8     8   1604 sub _map_uris ($data) {
  8         14  
  8         14  
199             return +{
200             %$data,
201 8         40 map +($_ => Mojo::URL->new->fragment($data->{$_})->to_string),
202             qw(instanceLocation keywordLocation),
203             };
204             }
205              
206             1;
207              
208             __END__
209              
210             =pod
211              
212             =encoding UTF-8
213              
214             =head1 NAME
215              
216             JSON::Schema::Modern::Result - Contains the result of a JSON Schema evaluation
217              
218             =head1 VERSION
219              
220             version 0.572
221              
222             =head1 SYNOPSIS
223              
224             use JSON::Schema::Modern;
225             my $js = JSON::Schema::Modern->new;
226             my $result = $js->evaluate($data, $schema);
227             my @errors = $result->errors;
228              
229             my $result_data_encoded = encode_json($result); # calls TO_JSON
230              
231             # use in numeric and boolean context
232             say sprintf('got %d %ss', $result, ($result ? 'annotation' : 'error'));
233              
234             # use in string context
235             say 'full results: ', $result;
236              
237             # combine two results into one:
238             my $overall_result = $result1 & $result2;
239              
240             =head1 DESCRIPTION
241              
242             This object holds the complete results of evaluating a data payload against a JSON Schema using
243             L<JSON::Schema::Modern>.
244              
245             =head1 OVERLOADS
246              
247             The object contains a I<boolean> overload, which evaluates to the value of L</valid>, so you can
248             use the result of L<JSON::Schema::Modern/evaluate> in boolean context.
249              
250             =for stopwords iff
251              
252             The object also contains a I<bitwise AND> overload (C<&>), for combining two results into one (the
253             result is valid iff both inputs are valid; annotations and errors from the second argument are
254             appended to those of the first in a new Result object).
255              
256             =head1 ATTRIBUTES
257              
258             =head2 valid
259              
260             A boolean. Indicates whether validation was successful or failed.
261              
262             =head2 errors
263              
264             Returns an array of L<JSON::Schema::Modern::Error> objects.
265              
266             =head2 annotations
267              
268             Returns an array of L<JSON::Schema::Modern::Annotation> objects.
269              
270             =head2 output_format
271              
272             =for stopwords subschemas
273              
274             One of: C<flag>, C<basic>, C<strict_basic>, C<detailed>, C<verbose>, C<terse>, C<data_only>. Defaults to C<basic>.
275              
276             =over 4
277              
278             =item *
279              
280             C<flag> returns just the result of the evaluation: either C<{"valid": true}> or C<{"valid": false}>.
281              
282             =item *
283              
284             C<basic> adds the list of C<errors> or C<annotations> to the boolean evaluation result.
285              
286             C<instance_location> and C<keyword_location> are always included, as JSON pointers, describing the
287             path to the evaluation location; C<absolute_keyword_location> is added (as a resolved URI) whenever
288             it is known and different from C<keyword_location>.
289              
290             =item *
291              
292             C<strict_basic> is like C<basic> but follows the draft-2019-09 specification precisely, including
293              
294             replicating an error fixed in the next draft, in that C<instance_location> and C<keyword_location>
295             values are provided as fragment-only URI references rather than JSON pointers.
296              
297             =item *
298              
299             C<terse> is not described in any specification; it is like C<basic>, but omits some redundant
300              
301             errors (for example the one for the C<allOf> keyword that is added when any of the subschemas under
302             C<allOf> failed evaluation).
303              
304             =item *
305              
306             C<data_only> returns a string, not a data structure: it contains a list of errors identified only
307              
308             by their C<instance_location> and error message (or C<keyword_location>, when the error occurred
309             while loading the schema itself). This format is suitable for generating errors when the schema is
310             not published, or for describing errors with the schema itself. This is not an official
311             specification format and may change slightly over time, as it is tested in production environments.
312              
313             =back
314              
315             =head2 formatted_annotations
316              
317             A boolean flag indicating whether L</format> should include annotations in the output. Defaults to true.
318              
319             =head1 METHODS
320              
321             =for Pod::Coverage BUILD OUTPUT_FORMATS result stringify annotation_count error_count
322              
323             =head2 format
324              
325             Returns a data structure suitable for serialization; requires one argument specifying the output
326             format to use, which corresponds to the formats documented in
327             L<https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.10.4>. The only supported
328             formats at this time are C<flag>, C<basic>, C<strict_basic>, and C<terse>.
329              
330             =head2 TO_JSON
331              
332             Calls L</format> with the style configured in L</output_format>.
333              
334             =head2 count
335              
336             Returns the number of annotations when the result is true, or the number of errors when the result
337             is false.
338              
339             =head2 combine
340              
341             When provided with another result object, returns a new object with the combination of all results.
342             See C<&> at L</OVERLOADS>.
343              
344             =head2 dump
345              
346             Returns a JSON string representing the result object, using the requested L</format>, according to
347             the L<specification|https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.10>.
348              
349             =head1 SERIALIZATION
350              
351             Results (and their contained errors and annotations) can be serialized in a number of ways.
352              
353             Results have defined L</output_format>s, which can be generated as nested unblessed hashes/arrays
354             and are suitable for serializing using a JSON encoder for use in another application. A JSON string of
355             the result can be obtained directly using L</dump>.
356              
357             If it is preferable to omit direct references to the schema (for example in an application where the
358             schema is not published), but still convey some semantic information about the nature of the errors,
359             stringify the object directly. This also means that result objects can be thrown as exceptions, or
360             embedded in error messages.
361              
362             If you are embedding the full result inside another data structure, perhaps to be serialized to JSON
363             (or another format) later on, use L</TO_JSON> or L</format>.
364              
365             =for stopwords OpenAPI
366              
367             =head1 SUPPORT
368              
369             Bugs may be submitted through L<https://github.com/karenetheridge/JSON-Schema-Modern/issues>.
370              
371             I am also usually active on irc, as 'ether' at C<irc.perl.org> and C<irc.libera.chat>.
372              
373             You can also find me on the L<JSON Schema Slack server|https://json-schema.slack.com> and L<OpenAPI Slack
374             server|https://open-api.slack.com>, which are also great resources for finding help.
375              
376             =head1 AUTHOR
377              
378             Karen Etheridge <ether@cpan.org>
379              
380             =head1 COPYRIGHT AND LICENCE
381              
382             This software is copyright (c) 2020 by Karen Etheridge.
383              
384             This is free software; you can redistribute it and/or modify it under
385             the same terms as the Perl 5 programming language system itself.
386              
387             =cut