File Coverage

blib/lib/JSON/Schema/Modern/Result.pm
Criterion Covered Total %
statement 142 150 94.6
branch 45 62 72.5
condition 29 48 60.4
subroutine 39 43 90.7
pod 7 12 58.3
total 262 315 83.1


line stmt bran cond sub pod time code
1 46     46   297 use strict;
  46         78  
  46         1482  
2 46     46   169 use warnings;
  46         81  
  46         3027  
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.641';
8              
9 46     46   616 use 5.020;
  46         130  
10 46     46   156 use Moo;
  46         69  
  46         259  
11 46     46   14008 use strictures 2;
  46         418  
  46         1571  
12 46     46   16196 use stable 0.031 'postderef';
  46         702  
  46         329  
13 46     46   8168 use experimental 'signatures';
  46         108  
  46         163  
14 46     46   2309 no autovivification warn => qw(fetch store exists delete);
  46         102  
  46         350  
15 46     46   2997 use if "$]" >= 5.022, experimental => 're_strict';
  46         79  
  46         1124  
16 46     46   3090 no if "$]" >= 5.031009, feature => 'indirect';
  46         118  
  46         2637  
17 46     46   202 no if "$]" >= 5.033001, feature => 'multidimensional';
  46         86  
  46         2158  
18 46     46   199 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  46         95  
  46         1915  
19 46     46   200 no if "$]" >= 5.041009, feature => 'smartmatch';
  46         66  
  46         1599  
20 46     46   194 no feature 'switch';
  46         80  
  46         1265  
21 46     46   236 use MooX::TypeTiny;
  46         107  
  46         390  
22 46     46   30683 use Types::Standard qw(ArrayRef InstanceOf Enum Bool Str Maybe Tuple Map Any);
  46         77  
  46         390  
23 46     46   92220 use Types::Common::Numeric 'PositiveInt';
  46         85  
  46         289  
24 46     46   51456 use JSON::Schema::Modern::Annotation;
  46         599  
  46         1491  
25 46     46   278 use JSON::Schema::Modern::Error;
  46         74  
  46         1108  
26 46     46   179 use JSON::Schema::Modern::Utilities qw(true false json_pointer_type jsonp_set);
  46         70  
  46         2570  
27 46     46   238 use JSON::PP ();
  46         71  
  46         976  
28 46     46   179 use List::Util 1.45 'uniqstr';
  46         741  
  46         3436  
29 46     46   229 use if "$]" < 5.041010, 'List::Util' => qw(any all);
  46         113  
  46         1811  
30 46     46   173 use if "$]" >= 5.041010, experimental => qw(keyword_any keyword_all);
  46         70  
  46         1114  
31 46     46   3861 use Carp 'croak';
  46         66  
  46         2170  
32 46     46   258 use builtin::compat qw(refaddr blessed);
  46         82  
  46         653  
33 46     46   6224 use namespace::clean;
  46         101  
  46         201  
34              
35             use overload
36             '&' => \&combine,
37 0     0   0 '""' => sub { $_[0]->stringify },
38 46     46   19296 fallback => 1;
  46         77  
  46         527  
39              
40             has valid => (
41             is => 'ro',
42             isa => Bool|InstanceOf('JSON::PP::true')|InstanceOf('JSON::PP::false'),
43             coerce => sub { $_[0] ? true : false }, # might be JSON::PP::* or builtin::* booleans
44             lazy => 1,
45             default => sub ($self) { $self->error_count == 0 },
46             );
47 0     0 0 0 sub result { goto \&valid } # backcompat only
48              
49             has exception => (
50             is => 'ro',
51             isa => Bool,
52             lazy => 1,
53             default => sub ($self) { any { $_->exception or $_->error =~ /^EXCEPTION: / } $self->errors },
54             );
55              
56             # has errors
57             # has annotations
58             # turn hashrefs in _errors or _annotations into blessed objects
59             has $_.'s' => (
60             is => 'bare',
61             reader => '__'.$_.'s',
62             isa => ArrayRef[InstanceOf['JSON::Schema::Modern::'.ucfirst]],
63             lazy => 1,
64             default => do {
65             my $type = $_;
66             sub ($self) {
67             return [] if not (($self->{'_'.$type.'s'}//[])->@*);
68              
69             # E() and A() in ::Utilities returns an unblessed hashref, which is used to create a real object
70             # by its BUILDARGS sub
71             return [ map +(('JSON::Schema::Modern::'.ucfirst($type))->new($_)), $self->{'_'.$type.'s'}->@* ];
72             };
73             },
74             ) foreach qw(error annotation);
75              
76 28385     28385 1 977911 sub errors { $_[0]->__errors->@* }
77 6 100 50 6 0 500 sub error_count { scalar(($_[0]->{errors}//[])->@*) || scalar(($_[0]->{_errors}//[])->@*) }
      50        
78 40     40 1 2391 sub annotations { $_[0]->__annotations->@* }
79 14322 100 100 14322 0 679712 sub annotation_count { scalar(($_[0]->{annotations}//[])->@*) || scalar(($_[0]->{_annotations}//[])->@*) }
      100        
80              
81             has recommended_response => (
82             is => 'rw',
83             isa => Maybe[Tuple[PositiveInt, Str]],
84             lazy => 1,
85             default => sub ($self) {
86             return if not $self->errors;
87              
88             for my $error ($self->errors) {
89             my $pe = $error->recommended_response;
90             return $pe if $pe;
91             }
92              
93             return [ 500, 'Internal Server Error' ] if $self->exception;
94             return [ 400, ($self->errors)[0]->stringify ];
95             },
96             );
97              
98             # strict_basic can only be used with draft2019-09.
99 46     46   36309 use constant OUTPUT_FORMATS => [qw(flag basic strict_basic terse data_only)];
  46         89  
  46         72744  
100              
101             has output_format => (
102             is => 'rw',
103             isa => Enum(OUTPUT_FORMATS),
104             default => 'basic',
105             );
106              
107             has formatted_annotations => (
108             is => 'ro',
109             isa => Bool,
110             default => 1,
111             );
112              
113             has defaults => (
114             is => 'ro',
115             isa => Map[json_pointer_type, Any],
116             );
117              
118             has data => (
119             is => 'ro',
120             );
121              
122             around BUILDARGS => sub ($orig, $class, @args) {
123             my $args = $class->$orig(@args);
124              
125             # set unblessed hashrefs aside, and defer creation of blessed objects until needed
126             $args->{_errors} = delete $args->{errors} if
127             exists $args->{errors} and any { !blessed($_) } $args->{errors}->@*;
128             $args->{_annotations} = delete $args->{annotations} if
129             exists $args->{annotations} and any { !blessed($_) } $args->{annotations}->@*;
130              
131             croak 'inconsistent inputs: errors is not empty but valid is true'
132             if $args->{valid}
133             and (exists $args->{_errors} and $args->{_errors}->@*
134             or exists $args->{errors} and $args->{errors}->@*);
135              
136             croak 'inconsistent inputs: errors is empty but valid is false'
137             if exists $args->{valid} and not $args->{valid}
138             and (not exists $args->{_errors} or not $args->{_errors}->@*)
139             and (not exists $args->{errors} or not $args->{errors}->@*);
140              
141             return $args;
142             };
143              
144 17414     17414 0 244828 sub BUILD ($self, $args) {
  17414         22037  
  17414         23900  
  17414         20063  
145 17414 50       48489 $self->{_errors} = $args->{_errors} if exists $args->{_errors};
146 17414 100       45908 $self->{_annotations} = $args->{_annotations} if exists $args->{_annotations};
147              
148 17414 100 100     329227 if (exists $args->{data} and exists $args->{defaults}) {
149 10         131 jsonp_set($args->{data}, $args->{defaults}->%{$_}) foreach keys $args->{defaults}->%*;
150             }
151             }
152              
153 25775     25775 1 117167 sub format ($self, $style, $formatted_annotations = undef) {
  25775         33768  
  25775         33467  
  25775         37639  
  25775         29983  
154 25775   33     121472 $formatted_annotations //= $self->formatted_annotations;
155              
156 25775 100       68720 if ($style eq 'flag') {
    100          
    100          
    100          
    50          
157 1 50       13 return +{ valid => $self->valid ? true : false };
158             }
159             elsif ($style eq 'basic') {
160             return +{
161 25769 100 100     315306 valid => $self->valid ? true : false,
    100 100        
    100          
    100          
162             $self->valid
163             ? ($formatted_annotations && $self->annotation_count ? (annotations => [ map $_->TO_JSON, $self->annotations ]) : ())
164             : (errors => [ map $_->TO_JSON, $self->errors ]),
165             $self->valid && $self->defaults ? (defaults => $self->defaults) : (),
166             };
167             }
168             # note: strict_basic will NOT be supported after draft 2019-09!
169             elsif ($style eq 'strict_basic') {
170             return +{
171 1 50 0     13 valid => ($self->valid ? true : false),
    0          
    50          
172             $self->valid
173             ? ($formatted_annotations && $self->annotation_count ? (annotations => [ map _map_uris($_->TO_JSON), $self->annotations ]) : ())
174             : (errors => [ map _map_uris($_->TO_JSON), $self->errors ]),
175             };
176             }
177             elsif ($style eq 'terse') {
178 2         3 my (%instance_locations, %keyword_locations);
179              
180             my @errors = grep {
181 2         8 my ($keyword, $error) = ($_->keyword, $_->error);
  29         62  
182              
183 29   66     240 my $keep = 0+!!(
184             not $keyword
185             or (
186             not grep $keyword eq $_, qw(allOf anyOf if then else dependentSchemas contains propertyNames)
187             and ($keyword ne 'oneOf' or $error ne 'no subschemas are valid')
188             and ($keyword ne 'prefixItems' or $error eq 'item not permitted')
189             and ($keyword ne 'items' or $error eq 'item not permitted' or $error eq 'additional item not permitted')
190             and ($keyword ne 'additionalItems' or $error eq 'additional item not permitted')
191             and (not grep $keyword eq $_, qw(properties patternProperties)
192             or $error eq 'property not permitted')
193             and ($keyword ne 'additionalProperties' or $error eq 'additional property not permitted'))
194             and ($keyword ne 'dependentRequired' or $error ne 'not all dependencies are satisfied')
195             );
196              
197 29 100       49 ++$instance_locations{$_->instance_location} if $keep;
198 29 100       52 ++$keyword_locations{$_->keyword_location} if $keep;
199              
200 29         55 $keep;
201             }
202             $self->errors;
203              
204 2 50 33     45 die 'uh oh, have no errors left to report' if not $self->valid and not @errors;
205              
206             return +{
207 2 50 0     52 valid => $self->valid ? true : false,
    0          
    50          
208             $self->valid
209             ? ($formatted_annotations && $self->annotation_count ? (annotations => [ map $_->TO_JSON, $self->annotations ]) : ())
210             : (errors => [ map $_->TO_JSON, @errors ]),
211             };
212             }
213             elsif ($style eq 'data_only') {
214 2 50       8 return 'valid' if not $self->error_count;
215             # Note: this output is going to be confusing when coming from a schema with a 'oneOf', 'not',
216             # etc. Perhaps generating the strings with indentation levels, as derived from a nested format,
217             # might be more readable.
218 2         5 return join("\n", uniqstr(map $_->stringify, $self->errors));
219             }
220              
221             # TODO: support detailed, verbose ?
222 0         0 croak 'unsupported output format';
223             }
224              
225 0 0   0 1 0 sub count { $_[0]->valid ? $_[0]->annotation_count : $_[0]->error_count }
226              
227 8     8 1 3527 sub combine ($self, $other, $swap) {
  8         15  
  8         12  
  8         11  
  8         11  
228 8 100 66     249 croak 'wrong type for & operation' if not (blessed($other) and $other->isa(__PACKAGE__));
229              
230 7 100       32 return $self if refaddr($other) == refaddr($self);
231              
232 6 100 100     132 return ref($self)->new(
      33        
      66        
      50        
      50        
233             valid => $self->valid && $other->valid,
234             annotations => [
235             $self->annotations,
236             $other->annotations,
237             ],
238             errors => [
239             $self->errors,
240             $other->errors,
241             ],
242             output_format => $self->output_format,
243             formatted_annotations => $self->formatted_annotations || $other->formatted_annotations,
244             $self->defaults || $other->defaults
245             ? (defaults => +{ ($self->defaults//{})->%*, ($other->defaults//{})->%* })
246             : (),
247             );
248             }
249              
250 0     0 0 0 sub stringify ($self) {
  0         0  
  0         0  
251 0         0 return $self->format('data_only');
252             }
253              
254 25773     25773 1 206955 sub TO_JSON ($self) {
  25773         35314  
  25773         32015  
255 25773 50       424289 croak 'cannot produce JSON output for data_only format' if $self->output_format eq 'data_only';
256 25773         406614 $self->format($self->output_format);
257             }
258              
259 16890     16890 1 98274 sub dump ($self) {
  16890         26511  
  16890         22680  
260 16890         166855 my $encoder = JSON::Schema::Modern::_JSON_BACKEND()->new
261             ->utf8(0)
262             ->convert_blessed(1)
263             ->canonical(1)
264             ->indent(1)
265             ->space_after(1);
266 16890 50       87831 $encoder->indent_length(2) if $encoder->can('indent_length');
267 16890         75166 $encoder->encode($self);
268             }
269              
270             # turns the JSON pointers in instance_location, keyword_location into a URI fragments,
271             # for strict draft-201909 adherence
272 8     8   1231 sub _map_uris ($data) {
  8         12  
  8         10  
273             return +{
274             %$data,
275 8         29 map +($_ => Mojo::URL->new->fragment($data->{$_})->to_string),
276             qw(instanceLocation keywordLocation),
277             };
278             }
279              
280             1;
281              
282             __END__