File Coverage

blib/lib/JSON/Schema/Modern/Result.pm
Criterion Covered Total %
statement 117 126 92.8
branch 28 42 66.6
condition 13 27 48.1
subroutine 30 35 85.7
pod 5 8 62.5
total 193 238 81.0


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