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   4753976 use strict;
  17         177  
  17         543  
2 17     17   120 use warnings;
  17         57  
  17         1167  
3             package Test::JSON::Schema::Acceptance; # git description: v1.017-8-g212c3e9
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.018';
8              
9 17     17   452 use 5.020;
  17         66  
10 17     17   10309 use Moo;
  17         126801  
  17         90  
11 17     17   33994 use strictures 2;
  17         28395  
  17         736  
12 17     17   5409 use experimental qw(signatures postderef);
  17         14589  
  17         135  
13 17     17   3542 no if "$]" >= 5.031009, feature => 'indirect';
  17         40  
  17         223  
14 17     17   795 no if "$]" >= 5.033001, feature => 'multidimensional';
  17         37  
  17         108  
15 17     17   807 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  17         41  
  17         105  
16 17     17   601 use Test2::API ();
  17         46  
  17         320  
17 17     17   8079 use Test2::Todo;
  17         19501  
  17         442  
18 17     17   10571 use Test2::Tools::Compare ();
  17         1983810  
  17         790  
19 17     17   8925 use JSON::MaybeXS 1.004001;
  17         101289  
  17         1362  
20 17     17   5422 use File::ShareDir 'dist_dir';
  17         276366  
  17         1465  
21 17     17   8319 use Feature::Compat::Try;
  17         5473  
  17         94  
22 17     17   54250 use MooX::TypeTiny 0.002002;
  17         5773  
  17         103  
23 17     17   221282 use Types::Standard 1.016003 qw(Str InstanceOf ArrayRef HashRef Dict Any HasMethods Bool Optional Slurpy);
  17         1828261  
  17         258  
24 17     17   77045 use Types::Common::Numeric 'PositiveOrZeroInt';
  17         421546  
  17         175  
25 17     17   24388 use Path::Tiny 0.069;
  17         134366  
  17         1303  
26 17     17   151 use List::Util 1.33 qw(any max sum0);
  17         320  
  17         1402  
27 17     17   9625 use Ref::Util qw(is_plain_arrayref is_plain_hashref is_ref);
  17         10077  
  17         1426  
28 17     17   8984 use namespace::clean;
  17         263513  
  17         311  
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         80800 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   9483 };
  17         48  
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   744 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 11613 sub BUILD ($self, @) {
  57         115  
  57         107  
115 57 100       1100 -d $self->test_dir or die 'test_dir does not exist: '.$self->test_dir;
116             }
117              
118             sub acceptance {
119 58     58 1 343923 my $self = shift;
120 58 100       313 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     276 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     337 if $options->{validate_data} and $options->{validate_json_string};
127              
128 58 100       262 warn "'skip_tests' option is deprecated" if $options->{skip_tests};
129              
130 58         221 my $ctx = Test2::API::context;
131              
132 58 100 66     5537 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         42 my $base = 'http://localhost:1234';
136 1         24 $ctx->note('adding resources from '.$self->additional_resources.' with the base URI "'.$base.'"...');
137 3         6 $self->additional_resources->visit(
138 3     3   7 sub ($path, @) {
  3         629  
139 3 100 66     10 return if not $path->is_file or $path !~ /\.json$/;
140 2         69 my $data = $self->json_deserialize($path->slurp_raw);
141 2         368 my $file = $path->relative($self->additional_resources);
142 2         498 my $uri = $base.'/'.$file;
143 2         14 $options->{add_resource}->($uri => $data);
144             },
145 1         1019 { recurse => 1 },
146             );
147             }
148              
149 58 100       1619 $ctx->note('running tests in '.$self->test_dir.' against '
150             .($self->_has_specification ? $self->specification : 'unknown version').'...');
151 58         19280 my $tests = $self->_test_data;
152              
153             # [ { file => .., pass => .., fail => .. }, ... ]
154 58         31571 my @results;
155              
156 58         190 foreach my $one_file (@$tests) {
157 223         515 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     958 ? $options->{tests}{file}->@* : $options->{tests}{file});
    100 100        
162              
163 212         948 $ctx->note('');
164              
165 212         47909 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 902 100 100     4187 ? $options->{tests}{group_description}->@* : $options->{tests}{group_description});
    100 100        
170              
171 878         1433 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   179 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     668 }
      100        
      100        
181 878 100 100     2824 $options->{todo_tests}->@*;
182              
183 878         3024 my $schema_fails;
184 878 50       2937 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 878         3548 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 2998 100 100     10659 ? $options->{tests}{test_description}->@* : $options->{tests}{test_description});
    100 100        
207              
208 2967         4593 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 2967 100 100     10013 $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   549 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     1777 (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 2967 100 100     10655 $options->{todo_tests}->@*;
225              
226 2967         11861 my $result = $self->_run_test($one_file, $test_group, $test, $options);
227 2967 50       8620 $result = 0 if $schema_fails;
228              
229 2967 100       12589 ++$results{ $result ? 'pass' : $todo ? 'todo_fail' : 'fail' };
    100          
230             }
231             }
232              
233 212         2487 push @results, { file => $one_file->{file}, pass => 0, 'todo_fail' => 0, fail => 0, %results };
234             }
235              
236 58         1700 $self->_set_results(\@results);
237              
238 58 50       6191 my $diag = $self->verbose ? 'diag' : 'note';
239 58         1250 $ctx->$diag("\n\n".$self->results_text);
240 58         15320 $ctx->$diag('');
241              
242 58 100 100     13968 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         2070 $ctx->diag('WARNING: some non-optional tests are failing! This implementation is not fully compliant with the specification!');
246 45         9709 $ctx->diag('');
247             }
248             else {
249 13         440 $ctx->$diag('Congratulations, all non-optional tests are passing!');
250 13         3358 $ctx->$diag('');
251             }
252              
253 58         12587 $ctx->release;
254             }
255              
256 2967     2967   4532 sub _run_test ($self, $one_file, $test_group, $test, $options) {
  2967         4695  
  2967         4340  
  2967         4150  
  2967         4589  
  2967         4245  
  2967         4259  
257 2967         13985 my $test_name = $one_file->{file}.': "'.$test_group->{description}.'" - "'.$test->{description}.'"';
258              
259 2967         26727 my $pass; # ignores TODO status
260              
261             Test2::API::run_subtest($test_name,
262             sub {
263 2967     2967   943567 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 2967         7331 catch ($e) {
324             chomp(my $exception = $e);
325             my $ctx = Test2::API::context;
326             $ctx->fail('died: '.$exception);
327             $ctx->release;
328             };
329             },
330 2967         24722 { buffered => 1, inherit_trace => 1 },
331             );
332              
333 2967         2759082 return $pass;
334             }
335              
336 5930     5930   8928 sub _mutation_check ($self, $data) {
  5930         9139  
  5930         9160  
  5930         8203  
337 5930         8524 my @error_paths;
338              
339             # [ path => data ]
340 5930         14175 my @nodes = ([ '', $data ]);
341 5930         14882 while (my $node = shift @nodes) {
342 19026 100       38690 if (not defined $node->[1]) {
343 291         908 next;
344             }
345 18735 100       44073 if (is_plain_arrayref($node->[1])) {
    100          
    100          
346 2097         10523 push @nodes, map [ $node->[0].'/'.$_, $node->[1][$_] ], 0 .. $node->[1]->$#*;
347 2097 50       8224 push @error_paths, $node->[0] if tied($node->[1]->@*);
348             }
349             elsif (is_plain_hashref($node->[1])) {
350 7779         39105 push @nodes, map [ $node->[0].'/'.(s/~/~0/gr =~ s!/!~1!gr), $node->[1]{$_} ], keys $node->[1]->%*;
351 7779 100       28615 push @error_paths, $node->[0] if tied($node->[1]->%*);
352             }
353             elsif (is_ref($node->[1])) {
354 1493         4155 next; # boolean or bignum
355             }
356             else {
357 7366         25647 my $flags = B::svref_2object(\$node->[1])->FLAGS;
358 7366 100 75     37856 push @error_paths, $node->[0]
359             if not ($flags & B::SVf_POK xor $flags & (B::SVf_IOK | B::SVf_NOK));
360             }
361             }
362              
363 5930         12667 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 1492 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         93  
  42         78  
425 42         82 my @test_groups;
426              
427             $self->test_dir->visit(
428             sub {
429 625     625   65545 my ($path) = @_;
430 625 100       16196 return if any { $self->test_dir->child($_)->subsumes($path) } $self->skip_dir->@*;
  10         241  
431 623 100       9504 return if not $path->is_file;
432 599 100       14949 return if $path !~ /\.json$/;
433 598         5457 my $data = $self->json_deserialize($path->slurp_raw);
434 598 100       274396 return if not @$data; # placeholder files for renamed tests
435 587         11505 my $file = $path->relative($self->test_dir);
436 587         123597 push @test_groups, [
437             scalar(split('/', $file)),
438             {
439             file => $file,
440             json => $data,
441             },
442             ];
443             },
444 42         761 { recurse => $self->include_optional },
445             );
446              
447             return [
448             map $_->[1],
449 42 50       3768 sort { $a->[0] <=> $b->[0] || $a->[1]{file} cmp $b->[1]{file} }
  2304         15219  
450             @test_groups
451             ];
452             }
453              
454 34     34   436 sub _build_results_text ($self) {
  34         76  
  34         62  
455 34         78 my @lines;
456 34         603 push @lines, 'Results using '.ref($self).' '.$self->VERSION;
457              
458 34         777 my $test_dir = $self->test_dir;
459 34         359 my $orig_dir = $self->_build_test_dir;
460              
461 34         7173 my $submodule_status = path(dist_dir('Test-JSON-Schema-Acceptance'), 'submodule_status');
462 34 100 66     4038 if ($submodule_status->exists and $submodule_status->parent->subsumes($self->test_dir)) {
    50 33        
463 3         970 chomp(my ($commit, $url) = $submodule_status->lines);
464 3         1116 push @lines, 'with commit '.$commit;
465 3         16 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     8535 push @lines, 'specification version: '.($self->specification//'unknown');
472              
473 34 100       466 if ($test_dir ne $orig_dir) {
474 31 50       315 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         7747 $test_dir = $test_dir->relative;
479             }
480 31         9690 push @lines, 'using custom test directory: '.$test_dir;
481             }
482 34 100       382 push @lines, 'optional tests included: '.($self->include_optional ? 'yes' : 'no');
483 34         786 push @lines, map 'skipping directory: '.$_, $self->skip_dir->@*;
484              
485 34         385 push @lines, '';
486 34         210 my $length = max(40, map length $_->{file}, $self->results->@*);
487              
488 34         836 push @lines, sprintf('%-'.$length.'s pass todo-fail fail', 'filename');
489 34         159 push @lines, '-'x($length + 23);
490 34         222 push @lines, map sprintf('%-'.$length.'s % 5d % 4d % 4d', $_->@{qw(file pass todo_fail fail)}),
491             $self->results->@*;
492              
493 34         884 my $total = +{ map { my $type = $_; $type => sum0(map $_->{$type}, $self->results->@*) } qw(pass todo_fail fail) };
  102         204  
  102         667  
494 34         144 push @lines, '-'x($length + 23);
495 34         213 push @lines, sprintf('%-'.$length.'s % 5d % 5d % 5d', 'TOTAL', $total->@{qw(pass todo_fail fail)});
496              
497 34         1028 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.018
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.
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.
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