File Coverage

blib/lib/JSON/Schema/Draft201909/Result.pm
Criterion Covered Total %
statement 80 82 97.5
branch 22 32 68.7
condition 15 21 71.4
subroutine 25 26 96.1
pod 3 5 60.0
total 145 166 87.3


line stmt bran cond sub pod time code
1 20     20   159 use strict;
  20         45  
  20         723  
2 20     20   119 use warnings;
  20         44  
  20         1186  
3             package JSON::Schema::Draft201909::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.028';
8              
9 20     20   438 use 5.016;
  20         75  
10 20     20   128 no if "$]" >= 5.031009, feature => 'indirect';
  20         99  
  20         238  
11 20     20   1229 no if "$]" >= 5.033001, feature => 'multidimensional';
  20         49  
  20         142  
12 20     20   929 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  20         67  
  20         147  
13 20     20   894 use strictures 2;
  20         279  
  20         872  
14 20     20   4531 use Moo;
  20         45  
  20         159  
15 20     20   8168 use MooX::TypeTiny;
  20         72  
  20         175  
16 20     20   16217 use Types::Standard qw(ArrayRef InstanceOf Enum);
  20         62  
  20         198  
17 20     20   18051 use MooX::HandlesVia;
  20         52  
  20         172  
18 20     20   12781 use JSON::Schema::Draft201909::Annotation;
  20         77  
  20         902  
19 20     20   168 use JSON::Schema::Draft201909::Error;
  20         50  
  20         446  
20 20     20   16981 use JSON::PP ();
  20         299825  
  20         841  
21 20     20   194 use List::Util 1.50 'head';
  20         547  
  20         1770  
22 20     20   220 use namespace::clean;
  20         46  
  20         435  
23              
24             use overload
25 14673     14673   543213 'bool' => sub { $_[0]->valid },
26 1     1   1022 '0+' => sub { $_[0]->count },
27 20     20   7791 fallback => 1;
  20         53  
  20         334  
28              
29             has valid => (
30             is => 'ro',
31             isa => InstanceOf['JSON::PP::Boolean'],
32             coerce => sub { $_[0] ? JSON::PP::true : JSON::PP::false },
33             );
34 0     0 0 0 sub result { goto \&valid } # backcompat only
35              
36             has $_.'s' => (
37             is => 'bare',
38             isa => ArrayRef[InstanceOf['JSON::Schema::Draft201909::'.ucfirst]],
39             lazy => 1,
40             default => sub { [] },
41             handles_via => 'Array',
42             handles => {
43             $_.'s' => 'elements',
44             $_.'_count' => 'count',
45             },
46             ) foreach qw(error annotation);
47              
48 20     20   6094 use constant OUTPUT_FORMATS => [qw(flag basic strict_basic detailed verbose terse)];
  20         49  
  20         19093  
49              
50             has output_format => (
51             is => 'rw',
52             isa => Enum(OUTPUT_FORMATS),
53             default => 'basic',
54             );
55              
56             sub BUILD {
57 4672     4672 0 162431 my $self = shift;
58 4672 50 66     24488 warn 'result is false but there are no errors' if not $self->valid and not $self->error_count;
59             }
60              
61             sub format {
62 7043     7043 1 56476 my ($self, $style) = @_;
63              
64 7043 100       22568 if ($style eq 'flag') {
    100          
    100          
    50          
65 1         13 return +{ valid => $self->valid };
66             }
67             elsif ($style eq 'basic') {
68             return +{
69 7038 100       27863 valid => $self->valid,
    100          
70             $self->valid
71             ? ($self->annotation_count ? (annotations => [ map $_->TO_JSON, $self->annotations ]) : ())
72             : (errors => [ map $_->TO_JSON, $self->errors ]),
73             };
74             }
75             # note: strict_basic will NOT be supported after draft 2019-09!
76             elsif ($style eq 'strict_basic') {
77             return +{
78 1 0       8 valid => $self->valid,
    50          
79             $self->valid
80             ? ($self->annotation_count ? (annotations => [ map _map_uris($_->TO_JSON), $self->annotations ]) : ())
81             : (errors => [ map _map_uris($_->TO_JSON), $self->errors ]),
82             };
83             }
84             elsif ($style eq 'terse') {
85             # we can also drop errors for unevaluatedItems, unevaluatedProperties
86             # when there is another (non-discarded) error at the same instance location or parent keyword
87             # location (indicating that "unevaluated" is actually "unsuccessfully evaluated").
88 3         9 my (%instance_locations, %keyword_locations);
89              
90             my @errors = grep {
91 3         159 my ($keyword, $error) = ($_->keyword, $_->error);
  40         345  
92              
93 40   66     505 my $keep = 0+!!(
94             not $keyword
95             or (
96             not grep $keyword eq $_, qw(allOf anyOf if then else dependentSchemas contains propertyNames)
97             and ($keyword ne 'oneOf' or $error ne 'no subschemas are valid')
98             and ($keyword ne 'items' or $error eq 'item not permitted' )
99             and ($keyword ne 'additionalItems' or $error eq 'additional item not permitted')
100             and (not grep $keyword eq $_, qw(properties patternProperties)
101             or $error eq 'property not permitted')
102             and ($keyword ne 'additionalProperties' or $error eq 'additional property not permitted'))
103             and ($keyword ne 'dependentRequired' or $error ne 'not all dependencies are satisfied')
104             );
105              
106 40 100 66     320 if ($keep and $keyword and $keyword =~ /^unevaluated(?:Items|Properties)$/
      100        
      100        
107             and $error !~ /"$keyword" keyword present, but/) {
108 10         53 my $parent_keyword_location = join('/', head(-1, split('/', $_->keyword_location)));
109 10         43 my $parent_instance_location = join('/', head(-1, split('/', $_->instance_location)));
110              
111             $keep = (
112             (($keyword eq 'unevaluatedProperties' and $error eq 'additional property not permitted')
113             or ($keyword eq 'unevaluatedItems' and $error eq 'additional item not permitted'))
114 10   66     135 and not $instance_locations{$_->instance_location}
115             and not grep m/^$parent_keyword_location/, keys %keyword_locations
116             );
117             }
118              
119 40 100       113 ++$instance_locations{$_->instance_location} if $keep;
120 40 100       117 ++$keyword_locations{$_->keyword_location} if $keep;
121              
122 40         94 $keep;
123             }
124             $self->errors;
125              
126 3 50 33     39 die 'uh oh, have no errors left to report' if not $self->valid and not @errors;
127              
128             return +{
129 3 0       52 valid => $self->valid,
    50          
130             $self->valid
131             ? ($self->annotation_count ? (annotations => [ map $_->TO_JSON, $self->annotations ]) : ())
132             : (errors => [ map $_->TO_JSON, @errors ]),
133             };
134             }
135              
136 0         0 die 'unsupported output format';
137             }
138              
139 1 50   1 1 8 sub count { $_[0]->valid ? $_[0]->annotation_count : $_[0]->error_count }
140              
141             sub TO_JSON {
142 7043     7043 1 309224 my $self = shift;
143 7043         144832 $self->format($self->output_format);
144             }
145              
146             # turns the json pointers in instance_location, keyword_location into a URI fragments,
147             # for strict draft-201909 adherence
148             sub _map_uris {
149 8     8   16 my $data = shift;
150             return +{
151             %$data,
152 8         42 map +($_ => Mojo::URL->new->fragment($data->{$_})->to_string),
153             qw(instanceLocation keywordLocation),
154             };
155             }
156              
157             1;
158              
159             __END__
160              
161             =pod
162              
163             =encoding UTF-8
164              
165             =head1 NAME
166              
167             JSON::Schema::Draft201909::Result - Contains the result of a JSON Schema evaluation
168              
169             =head1 VERSION
170              
171             version 0.028
172              
173             =head1 SYNOPSIS
174              
175             use JSON::Schema::Draft201909;
176             my $js = JSON::Schema::Draft201909->new;
177             my $result = $js->evaluate($data, $schema);
178             my @errors = $result->errors;
179              
180             my $result_data_encoded = encode_json($result); # calls TO_JSON
181              
182             # use in numeric and boolean context
183             say sprintf('got %d %ss', $result, ($result ? 'annotation' : 'error'));
184              
185             # use in string context
186             say 'full results: ', $result;
187              
188             =head1 DESCRIPTION
189              
190             This object holds the complete results of evaluating a data payload against a JSON Schema using
191             L<JSON::Schema::Draft201909>.
192              
193             =head1 OVERLOADS
194              
195             The object contains a boolean overload, which evaluates to the value of L</valid>, so you can
196             use the result of L<JSON::Schema::Draft201909/evaluate> in boolean context.
197              
198             =head1 ATTRIBUTES
199              
200             =head2 valid
201              
202             A boolean. Indicates whether validation was successful or failed.
203              
204             =head2 errors
205              
206             Returns an array of L<JSON::Schema::Draft201909::Error> objects.
207              
208             =head2 annotations
209              
210             Returns an array of L<JSON::Schema::Draft201909::Annotation> objects.
211              
212             =head2 output_format
213              
214             =for stopwords subschemas
215              
216             One of: C<flag>, C<basic>, C<strict_basic>, C<detailed>, C<verbose>, C<terse>. Defaults to C<basic>.
217              
218             =over 4
219              
220             =item *
221              
222             C<flag> returns just the result of the evaluation: either C<{"valid": true}> or C<{"valid": false}>.
223              
224             =item *
225              
226             C<basic> adds the list of C<errors> or C<annotations> to the boolean evaluation result.
227              
228             C<instance_location> and C<keyword_location> are always included, as json pointers, describing the
229             path to the evaluation location; C<absolute_keyword_location> is added (as a resolved URI) whenever
230             it is known and different from C<keyword_location>.
231              
232             =item *
233              
234             C<strict_basic> is like C<basic> but follows the draft-2019-09 specification precisely, including
235              
236             replicating an error fixed in the next draft, in that C<instance_location> and C<keyword_location>
237             values are provided as fragment-only URI references rather than json pointers.
238              
239             =item *
240              
241             C<terse> is not described in any specification; it is like C<basic>, but omits some redundant
242              
243             errors (for example the one for the C<allOf> keyword that is added when any of the subschemas under
244             C<allOf> failed evaluation).
245              
246             =back
247              
248             =head1 METHODS
249              
250             =for Pod::Coverage BUILD OUTPUT_FORMATS result
251              
252             =head2 format
253              
254             Returns a data structure suitable for serialization; requires one argument specifying the output
255             format to use, which corresponds to the formats documented in
256             L<https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.10.4>. The only supported
257             formats at this time are C<flag>, C<basic>, C<strict_basic>, and C<terse>.
258              
259             =head2 TO_JSON
260              
261             Calls L</format> with the style configured in L</output_format>.
262              
263             =head2 count
264              
265             Returns the number of annotations when the result is true, or the number of errors when the result
266             is false.
267              
268             =head1 SUPPORT
269              
270             Bugs may be submitted through L<https://github.com/karenetheridge/JSON-Schema-Draft201909/issues>.
271              
272             I am also usually active on irc, as 'ether' at C<irc.perl.org> and C<irc.libera.chat>.
273              
274             =head1 AUTHOR
275              
276             Karen Etheridge <ether@cpan.org>
277              
278             =head1 COPYRIGHT AND LICENCE
279              
280             This software is copyright (c) 2020 by Karen Etheridge.
281              
282             This is free software; you can redistribute it and/or modify it under
283             the same terms as the Perl 5 programming language system itself.
284              
285             =cut