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   828 use strict;
  34         94  
  34         1280  
2 34     34   229 use warnings;
  34         94  
  34         1858  
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.570';
8              
9 34     34   679 use 5.020;
  34         159  
10 34     34   242 use Moo;
  34         131  
  34         286  
11 34     34   13775 use strictures 2;
  34         504  
  34         1745  
12 34     34   8151 use stable 0.031 'postderef';
  34         730  
  34         567  
13 34     34   6733 use experimental 'signatures';
  34         118  
  34         226  
14 34     34   3738 use if "$]" >= 5.022, experimental => 're_strict';
  34         107  
  34         835  
15 34     34   3606 no if "$]" >= 5.031009, feature => 'indirect';
  34         104  
  34         315  
16 34     34   1898 no if "$]" >= 5.033001, feature => 'multidimensional';
  34         106  
  34         294  
17 34     34   1772 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  34         95  
  34         245  
18 34     34   1472 use MooX::TypeTiny;
  34         115  
  34         361  
19 34     34   30259 use Types::Standard qw(ArrayRef InstanceOf Enum Bool);
  34         110  
  34         396  
20 34     34   73196 use MooX::HandlesVia;
  34         108  
  34         394  
21 34     34   20650 use JSON::Schema::Modern::Annotation;
  34         128  
  34         1133  
22 34     34   249 use JSON::Schema::Modern::Error;
  34         104  
  34         779  
23 34     34   200 use JSON::PP ();
  34         69  
  34         880  
24 34     34   195 use List::Util 1.50 qw(any uniq all);
  34         858  
  34         3267  
25 34     34   286 use Scalar::Util qw(refaddr blessed);
  34         87  
  34         1654  
26 34     34   217 use Safe::Isa;
  34         80  
  34         3357  
27 34     34   248 use namespace::clean;
  34         86  
  34         166  
28              
29             use overload
30 25812     25812   1754014 '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   16473 fallback => 1;
  34         96  
  34         591  
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   20454 use constant OUTPUT_FORMATS => [qw(flag basic strict_basic detailed verbose terse data_only)];
  34         92  
  34         41625  
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 13212     13212 0 412714 sub BUILD ($self, $) {
  13212         22575  
  13212         19114  
86 13212 50 66     57179 warn 'result is false but there are no errors' if not $self->valid and not $self->error_count;
87             }
88              
89 13262     13262 1 81930 sub format ($self, $style, $formatted_annotations = undef) {
  13262         22320  
  13262         20520  
  13262         22531  
  13262         19009  
90 13262   66     71394 $formatted_annotations //= $self->formatted_annotations;
91              
92 13262 100       39907 if ($style eq 'flag') {
    100          
    100          
    100          
    50          
93 1         11 return +{ valid => $self->valid };
94             }
95             elsif ($style eq 'basic') {
96             return +{
97 13256 100 100     59739 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     15 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         4 my (%instance_locations, %keyword_locations);
114              
115             my @errors = grep {
116 2         38 my ($keyword, $error) = ($_->keyword, $_->error);
  29         168  
117              
118 29   66     355 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       77 ++$instance_locations{$_->instance_location} if $keep;
133 29 100       81 ++$keyword_locations{$_->keyword_location} if $keep;
134              
135 29         54 $keep;
136             }
137             $self->errors;
138              
139 2 50 33     17 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       39 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         124 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 6657 sub combine ($self, $other, $swap) {
  5         10  
  5         11  
  5         9  
  5         10  
162 5 100       17 die 'wrong type for & operation' if not $other->$_isa(__PACKAGE__);
163              
164 4 100       140 return $self if refaddr($other) == refaddr($self);
165              
166 3   66     21 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 13260     13260 1 104078 sub TO_JSON ($self) {
  13260         20642  
  13260         18550  
187 13260 50       287619 die 'cannot produce JSON output for data_only format' if $self->output_format eq 'data_only';
188 13260         282312 $self->format($self->output_format);
189             }
190              
191 12854     12854 1 350712 sub dump ($self) {
  12854         22067  
  12854         19323  
192 12854         56230 my $encoder = JSON::MaybeXS->new(utf8 => 0, convert_blessed => 1, canonical => 1, indent => 1, space_after => 1);
193 12854 50       297657 $encoder->indent_length(2) if $encoder->can('indent_length');
194 12854         55513 $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   1587 sub _map_uris ($data) {
  8         17  
  8         9  
200             return +{
201             %$data,
202 8         55 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.570
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