File Coverage

blib/lib/JSON/Schema/Modern/Result.pm
Criterion Covered Total %
statement 145 155 93.5
branch 45 62 72.5
condition 27 45 60.0
subroutine 40 45 88.8
pod 7 12 58.3
total 264 319 82.7


line stmt bran cond sub pod time code
1 45     45   243 use strict;
  45         84  
  45         1544  
2 45     45   1173 use warnings;
  45         339  
  45         3474  
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.637';
8              
9 45     45   732 use 5.020;
  45         169  
10 45     45   167 use Moo;
  45         65  
  45         313  
11 45     45   14965 use strictures 2;
  45         404  
  45         1598  
12 45     45   16894 use stable 0.031 'postderef';
  45         734  
  45         330  
13 45     45   8218 use experimental 'signatures';
  45         116  
  45         126  
14 45     45   2216 no autovivification warn => qw(fetch store exists delete);
  45         77  
  45         352  
15 45     45   3102 use if "$]" >= 5.022, experimental => 're_strict';
  45         98  
  45         1280  
16 45     45   3239 no if "$]" >= 5.031009, feature => 'indirect';
  45         86  
  45         2664  
17 45     45   194 no if "$]" >= 5.033001, feature => 'multidimensional';
  45         77  
  45         2156  
18 45     45   174 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  45         81  
  45         2064  
19 45     45   188 no if "$]" >= 5.041009, feature => 'smartmatch';
  45         67  
  45         1562  
20 45     45   180 no feature 'switch';
  45         100  
  45         1265  
21 45     45   171 use MooX::TypeTiny;
  45         140  
  45         421  
22 45     45   31316 use Types::Standard qw(ArrayRef InstanceOf Enum Bool Str Maybe Tuple Map Any);
  45         77  
  45         408  
23 45     45   94568 use Types::Common::Numeric 'PositiveInt';
  45         80  
  45         290  
24 45     45   49463 use JSON::Schema::Modern::Annotation;
  45         601  
  45         1565  
25 45     45   274 use JSON::Schema::Modern::Error;
  45         70  
  45         1168  
26 45     45   159 use JSON::Schema::Modern::Utilities qw(true false json_pointer_type jsonp_set);
  45         68  
  45         2572  
27 45     45   239 use JSON::PP ();
  45         67  
  45         1049  
28 45     45   173 use List::Util 1.45 'uniq';
  45         757  
  45         3564  
29 45     45   211 use if "$]" < 5.041010, 'List::Util' => qw(any all);
  45         66  
  45         1743  
30 45     45   199 use if "$]" >= 5.041010, experimental => qw(keyword_any keyword_all);
  45         63  
  45         626  
31 45     45   3774 use Carp 'croak';
  45         67  
  45         1935  
32 45     45   206 use builtin::compat qw(refaddr blessed);
  45         61  
  45         402  
33 45     45   6011 use Safe::Isa;
  45         70  
  45         6061  
34 45     45   232 use namespace::clean;
  45         60  
  45         386  
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   22446 fallback => 1;
  45         74  
  45         642  
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 28385     28385 1 1019393 sub errors { $_[0]->__errors->@* }
82 6 100 50 6 0 507 sub error_count { scalar(($_[0]->{errors}//[])->@*) || scalar(($_[0]->{_errors}//[])->@*) }
      50        
83 40     40 1 2161 sub annotations { $_[0]->__annotations->@* }
84 14322 100 100 14322 0 715761 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   37089 use constant OUTPUT_FORMATS => [qw(flag basic strict_basic terse data_only)];
  45         81  
  45         75392  
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             has data => (
124             is => 'ro',
125             );
126              
127             around BUILDARGS => sub ($orig, $class, @args) {
128             my $args = $class->$orig(@args);
129              
130             # set unblessed hashrefs aside, and defer creation of blessed objects until needed
131             $args->{_errors} = delete $args->{errors} if
132             exists $args->{errors} and any { !blessed($_) } $args->{errors}->@*;
133             $args->{_annotations} = delete $args->{annotations} if
134             exists $args->{annotations} and any { !blessed($_) } $args->{annotations}->@*;
135              
136             croak 'inconsistent inputs: errors is not empty but valid is true'
137             if $args->{valid}
138             and (exists $args->{_errors} and $args->{_errors}->@*
139             or exists $args->{errors} and $args->{errors}->@*);
140              
141             croak 'inconsistent inputs: errors is empty but valid is false'
142             if exists $args->{valid} and not $args->{valid}
143             and (not exists $args->{_errors} or not $args->{_errors}->@*)
144             and (not exists $args->{errors} or not $args->{errors}->@*);
145              
146             return $args;
147             };
148              
149 17414     17414 0 265411 sub BUILD ($self, $args) {
  17414         24060  
  17414         21979  
  17414         28546  
150 17414 50       48846 $self->{_errors} = $args->{_errors} if exists $args->{_errors};
151 17414 100       43420 $self->{_annotations} = $args->{_annotations} if exists $args->{_annotations};
152              
153 17414 100 100     348212 if (exists $args->{data} and exists $args->{defaults}) {
154 10         181 jsonp_set($args->{data}, $args->{defaults}->%{$_}) foreach keys $args->{defaults}->%*;
155             }
156             }
157              
158 25775     25775 1 118913 sub format ($self, $style, $formatted_annotations = undef) {
  25775         33875  
  25775         35667  
  25775         36907  
  25775         33413  
159 25775   33     126598 $formatted_annotations //= $self->formatted_annotations;
160              
161 25775 100       69997 if ($style eq 'flag') {
    100          
    100          
    100          
    50          
162 1 50       15 return +{ valid => $self->valid ? true : false };
163             }
164             elsif ($style eq 'basic') {
165             return +{
166 25769 100 100     335883 valid => $self->valid ? true : false,
    100 100        
    100          
    100          
167             $self->valid
168             ? ($formatted_annotations && $self->annotation_count ? (annotations => [ map $_->TO_JSON, $self->annotations ]) : ())
169             : (errors => [ map $_->TO_JSON, $self->errors ]),
170             $self->valid && $self->defaults ? (defaults => $self->defaults) : (),
171             };
172             }
173             # note: strict_basic will NOT be supported after draft 2019-09!
174             elsif ($style eq 'strict_basic') {
175             return +{
176 1 50 0     16 valid => ($self->valid ? true : false),
    0          
    50          
177             $self->valid
178             ? ($formatted_annotations && $self->annotation_count ? (annotations => [ map _map_uris($_->TO_JSON), $self->annotations ]) : ())
179             : (errors => [ map _map_uris($_->TO_JSON), $self->errors ]),
180             };
181             }
182             elsif ($style eq 'terse') {
183 2         3 my (%instance_locations, %keyword_locations);
184              
185             my @errors = grep {
186 2         6 my ($keyword, $error) = ($_->keyword, $_->error);
  29         68  
187              
188 29   66     299 my $keep = 0+!!(
189             not $keyword
190             or (
191             not grep $keyword eq $_, qw(allOf anyOf if then else dependentSchemas contains propertyNames)
192             and ($keyword ne 'oneOf' or $error ne 'no subschemas are valid')
193             and ($keyword ne 'prefixItems' or $error eq 'item not permitted')
194             and ($keyword ne 'items' or $error eq 'item not permitted' or $error eq 'additional item not permitted')
195             and ($keyword ne 'additionalItems' or $error eq 'additional item not permitted')
196             and (not grep $keyword eq $_, qw(properties patternProperties)
197             or $error eq 'property not permitted')
198             and ($keyword ne 'additionalProperties' or $error eq 'additional property not permitted'))
199             and ($keyword ne 'dependentRequired' or $error ne 'not all dependencies are satisfied')
200             );
201              
202 29 100       51 ++$instance_locations{$_->instance_location} if $keep;
203 29 100       54 ++$keyword_locations{$_->keyword_location} if $keep;
204              
205 29         39 $keep;
206             }
207             $self->errors;
208              
209 2 50 33     32 die 'uh oh, have no errors left to report' if not $self->valid and not @errors;
210              
211             return +{
212 2 50 0     54 valid => $self->valid ? true : false,
    0          
    50          
213             $self->valid
214             ? ($formatted_annotations && $self->annotation_count ? (annotations => [ map $_->TO_JSON, $self->annotations ]) : ())
215             : (errors => [ map $_->TO_JSON, @errors ]),
216             };
217             }
218             elsif ($style eq 'data_only') {
219 2 50       23 return 'valid' if not $self->error_count;
220             # Note: this output is going to be confusing when coming from a schema with a 'oneOf', 'not',
221             # etc. Perhaps generating the strings with indentation levels, as derived from a nested format,
222             # might be more readable.
223 2         5 return join("\n", uniq(map $_->stringify, $self->errors));
224             }
225              
226             # TODO: support detailed, verbose ?
227 0         0 croak 'unsupported output format';
228             }
229              
230 0 0   0 1 0 sub count { $_[0]->valid ? $_[0]->annotation_count : $_[0]->error_count }
231              
232 8     8 1 3730 sub combine ($self, $other, $swap) {
  8         12  
  8         10  
  8         11  
  8         11  
233 8 100       27 croak 'wrong type for & operation' if not $other->$_isa(__PACKAGE__);
234              
235 7 100       124 return $self if refaddr($other) == refaddr($self);
236              
237 6 100 100     104 return ref($self)->new(
      33        
      66        
      50        
      50        
238             valid => $self->valid && $other->valid,
239             annotations => [
240             $self->annotations,
241             $other->annotations,
242             ],
243             errors => [
244             $self->errors,
245             $other->errors,
246             ],
247             output_format => $self->output_format,
248             formatted_annotations => $self->formatted_annotations || $other->formatted_annotations,
249             $self->defaults || $other->defaults
250             ? (defaults => +{ ($self->defaults//{})->%*, ($other->defaults//{})->%* })
251             : (),
252             );
253             }
254              
255 0     0 0 0 sub stringify ($self) {
  0         0  
  0         0  
256 0         0 return $self->format('data_only');
257             }
258              
259 25773     25773 1 220202 sub TO_JSON ($self) {
  25773         34995  
  25773         30338  
260 25773 50       456523 croak 'cannot produce JSON output for data_only format' if $self->output_format eq 'data_only';
261 25773         428687 $self->format($self->output_format);
262             }
263              
264 16890     16890 1 91806 sub dump ($self) {
  16890         25855  
  16890         23988  
265 16890         166710 my $encoder = JSON::Schema::Modern::_JSON_BACKEND()->new
266             ->utf8(0)
267             ->convert_blessed(1)
268             ->canonical(1)
269             ->indent(1)
270             ->space_after(1);
271 16890 50       92709 $encoder->indent_length(2) if $encoder->can('indent_length');
272 16890         74480 $encoder->encode($self);
273             }
274              
275             # turns the JSON pointers in instance_location, keyword_location into a URI fragments,
276             # for strict draft-201909 adherence
277 8     8   1255 sub _map_uris ($data) {
  8         26  
  8         8  
278             return +{
279             %$data,
280 8         31 map +($_ => Mojo::URL->new->fragment($data->{$_})->to_string),
281             qw(instanceLocation keywordLocation),
282             };
283             }
284              
285             1;
286              
287             __END__