File Coverage

blib/lib/Test/JSON/Schema/Acceptance.pm
Criterion Covered Total %
statement 205 218 94.0
branch 78 96 81.2
condition 68 81 83.9
subroutine 36 39 92.3
pod 1 4 25.0
total 388 438 88.5


line stmt bran cond sub pod time code
1 17     17   4732080 use strict;
  17         158  
  17         550  
2 17     17   97 use warnings;
  17         37  
  17         1153  
3             package Test::JSON::Schema::Acceptance; # git description: v1.018-3-g10756e0
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Acceptance testing for JSON-Schema based validators
6              
7             our $VERSION = '1.019';
8              
9 17     17   449 use 5.020;
  17         70  
10 17     17   10876 use Moo;
  17         126930  
  17         92  
11 17     17   34614 use strictures 2;
  17         29192  
  17         771  
12 17     17   5435 use experimental qw(signatures postderef);
  17         14586  
  17         197  
13 17     17   3735 no if "$]" >= 5.031009, feature => 'indirect';
  17         51  
  17         214  
14 17     17   847 no if "$]" >= 5.033001, feature => 'multidimensional';
  17         67  
  17         115  
15 17     17   804 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  17         49  
  17         138  
16 17     17   601 use Test2::API ();
  17         46  
  17         386  
17 17     17   8247 use Test2::Todo;
  17         19850  
  17         439  
18 17     17   11166 use Test2::Tools::Compare ();
  17         1987963  
  17         682  
19 17     17   9023 use JSON::MaybeXS 1.004001;
  17         103195  
  17         1346  
20 17     17   5883 use File::ShareDir 'dist_dir';
  17         275538  
  17         1100  
21 17     17   8423 use Feature::Compat::Try;
  17         5456  
  17         107  
22 17     17   53243 use MooX::TypeTiny 0.002002;
  17         5585  
  17         111  
23 17     17   219545 use Types::Standard 1.016003 qw(Str InstanceOf ArrayRef HashRef Dict Any HasMethods Bool Optional Slurpy);
  17         1893625  
  17         254  
24 17     17   77497 use Types::Common::Numeric 'PositiveOrZeroInt';
  17         429383  
  17         168  
25 17     17   24776 use Path::Tiny 0.069;
  17         133985  
  17         1398  
26 17     17   162 use List::Util 1.33 qw(any max sum0);
  17         348  
  17         1446  
27 17     17   9529 use Ref::Util qw(is_plain_arrayref is_plain_hashref is_ref);
  17         10043  
  17         1329  
28 17     17   8797 use namespace::clean;
  17         265560  
  17         376  
29              
30             has specification => (
31             is => 'ro',
32             isa => Str,
33             lazy => 1,
34             default => 'draft2020-12',
35             predicate => '_has_specification',
36             );
37              
38             # specification version => metaschema URI
39 17         82272 use constant METASCHEMA => {
40             'draft-next' => 'https://json-schema.org/draft/next/schema',
41             'draft2020-12' => 'https://json-schema.org/draft/2020-12/schema',
42             'draft2019-09' => 'https://json-schema.org/draft/2019-09/schema',
43             'draft7' => 'http://json-schema.org/draft-07/schema#',
44             'draft6' => 'http://json-schema.org/draft-06/schema#',
45             'draft4' => 'http://json-schema.org/draft-04/schema#',
46 17     17   10066 };
  17         59  
47              
48             has test_dir => (
49             is => 'ro',
50             isa => InstanceOf['Path::Tiny'],
51             coerce => sub { path($_[0])->absolute('.') },
52             lazy => 1,
53             builder => '_build_test_dir',
54             predicate => '_has_test_dir',
55             );
56 58     58   795 sub _build_test_dir { path(dist_dir('Test-JSON-Schema-Acceptance'), 'tests', $_[0]->specification) };
57              
58             has additional_resources => (
59             is => 'ro',
60             isa => InstanceOf['Path::Tiny'],
61             coerce => sub { path($_[0])->absolute('.') },
62             lazy => 1,
63             default => sub { $_[0]->test_dir->parent->parent->child('remotes') },
64             );
65              
66             has verbose => (
67             is => 'ro',
68             isa => Bool,
69             default => 0,
70             );
71              
72             has include_optional => (
73             is => 'ro',
74             isa => Bool,
75             default => 0,
76             );
77              
78             has skip_dir => (
79             is => 'ro',
80             isa => ArrayRef[Str],
81             coerce => sub { ref($_[0]) ? $_[0] : [ $_[0] ] },
82             lazy => 1,
83             default => sub { [] },
84             );
85              
86             has test_schemas => (
87             is => 'ro',
88             isa => Bool,
89             );
90              
91             has results => (
92             is => 'rwp',
93             init_arg => undef,
94             isa => ArrayRef[Dict[
95             file => InstanceOf['Path::Tiny'],
96             map +($_ => PositiveOrZeroInt), qw(pass todo_fail fail),
97             ]],
98             );
99              
100             has results_text => (
101             is => 'ro',
102             init_arg => undef,
103             isa => Str,
104             lazy => 1,
105             builder => '_build_results_text',
106             );
107              
108             around BUILDARGS => sub ($orig, $class, @args) {
109             my %args = @args % 2 ? ( specification => 'draft'.$args[0] ) : @args;
110             $args{specification} = 'draft2020-12' if ($args{specification} // '') eq 'latest';
111             $class->$orig(\%args);
112             };
113              
114 57     57 0 11595 sub BUILD ($self, @) {
  57         125  
  57         104  
115 57 100       1122 -d $self->test_dir or die 'test_dir does not exist: '.$self->test_dir;
116             }
117              
118             sub acceptance {
119 58     58 1 346740 my $self = shift;
120 58 100       318 my $options = +{ ref $_[0] eq 'CODE' ? (validate_json_string => @_) : @_ };
121              
122             die 'require one or the other of "validate_data", "validate_json_string"'
123 58 50 66     259 if not $options->{validate_data} and not $options->{validate_json_string};
124              
125             die 'cannot provide both "validate_data" and "validate_json_string"'
126 58 50 66     340 if $options->{validate_data} and $options->{validate_json_string};
127              
128 58 100       265 warn "'skip_tests' option is deprecated" if $options->{skip_tests};
129              
130 58         229 my $ctx = Test2::API::context;
131              
132 58 100 66     5768 if ($options->{add_resource} and -d $self->additional_resources) {
133             # this is essentially what `bin/jsonschema_suite remote` does: resolves the filename against the
134             # base uri to determine the absolute schema location of each resource.
135 1         43 my $base = 'http://localhost:1234';
136 1         23 $ctx->note('adding resources from '.$self->additional_resources.' with the base URI "'.$base.'"...');
137 3         8 $self->additional_resources->visit(
138 3     3   6 sub ($path, @) {
  3         654  
139 3 100 66     12 return if not $path->is_file or $path !~ /\.json$/;
140 2         72 my $data = $self->json_deserialize($path->slurp_raw);
141 2         373 my $file = $path->relative($self->additional_resources);
142 2         510 my $uri = $base.'/'.$file;
143 2         17 $options->{add_resource}->($uri => $data);
144             },
145 1         1039 { recurse => 1 },
146             );
147             }
148              
149 58 100       1587 $ctx->note('running tests in '.$self->test_dir.' against '
150             .($self->_has_specification ? $self->specification : 'unknown version').'...');
151 58         20135 my $tests = $self->_test_data;
152              
153             # [ { file => .., pass => .., fail => .. }, ... ]
154 58         30877 my @results;
155              
156 58         189 foreach my $one_file (@$tests) {
157 223         491 my %results;
158             next if $options->{tests} and $options->{tests}{file}
159             and not grep $_ eq $one_file->{file},
160             (ref $options->{tests}{file} eq 'ARRAY'
161 223 100 100     911 ? $options->{tests}{file}->@* : $options->{tests}{file});
    100 100        
162              
163 212         947 $ctx->note('');
164              
165 212         48395 foreach my $test_group ($one_file->{json}->@*) {
166             next if $options->{tests} and $options->{tests}{group_description}
167             and not grep $_ eq $test_group->{description},
168             (ref $options->{tests}{group_description} eq 'ARRAY'
169 920 100 100     4290 ? $options->{tests}{group_description}->@* : $options->{tests}{group_description});
    100 100        
170              
171 896         1567 my $todo;
172             $todo = Test2::Todo->new(reason => 'Test marked TODO via "todo_tests"')
173             if $options->{todo_tests}
174             and any {
175 74     74   156 my $o = $_;
176             (not $o->{file} or grep $_ eq $one_file->{file}, (ref $o->{file} eq 'ARRAY' ? $o->{file}->@* : $o->{file}))
177             and
178             (not $o->{group_description} or grep $_ eq $test_group->{description}, (ref $o->{group_description} eq 'ARRAY' ? $o->{group_description}->@* : $o->{group_description}))
179             and not $o->{test_description}
180 74 100 100     673 }
      100        
      100        
181 896 100 100     2708 $options->{todo_tests}->@*;
182              
183 896         3231 my $schema_fails;
184 896 50       3170 if ($self->test_schemas) {
185 0 0       0 die 'specification_version unknown: cannot evaluate schema against metaschema'
186             if not $self->_has_specification;
187              
188             my $metaschema_uri = is_plain_hashref($test_group->{schema}) && $test_group->{schema}{'$schema'}
189             ? $test_group->{schema}{'$schema'}
190 0 0 0     0 : METASCHEMA->{$self->specification};
191 0         0 my $metaschema_schema = { '$ref' => $metaschema_uri };
192             my $result = $options->{validate_data}
193             ? $options->{validate_data}->($metaschema_schema, $test_group->{schema})
194 0 0       0 : $options->{validate_json_string}->($metaschema_schema, $self->json_serialize($test_group->{schema}));
195 0 0       0 if (not $result) {
196 0         0 $ctx->fail('schema for '.$one_file->{file}.': "'.$test_group->{description}.'" fails to validate against '.$metaschema_uri.':');
197 0         0 $ctx->note($self->json_prettyprint($result));
198 0         0 $schema_fails = 1;
199             }
200             }
201              
202 896         4180 foreach my $test ($test_group->{tests}->@*) {
203             next if $options->{tests} and $options->{tests}{test_description}
204             and not grep $_ eq $test->{description},
205             (ref $options->{tests}{test_description} eq 'ARRAY'
206 3031 100 100     11183 ? $options->{tests}{test_description}->@* : $options->{tests}{test_description});
    100 100        
207              
208 3000         4415 my $todo;
209             $todo = Test2::Todo->new(reason => 'Test marked TODO via deprecated "skip_tests"')
210             if ref $options->{skip_tests} eq 'ARRAY'
211             and grep +(($test_group->{description}.' - '.$test->{description}) =~ /$_/),
212 3000 100 100     9995 $options->{skip_tests}->@*;
213              
214             $todo = Test2::Todo->new(reason => 'Test marked TODO via "todo_tests"')
215             if $options->{todo_tests}
216             and any {
217 222     222   466 my $o = $_;
218             (not $o->{file} or grep $_ eq $one_file->{file}, (ref $o->{file} eq 'ARRAY' ? $o->{file}->@* : $o->{file}))
219             and
220             (not $o->{group_description} or grep $_ eq $test_group->{description}, (ref $o->{group_description} eq 'ARRAY' ? $o->{group_description}->@* : $o->{group_description}))
221             and
222 222 100 100     1792 (not $o->{test_description} or grep $_ eq $test->{description}, (ref $o->{test_description} eq 'ARRAY' ? $o->{test_description}->@* : $o->{test_description}))
    100 100        
      100        
      100        
223             }
224 3000 100 100     10882 $options->{todo_tests}->@*;
225              
226 3000         12213 my $result = $self->_run_test($one_file, $test_group, $test, $options);
227 3000 50       8705 $result = 0 if $schema_fails;
228              
229 3000 100       12871 ++$results{ $result ? 'pass' : $todo ? 'todo_fail' : 'fail' };
    100          
230             }
231             }
232              
233 212         2350 push @results, { file => $one_file->{file}, pass => 0, 'todo_fail' => 0, fail => 0, %results };
234             }
235              
236 58         1724 $self->_set_results(\@results);
237              
238 58 50       7184 my $diag = $self->verbose ? 'diag' : 'note';
239 58         1245 $ctx->$diag("\n\n".$self->results_text);
240 58         17768 $ctx->$diag('');
241              
242 58 100 100     14833 if ($self->test_dir !~ m{\boptional\b}
      66        
243             and grep +($_->{file} !~ m{^optional/} && $_->{todo_fail} + $_->{fail}), @results) {
244             # non-optional test failures will always be visible, even when not in verbose mode.
245 45         2356 $ctx->diag('WARNING: some non-optional tests are failing! This implementation is not fully compliant with the specification!');
246 45         10489 $ctx->diag('');
247             }
248             else {
249 13         532 $ctx->$diag('Congratulations, all non-optional tests are passing!');
250 13         3912 $ctx->$diag('');
251             }
252              
253 58         13295 $ctx->release;
254             }
255              
256 3000     3000   4629 sub _run_test ($self, $one_file, $test_group, $test, $options) {
  3000         4988  
  3000         4553  
  3000         4342  
  3000         4530  
  3000         4264  
  3000         4441  
257 3000         14668 my $test_name = $one_file->{file}.': "'.$test_group->{description}.'" - "'.$test->{description}.'"';
258              
259 3000         29233 my $pass; # ignores TODO status
260              
261             Test2::API::run_subtest($test_name,
262             sub {
263 3000     3000   963169 my ($result, $schema_before, $data_before, $schema_after, $data_after);
264             try {
265             ($schema_before, $data_before) = map $self->json_serialize($_),
266             $test_group->{schema}, $test->{data};
267              
268             $result = $options->{validate_data}
269             ? $options->{validate_data}->($test_group->{schema}, $test->{data})
270             : $options->{validate_json_string}->($test_group->{schema}, $self->json_serialize($test->{data}));
271              
272             ($schema_after, $data_after) = map $self->json_serialize($_),
273             $test_group->{schema}, $test->{data};
274              
275             my $ctx = Test2::API::context;
276              
277             # skip the ugly matrix comparison
278             my $expected = $test->{valid} ? 'true' : 'false';
279             if ($result xor $test->{valid}) {
280             my $got = $result ? 'true' : 'false';
281             $ctx->fail('evaluation result is incorrect', 'expected '.$expected.'; got '.$got);
282             $ctx->${ $self->verbose ? \'diag' : \'note' }($self->json_prettyprint($result));
283             $pass = 0;
284             }
285             else {
286             $ctx->ok(1, 'test passes: data is valid: '.$expected);
287             $pass = 1;
288             }
289              
290             my @mutated_data_paths = $self->_mutation_check($test->{data});
291             my @mutated_schema_paths = $self->_mutation_check($test_group->{schema});
292              
293             # string check path check behaviour
294             # 0 0 ::is(), and note. $pass = 0
295             # 0 1 ::is(). $pass = 0
296             # 1 0 ->fail and note. $pass = 0
297             # 1 1 no test. $pass does not change.
298              
299             if ($data_before ne $data_after) {
300             Test2::Tools::Compare::is($data_after, $data_before, 'evaluator did not mutate data');
301             $pass = 0;
302             }
303             elsif (@mutated_data_paths) {
304             $ctx->fail('evaluator did not mutate data');
305             $pass = 0
306             }
307              
308             $ctx->note('mutated data at location'.(@mutated_data_paths > 1 ? 's' : '').': '.join(', ', @mutated_data_paths)) if @mutated_data_paths;
309              
310             if ($schema_before ne $schema_after) {
311             Test2::Tools::Compare::is($schema_after, $schema_before, 'evaluator did not mutate schema');
312             $pass = 0;
313             }
314             elsif (@mutated_schema_paths) {
315             $ctx->fail('evaluator did not mutate schema');
316             $pass = 0;
317             }
318              
319             $ctx->note('mutated schema at location'.(@mutated_schema_paths > 1 ? 's' : '').': '.join(', ', @mutated_schema_paths)) if @mutated_schema_paths;
320              
321             $ctx->release;
322             }
323 3000         7423 catch ($e) {
324             chomp(my $exception = $e);
325             my $ctx = Test2::API::context;
326             $ctx->fail('died: '.$exception);
327             $ctx->release;
328             };
329             },
330 3000         24974 { buffered => 1, inherit_trace => 1 },
331             );
332              
333 3000         2811723 return $pass;
334             }
335              
336 5996     5996   9196 sub _mutation_check ($self, $data) {
  5996         9069  
  5996         9409  
  5996         8223  
337 5996         8971 my @error_paths;
338              
339             # [ path => data ]
340 5996         14490 my @nodes = ([ '', $data ]);
341 5996         14705 while (my $node = shift @nodes) {
342 19332 100       39355 if (not defined $node->[1]) {
343 291         859 next;
344             }
345 19041 100       44939 if (is_plain_arrayref($node->[1])) {
    100          
    100          
346 2127         10549 push @nodes, map [ $node->[0].'/'.$_, $node->[1][$_] ], 0 .. $node->[1]->$#*;
347 2127 50       7854 push @error_paths, $node->[0] if tied($node->[1]->@*);
348             }
349             elsif (is_plain_hashref($node->[1])) {
350 7902         39576 push @nodes, map [ $node->[0].'/'.(s/~/~0/gr =~ s!/!~1!gr), $node->[1]{$_} ], keys $node->[1]->%*;
351 7902 100       28938 push @error_paths, $node->[0] if tied($node->[1]->%*);
352             }
353             elsif (is_ref($node->[1])) {
354 1493         4214 next; # boolean or bignum
355             }
356             else {
357 7519         25767 my $flags = B::svref_2object(\$node->[1])->FLAGS;
358 7519 100 75     39369 push @error_paths, $node->[0]
359             if not ($flags & B::SVf_POK xor $flags & (B::SVf_IOK | B::SVf_NOK));
360             }
361             }
362              
363 5996         12719 return @error_paths;
364             }
365              
366             # used for internal serialization/deserialization; does not prettify the string.
367             has _json_serializer => (
368             is => 'ro',
369             isa => HasMethods[qw(encode decode)],
370             handles => {
371             json_serialize => 'encode',
372             json_deserialize => 'decode',
373             },
374             lazy => 1,
375             default => sub { JSON::MaybeXS->new(allow_nonref => 1, utf8 => 1, allow_blessed => 1, canonical => 1) },
376             );
377              
378             # used for displaying diagnostics only
379             has _json_prettyprinter => (
380             is => 'ro',
381             isa => HasMethods['encode'],
382             lazy => 1,
383             handles => {
384             json_prettyprint => 'encode',
385             },
386             default => sub {
387             my $encoder = JSON::MaybeXS->new(allow_nonref => 1, utf8 => 1, allow_blessed => 1, canonical => 1, convert_blessed => 1, pretty => 1)->space_before(0);
388             $encoder->indent_length(2) if $encoder->can('indent_length');
389             $encoder;
390             },
391             );
392              
393             # backcompat shims
394 0     0   0 sub _json_decoder { shift->_json_serializer(@_) }
395 22     22 0 1543 sub json_decoder { shift->_json_serializer(@_) }
396 0     0   0 sub _json_encoder { shift->_json_prettyprinter(@_) }
397 0     0 0 0 sub json_encoder { shift->_json_prettyprinter(@_) }
398              
399             # see JSON::MaybeXS::is_bool
400             my $json_bool = InstanceOf[qw(JSON::XS::Boolean Cpanel::JSON::XS::Boolean JSON::PP::Boolean)];
401              
402             has _test_data => (
403             is => 'lazy',
404             isa => ArrayRef[Dict[
405             file => InstanceOf['Path::Tiny'],
406             json => ArrayRef[Dict[
407             # id => Optional[Str],
408             description => Str,
409             comment => Optional[Str],
410             schema => $json_bool|HashRef,
411             tests => ArrayRef[Dict[
412             # id => Optional[Str],
413             data => Any,
414             description => Str,
415             comment => Optional[Str],
416             valid => $json_bool,
417             Slurpy[Any],
418             ]],
419             Slurpy[Any],
420             ]],
421             ]],
422             );
423              
424 42     42   2257 sub _build__test_data ($self) {
  42         95  
  42         75  
425 42         87 my @test_groups;
426              
427             $self->test_dir->visit(
428             sub {
429 625     625   65727 my ($path) = @_;
430 625 100       16150 return if any { $self->test_dir->child($_)->subsumes($path) } $self->skip_dir->@*;
  10         241  
431 623 100       9957 return if not $path->is_file;
432 599 100       14348 return if $path !~ /\.json$/;
433 598         5562 my $data = $self->json_deserialize($path->slurp_raw);
434 598 100       280048 return if not @$data; # placeholder files for renamed tests
435 587         11625 my $file = $path->relative($self->test_dir);
436 587         123261 push @test_groups, [
437             scalar(split('/', $file)),
438             {
439             file => $file,
440             json => $data,
441             },
442             ];
443             },
444 42         738 { recurse => $self->include_optional },
445             );
446              
447             return [
448             map $_->[1],
449 42 50       3762 sort { $a->[0] <=> $b->[0] || $a->[1]{file} cmp $b->[1]{file} }
  2304         15138  
450             @test_groups
451             ];
452             }
453              
454 34     34   479 sub _build_results_text ($self) {
  34         85  
  34         62  
455 34         74 my @lines;
456 34         589 push @lines, 'Results using '.ref($self).' '.$self->VERSION;
457              
458 34         799 my $test_dir = $self->test_dir;
459 34         410 my $orig_dir = $self->_build_test_dir;
460              
461 34         6986 my $submodule_status = path(dist_dir('Test-JSON-Schema-Acceptance'), 'submodule_status');
462 34 100 66     4101 if ($submodule_status->exists and $submodule_status->parent->subsumes($self->test_dir)) {
    50 33        
463 3         1143 chomp(my ($commit, $url) = $submodule_status->lines);
464 3         1282 push @lines, 'with commit '.$commit;
465 3         20 push @lines, 'from '.$url.':';
466             }
467             elsif ($test_dir eq $orig_dir and not -d '.git') {
468 0         0 die 'submodule_status file is missing - packaging error? cannot continue';
469             }
470              
471 34   50     8305 push @lines, 'specification version: '.($self->specification//'unknown');
472              
473 34 100       532 if ($test_dir ne $orig_dir) {
474 31 50       313 if ($orig_dir->subsumes($test_dir)) {
    50          
475 0         0 $test_dir = '<base test directory>/'.substr($test_dir, length($orig_dir)+1);
476             }
477             elsif (Path::Tiny->cwd->subsumes($test_dir)) {
478 31         7673 $test_dir = $test_dir->relative;
479             }
480 31         9576 push @lines, 'using custom test directory: '.$test_dir;
481             }
482 34 100       388 push @lines, 'optional tests included: '.($self->include_optional ? 'yes' : 'no');
483 34         819 push @lines, map 'skipping directory: '.$_, $self->skip_dir->@*;
484              
485 34         441 push @lines, '';
486 34         247 my $length = max(40, map length $_->{file}, $self->results->@*);
487              
488 34         947 push @lines, sprintf('%-'.$length.'s pass todo-fail fail', 'filename');
489 34         142 push @lines, '-'x($length + 23);
490 34         254 push @lines, map sprintf('%-'.$length.'s % 5d % 4d % 4d', $_->@{qw(file pass todo_fail fail)}),
491             $self->results->@*;
492              
493 34         1031 my $total = +{ map { my $type = $_; $type => sum0(map $_->{$type}, $self->results->@*) } qw(pass todo_fail fail) };
  102         220  
  102         774  
494 34         170 push @lines, '-'x($length + 23);
495 34         198 push @lines, sprintf('%-'.$length.'s % 5d % 5d % 5d', 'TOTAL', $total->@{qw(pass todo_fail fail)});
496              
497 34         1155 return join("\n", @lines, '');
498             }
499              
500             1;
501              
502             __END__
503              
504             =pod
505              
506             =encoding UTF-8
507              
508             =for stopwords validators Schemas ANDed ORed TODO
509              
510             =head1 NAME
511              
512             Test::JSON::Schema::Acceptance - Acceptance testing for JSON-Schema based validators
513              
514             =head1 VERSION
515              
516             version 1.019
517              
518             =head1 SYNOPSIS
519              
520             This module allows the
521             L<JSON Schema Test Suite|https://github.com/json-schema/JSON-Schema-Test-Suite> tests to be used in
522             perl to test a module that implements the JSON Schema specification ("json-schema"). These are the
523             same tests that many modules (libraries, plugins, packages, etc.) use to confirm support of
524             json-schema. Using this module to confirm support gives assurance of interoperability with other
525             modules that run the same tests in different languages.
526              
527             In the JSON::Schema::Modern module, a test could look like the following:
528              
529             use Test::More;
530             use JSON::Schema::Modern;
531             use Test::JSON::Schema::Acceptance;
532              
533             my $accepter = Test::JSON::Schema::Acceptance->new(specification => 'draft7');
534              
535             $accepter->acceptance(
536             validate_data => sub ($schema, $input_data) {
537             return JSON::Schema::Modern->new($schema)->validate($input_data);
538             },
539             todo_tests => [ { file => 'dependencies.json' } ],
540             );
541              
542             done_testing();
543              
544             This would determine if JSON::Schema::Modern's C<validate> method returns the right result for all
545             of the cases in the JSON Schema Test Suite, except for those listed in C<$skip_tests>.
546              
547             =head1 DESCRIPTION
548              
549             L<JSON Schema|http://json-schema.org> is an IETF draft (at time of writing) which allows you to
550             define the structure of JSON.
551              
552             From the overview of the L<draft 2020-12 version of the
553             specification|https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.3>:
554              
555             =over 4
556              
557             This document proposes a new media type "application/schema+json" to identify a JSON Schema for
558             describing JSON data. It also proposes a further optional media type,
559             "application/schema-instance+json", to provide additional integration features. JSON Schemas are
560             themselves JSON documents. This, and related specifications, define keywords allowing authors to
561             describe JSON data in several ways.
562              
563             JSON Schema uses keywords to assert constraints on JSON instances or annotate those instances with
564             additional information. Additional keywords are used to apply assertions and annotations to more
565             complex JSON data structures, or based on some sort of condition.
566              
567             =back
568              
569             This module allows other perl modules (for example JSON::Schema::Modern) to test that they are JSON
570             Schema-compliant, by running the tests from the official test suite, without having to manually
571             convert them to perl tests.
572              
573             You are unlikely to want this module, unless you are attempting to write a module which implements
574             JSON Schema the specification, and want to test your compliance.
575              
576             =head1 CONSTRUCTOR
577              
578             Test::JSON::Schema::Acceptance->new(specification => $specification_version)
579              
580             Create a new instance of Test::JSON::Schema::Acceptance.
581              
582             Available options (which are also available as accessor methods on the object) are:
583              
584             =head2 specification
585              
586             This determines the draft version of the schema to confirm compliance to.
587             Possible values are:
588              
589             =over 4
590              
591             =item *
592              
593             C<draft3>
594              
595             =item *
596              
597             C<draft4>
598              
599             =item *
600              
601             C<draft6>
602              
603             =item *
604              
605             C<draft7>
606              
607             =item *
608              
609             C<draft2019-09>
610              
611             =item *
612              
613             C<draft2020-12>
614              
615             =item *
616              
617             C<latest> (alias for C<draft2020-12>)
618              
619             =item *
620              
621             C<draft-next>
622              
623             =back
624              
625             The default is C<latest>, but in the synopsis example, L<JSON::Schema::Modern> is testing draft 7
626             compliance.
627              
628             (For backwards compatibility, C<new> can be called with a single numeric argument of 3 to 7, which
629             maps to C<draft3> through C<draft7>.)
630              
631             =head2 test_dir
632              
633             Instead of specifying a draft specification to test against, which will select the most appropriate
634             tests, you can pass in the name of a directory of tests to run directly. Files in this directory
635             should be F<.json> files following the format described in
636             L<https://github.com/json-schema-org/JSON-Schema-Test-Suite/blob/main/README.md>.
637              
638             =head2 additional_resources
639              
640             A directory of additional resources which should be made available to the implementation under the
641             base URI C<http://localhost:1234>. This is automatically provided if you did not override
642             C</test_dir>; otherwise, you need to supply it yourself, if any tests require it (for example by
643             containing C<< {"$ref": "http://localhost:1234/foo.json/#a/b/c"} >>). If you supply an
644             L</add_resource> value to L</acceptance> (see below), this will be done for you.
645              
646             =head2 verbose
647              
648             Optional. When true, prints version information and the test result table such that it is visible
649             during C<make test> or C<prove>.
650              
651             =head2 include_optional
652              
653             Optional. When true, tests in subdirectories (most notably F<optional/> are also included.
654              
655             =head2 skip_dir
656              
657             Optional. Pass a string or arrayref consisting of relative path name(s) to indicate directories
658             (within the test directory as specified above with C<specification> or C<test_dir>) which will be
659             skipped. Note that this is only useful currently with C<include_optional => 1>, as otherwise all
660             subdirectories would be skipped anyway.
661              
662             =head2 results
663              
664             After calling L</acceptance>, a list of test results are provided here. It is an arrayref of
665             hashrefs with four keys:
666              
667             =over 4
668              
669             =item *
670              
671             file - the filename
672              
673             =item *
674              
675             pass - the number of pass results for that file
676              
677             =item *
678              
679             todo_fail - the number of fail results for that file that were marked TODO
680              
681             =item *
682              
683             fail - the number of fail results for that file (not including TODO tests)
684              
685             =back
686              
687             =head2 results_text
688              
689             After calling L</acceptance>, a text string tabulating the test results are provided here. This is
690             the same table that is printed at the end of the test run.
691              
692             =head2 test_schemas
693              
694             =for stopwords metaschema
695              
696             Optional. A boolean that, when true, will test every schema against its
697             specification metaschema. (When set, C<specification> must also be set.)
698              
699             This normally should not be set as the official test suite has already been
700             sanity-tested, but you may want to set this in development environments if you
701             are using your own test files.
702              
703             Defaults to false.
704              
705             =head1 SUBROUTINES/METHODS
706              
707             =head2 acceptance
708              
709             =for stopwords truthy falsey
710              
711             Accepts a hash of options as its arguments.
712              
713             (Backwards-compatibility mode: accepts a subroutine which is used as C<validate_json_string>,
714             and a hashref of arguments.)
715              
716             Available options are:
717              
718             =head3 validate_data
719              
720             A subroutine reference, which is passed two arguments: the JSON Schema, and the B<inflated> data
721             structure to be validated. This is the main entry point to your JSON Schema library being tested.
722              
723             The subroutine should return truthy or falsey depending on if the schema was valid for the input or
724             not (an object with a boolean overload is acceptable).
725              
726             Either C<validate_data> or C<validate_json_string> is required.
727              
728             =head3 validate_json_string
729              
730             A subroutine reference, which is passed two arguments: the JSON Schema, and the B<JSON string>
731             containing the data to be validated. This is an alternative to L</validate_data> above, if your
732             library only accepts JSON strings.
733              
734             The subroutine should return truthy or falsey depending on if the schema was valid for the input or
735             not (an object with a boolean overload is acceptable).
736              
737             Exactly one of C<validate_data> or C<validate_json_string> is required.
738              
739             =head3 add_resource
740              
741             Optional. A subroutine reference, which will be called at the start of L</acceptance> multiple
742             times, with two arguments: a URI (string), and a data structure containing schema data to be
743             associated with that URI, for use in some tests that use additional resources (see above). If you do
744             not provide this option, you will be responsible for ensuring that those additional resources are
745             made available to your implementation for the successful execution of the tests that rely on them.
746              
747             For more information, see <https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.9.1.2>.
748              
749             =head3 tests
750              
751             Optional. Restricts tests to just those mentioned (the conditions are ANDed together, not ORed).
752             The syntax can take one of many forms:
753              
754             # run tests in this file
755             tests => { file => 'dependencies.json' }
756              
757             # run tests in these files
758             tests => { file => [ 'dependencies.json', 'refRemote.json' ] }
759              
760             # run tests in this file with this group description
761             tests => {
762             file => 'refRemote.json',
763             group_description => 'remote ref',
764             }
765              
766             # run tests in this file with these group descriptions
767             tests => {
768             file => 'const.json',
769             group_description => [ 'const validation', 'const with object' ],
770             }
771              
772             # run tests in this file with this group description and test description
773             tests => {
774             file => 'const.json',
775             group_description => 'const validation',
776             test_description => 'another type is invalid',
777             }
778              
779             # run tests in this file with this group description and these test descriptions
780             tests => {
781             file => 'const.json',
782             group_description => 'const validation',
783             test_description => [ 'same value is valid', 'another type is invalid' ],
784             }
785              
786             =head3 todo_tests
787              
788             Optional. Mentioned tests will run as L<"TODO"|Test::More/TODO: BLOCK>. Uses arrayrefs of
789             the same hashref structure as L</tests> above, which are ORed together.
790              
791             todo_tests => [
792             # all tests in this file are TODO
793             { file => 'dependencies.json' },
794             # just some tests in this file are TODO
795             { file => 'boolean_schema.json', test_description => 'array is invalid' },
796             # .. etc
797             ]
798              
799             =head1 ACKNOWLEDGEMENTS
800              
801             =for stopwords Perrett Signes
802              
803             Daniel Perrett <perrettdl@cpan.org> for the concept and help in design.
804              
805             Ricardo Signes <rjbs@cpan.org> for direction to and creation of Test::Fatal.
806              
807             Various others in #perl-help.
808              
809             =for stopwords OpenAPI
810              
811             =head1 SUPPORT
812              
813             Bugs may be submitted through L<https://github.com/karenetheridge/Test-JSON-Schema-Acceptance/issues>.
814              
815             You can also find me on the L<JSON Schema Slack server|https://json-schema.slack.com> and L<OpenAPI Slack
816             server|https://open-api.slack.com>, which are also great resources for finding help.
817              
818             =head1 AUTHOR
819              
820             Ben Hutton (@relequestual) <relequest@cpan.org>
821              
822             =head1 CONTRIBUTORS
823              
824             =for stopwords Karen Etheridge Daniel Perrett
825              
826             =over 4
827              
828             =item *
829              
830             Karen Etheridge <ether@cpan.org>
831              
832             =item *
833              
834             Daniel Perrett <dp13@sanger.ac.uk>
835              
836             =back
837              
838             =head1 COPYRIGHT AND LICENCE
839              
840             This software is Copyright (c) 2015 by Ben Hutton.
841              
842             This is free software, licensed under:
843              
844             The MIT (X11) License
845              
846             This distribution includes data from the L<https://json-schema.org> test suite, which carries its own
847             licence (see F<share/LICENSE>).
848              
849             =for Pod::Coverage BUILDARGS BUILD json_encoder json_decoder
850              
851             =cut