File Coverage

blib/lib/JSON/Schema/Modern/Result.pm
Criterion Covered Total %
statement 143 153 93.4
branch 43 60 71.6
condition 24 42 57.1
subroutine 40 45 88.8
pod 7 12 58.3
total 257 312 82.3


line stmt bran cond sub pod time code
1 45     45   336 use strict;
  45         117  
  45         2142  
2 45     45   273 use warnings;
  45         103  
  45         4199  
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.632';
8              
9 45     45   2095 use 5.020;
  45         753  
10 45     45   483 use Moo;
  45         114  
  45         445  
11 45     45   20599 use strictures 2;
  45         560  
  45         2343  
12 45     45   24188 use stable 0.031 'postderef';
  45         1220  
  45         401  
13 45     45   10979 use experimental 'signatures';
  45         132  
  45         275  
14 45     45   3329 no autovivification warn => qw(fetch store exists delete);
  45         115  
  45         434  
15 45     45   4324 use if "$]" >= 5.022, experimental => 're_strict';
  45         143  
  45         3568  
16 45     45   4684 no if "$]" >= 5.031009, feature => 'indirect';
  45         122  
  45         3879  
17 45     45   285 no if "$]" >= 5.033001, feature => 'multidimensional';
  45         137  
  45         3371  
18 45     45   341 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  45         128  
  45         3294  
19 45     45   311 no if "$]" >= 5.041009, feature => 'smartmatch';
  45         96  
  45         2500  
20 45     45   280 no feature 'switch';
  45         118  
  45         1798  
21 45     45   269 use MooX::TypeTiny;
  45         166  
  45         531  
22 45     45   45377 use Types::Standard qw(ArrayRef InstanceOf Enum Bool Str Maybe Tuple Map Any);
  45         103  
  45         560  
23 45     45   148485 use Types::Common::Numeric 'PositiveInt';
  45         110  
  45         391  
24 45     45   71396 use JSON::Schema::Modern::Annotation;
  45         884  
  45         2395  
25 45     45   402 use JSON::Schema::Modern::Error;
  45         98  
  45         1604  
26 45     45   276 use JSON::Schema::Modern::Utilities qw(true false json_pointer_type);
  45         124  
  45         3490  
27 45     45   301 use JSON::PP ();
  45         99  
  45         1452  
28 45     45   288 use List::Util 1.45 'uniq';
  45         1022  
  45         4985  
29 45     45   326 use if "$]" < 5.041010, 'List::Util' => qw(any all);
  45         100  
  45         2616  
30 45     45   316 use if "$]" >= 5.041010, experimental => qw(keyword_any keyword_all);
  45         104  
  45         1023  
31 45     45   5738 use Carp 'croak';
  45         105  
  45         2698  
32 45     45   284 use builtin::compat qw(refaddr blessed);
  45         94  
  45         572  
33 45     45   8626 use Safe::Isa;
  45         102  
  45         8022  
34 45     45   347 use namespace::clean;
  45         126  
  45         770  
35              
36             use overload
37             'bool' => sub {
38 0     0   0 croak 'boolean overload is deprecated and could be removed anytime after 2026-02-01';
39 0         0 $_[0]->valid;
40             },
41             '&' => \&combine,
42 0     0   0 '""' => sub { $_[0]->stringify },
43 45     45   31577 fallback => 1;
  45         113  
  45         866  
44              
45             has valid => (
46             is => 'ro',
47             isa => Bool|InstanceOf('JSON::PP::true')|InstanceOf('JSON::PP::false'),
48             coerce => sub { $_[0] ? true : false }, # might be JSON::PP::* or builtin::* booleans
49             lazy => 1,
50             default => sub ($self) { $self->error_count == 0 },
51             );
52 0     0 0 0 sub result { goto \&valid } # backcompat only
53              
54             has exception => (
55             is => 'ro',
56             isa => Bool,
57             lazy => 1,
58             default => sub ($self) { any { $_->exception or $_->error =~ /^EXCEPTION: / } $self->errors },
59             );
60              
61             # has errors
62             # has annotations
63             # turn hashrefs in _errors or _annotations into blessed objects
64             has $_.'s' => (
65             is => 'bare',
66             reader => '__'.$_.'s',
67             isa => ArrayRef[InstanceOf['JSON::Schema::Modern::'.ucfirst]],
68             lazy => 1,
69             default => do {
70             my $type = $_;
71             sub ($self) {
72             return [] if not (($self->{'_'.$type.'s'}//[])->@*);
73              
74             # E() and A() in ::Utilities returns an unblessed hashref, which is used to create a real object
75             # by its BUILDARGS sub
76             return [ map +(('JSON::Schema::Modern::'.ucfirst($type))->new($_)), $self->{'_'.$type.'s'}->@* ];
77             };
78             },
79             ) foreach qw(error annotation);
80              
81 28346     28346 1 1564060 sub errors { $_[0]->__errors->@* }
82 6 100 50 6 0 867 sub error_count { scalar(($_[0]->{errors}//[])->@*) || scalar(($_[0]->{_errors}//[])->@*) }
      50        
83 40     40 1 2720 sub annotations { $_[0]->__annotations->@* }
84 14328 100 100 14328 0 1133842 sub annotation_count { scalar(($_[0]->{annotations}//[])->@*) || scalar(($_[0]->{_annotations}//[])->@*) }
      100        
85              
86             has recommended_response => (
87             is => 'rw',
88             isa => Maybe[Tuple[PositiveInt, Str]],
89             lazy => 1,
90             default => sub ($self) {
91             return if not $self->errors;
92              
93             for my $error ($self->errors) {
94             my $pe = $error->recommended_response;
95             return $pe if $pe;
96             }
97              
98             return [ 500, 'Internal Server Error' ] if $self->exception;
99             return [ 400, ($self->errors)[0]->stringify ];
100             },
101             );
102              
103             # strict_basic can only be used with draft2019-09.
104 45     45   54725 use constant OUTPUT_FORMATS => [qw(flag basic strict_basic terse data_only)];
  45         114  
  45         124375  
105              
106             has output_format => (
107             is => 'rw',
108             isa => Enum(OUTPUT_FORMATS),
109             default => 'basic',
110             );
111              
112             has formatted_annotations => (
113             is => 'ro',
114             isa => Bool,
115             default => 1,
116             );
117              
118             has defaults => (
119             is => 'ro',
120             isa => Map[json_pointer_type, Any],
121             );
122              
123             around BUILDARGS => sub ($orig, $class, @args) {
124             my $args = $class->$orig(@args);
125              
126             # set unblessed hashrefs aside, and defer creation of blessed objects until needed
127             $args->{_errors} = delete $args->{errors} if
128             exists $args->{errors} and any { !blessed($_) } $args->{errors}->@*;
129             $args->{_annotations} = delete $args->{annotations} if
130             exists $args->{annotations} and any { !blessed($_) } $args->{annotations}->@*;
131              
132             croak 'inconsistent inputs: errors is not empty but valid is true'
133             if $args->{valid}
134             and (exists $args->{_errors} and $args->{_errors}->@*
135             or exists $args->{errors} and $args->{errors}->@*);
136              
137             croak 'inconsistent inputs: errors is empty but valid is false'
138             if exists $args->{valid} and not $args->{valid}
139             and (not exists $args->{_errors} or not $args->{_errors}->@*)
140             and (not exists $args->{errors} or not $args->{errors}->@*);
141              
142             return $args;
143             };
144              
145 17399     17399 0 390976 sub BUILD ($self, $args) {
  17399         35152  
  17399         35192  
  17399         32626  
146 17399 50       84721 $self->{_errors} = $args->{_errors} if exists $args->{_errors};
147 17399 100       477214 $self->{_annotations} = $args->{_annotations} if exists $args->{_annotations};
148             }
149              
150 25754     25754 1 178655 sub format ($self, $style, $formatted_annotations = undef) {
  25754         47505  
  25754         51711  
  25754         52505  
  25754         42589  
151 25754   33     177848 $formatted_annotations //= $self->formatted_annotations;
152              
153 25754 100       106811 if ($style eq 'flag') {
    100          
    100          
    100          
    50          
154 1 50       27 return +{ valid => $self->valid ? true : false };
155             }
156             elsif ($style eq 'basic') {
157             return +{
158 25748 100 100     557775 valid => $self->valid ? true : false,
    100 100        
    100          
    100          
159             $self->valid
160             ? ($formatted_annotations && $self->annotation_count ? (annotations => [ map $_->TO_JSON, $self->annotations ]) : ())
161             : (errors => [ map $_->TO_JSON, $self->errors ]),
162             $self->valid && $self->defaults ? (defaults => $self->defaults) : (),
163             };
164             }
165             # note: strict_basic will NOT be supported after draft 2019-09!
166             elsif ($style eq 'strict_basic') {
167             return +{
168 1 50 0     30 valid => ($self->valid ? true : false),
    0          
    50          
169             $self->valid
170             ? ($formatted_annotations && $self->annotation_count ? (annotations => [ map _map_uris($_->TO_JSON), $self->annotations ]) : ())
171             : (errors => [ map _map_uris($_->TO_JSON), $self->errors ]),
172             };
173             }
174             elsif ($style eq 'terse') {
175 2         8 my (%instance_locations, %keyword_locations);
176              
177             my @errors = grep {
178 2         10 my ($keyword, $error) = ($_->keyword, $_->error);
  29         127  
179              
180 29   66     479 my $keep = 0+!!(
181             not $keyword
182             or (
183             not grep $keyword eq $_, qw(allOf anyOf if then else dependentSchemas contains propertyNames)
184             and ($keyword ne 'oneOf' or $error ne 'no subschemas are valid')
185             and ($keyword ne 'prefixItems' or $error eq 'item not permitted')
186             and ($keyword ne 'items' or $error eq 'item not permitted' or $error eq 'additional item not permitted')
187             and ($keyword ne 'additionalItems' or $error eq 'additional item not permitted')
188             and (not grep $keyword eq $_, qw(properties patternProperties)
189             or $error eq 'property not permitted')
190             and ($keyword ne 'additionalProperties' or $error eq 'additional property not permitted'))
191             and ($keyword ne 'dependentRequired' or $error ne 'not all dependencies are satisfied')
192             );
193              
194 29 100       121 ++$instance_locations{$_->instance_location} if $keep;
195 29 100       87 ++$keyword_locations{$_->keyword_location} if $keep;
196              
197 29         61 $keep;
198             }
199             $self->errors;
200              
201 2 50 33     69 die 'uh oh, have no errors left to report' if not $self->valid and not @errors;
202              
203             return +{
204 2 50 0     95 valid => $self->valid ? true : false,
    0          
    50          
205             $self->valid
206             ? ($formatted_annotations && $self->annotation_count ? (annotations => [ map $_->TO_JSON, $self->annotations ]) : ())
207             : (errors => [ map $_->TO_JSON, @errors ]),
208             };
209             }
210             elsif ($style eq 'data_only') {
211 2 50       8 return 'valid' if not $self->error_count;
212             # Note: this output is going to be confusing when coming from a schema with a 'oneOf', 'not',
213             # etc. Perhaps generating the strings with indentation levels, as derived from a nested format,
214             # might be more readable.
215 2         8 return join("\n", uniq(map $_->stringify, $self->errors));
216             }
217              
218             # TODO: support detailed, verbose ?
219 0         0 croak 'unsupported output format';
220             }
221              
222 0 0   0 1 0 sub count { $_[0]->valid ? $_[0]->annotation_count : $_[0]->error_count }
223              
224 8     8 1 6032 sub combine ($self, $other, $swap) {
  8         20  
  8         16  
  8         16  
  8         15  
225 8 100       56 croak 'wrong type for & operation' if not $other->$_isa(__PACKAGE__);
226              
227 7 100       139 return $self if refaddr($other) == refaddr($self);
228              
229 6 100 100     173 return ref($self)->new(
      33        
      66        
      50        
      50        
230             valid => $self->valid && $other->valid,
231             annotations => [
232             $self->annotations,
233             $other->annotations,
234             ],
235             errors => [
236             $self->errors,
237             $other->errors,
238             ],
239             output_format => $self->output_format,
240             formatted_annotations => $self->formatted_annotations || $other->formatted_annotations,
241             $self->defaults || $other->defaults
242             ? (defaults => +{ ($self->defaults//{})->%*, ($other->defaults//{})->%* })
243             : (),
244             );
245             }
246              
247 0     0 0 0 sub stringify ($self) {
  0         0  
  0         0  
248 0         0 return $self->format('data_only');
249             }
250              
251 25752     25752 1 422128 sub TO_JSON ($self) {
  25752         52501  
  25752         43463  
252 25752 50       696491 croak 'cannot produce JSON output for data_only format' if $self->output_format eq 'data_only';
253 25752         675289 $self->format($self->output_format);
254             }
255              
256 16878     16878 1 130739 sub dump ($self) {
  16878         37744  
  16878         32929  
257 16878         239946 my $encoder = JSON::Schema::Modern::_JSON_BACKEND()->new
258             ->utf8(0)
259             ->convert_blessed(1)
260             ->canonical(1)
261             ->indent(1)
262             ->space_after(1);
263 16878 50       133251 $encoder->indent_length(2) if $encoder->can('indent_length');
264 16878         101963 $encoder->encode($self);
265             }
266              
267             # turns the JSON pointers in instance_location, keyword_location into a URI fragments,
268             # for strict draft-201909 adherence
269 8     8   1586 sub _map_uris ($data) {
  8         17  
  8         12  
270             return +{
271             %$data,
272 8         34 map +($_ => Mojo::URL->new->fragment($data->{$_})->to_string),
273             qw(instanceLocation keywordLocation),
274             };
275             }
276              
277             1;
278              
279             __END__