File Coverage

blib/lib/Test/JSON/Schema/Acceptance.pm
Criterion Covered Total %
statement 204 213 95.7
branch 78 94 82.9
condition 68 78 87.1
subroutine 35 35 100.0
pod 1 2 50.0
total 386 422 91.4


line stmt bran cond sub pod time code
1 17     17   4311823 use strict;
  17         179  
  17         593  
2 17     17   100 use warnings;
  17         35  
  17         1204  
3             package Test::JSON::Schema::Acceptance; # git description: v1.013-11-gc922512
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Acceptance testing for JSON-Schema based validators like JSON::Schema
6              
7             our $VERSION = '1.014';
8              
9 17     17   484 use 5.020;
  17         65  
10 17     17   10462 use Moo;
  17         133062  
  17         103  
11 17     17   37385 use strictures 2;
  17         29475  
  17         802  
12 17     17   12786 use experimental qw(signatures postderef);
  17         62900  
  17         124  
13 17     17   4730 no if "$]" >= 5.031009, feature => 'indirect';
  17         44  
  17         194  
14 17     17   846 no if "$]" >= 5.033001, feature => 'multidimensional';
  17         198  
  17         147  
15 17     17   895 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  17         63  
  17         154  
16 17     17   734 use Test2::API ();
  17         54  
  17         451  
17 17     17   9181 use Test2::Todo;
  17         20108  
  17         471  
18 17     17   11447 use Test2::Tools::Compare ();
  17         2059152  
  17         830  
19 17     17   10819 use JSON::MaybeXS 1.004001;
  17         105430  
  17         1353  
20 17     17   5796 use File::ShareDir 'dist_dir';
  17         278096  
  17         1143  
21 17     17   9032 use Feature::Compat::Try;
  17         5521  
  17         94  
22 17     17   55382 use MooX::TypeTiny 0.002002;
  17         6069  
  17         98  
23 17     17   227780 use Types::Standard 1.010002 qw(Str InstanceOf ArrayRef HashRef Dict Any HasMethods Bool Optional);
  17         1742304  
  17         451  
24 17     17   45107 use Types::Common::Numeric 'PositiveOrZeroInt';
  17         379875  
  17         185  
25 17     17   19607 use Path::Tiny 0.069;
  17         125343  
  17         1427  
26 17     17   180 use List::Util 1.33 qw(any max sum0);
  17         349  
  17         1398  
27 17     17   10880 use Ref::Util qw(is_plain_arrayref is_plain_hashref is_ref);
  17         10325  
  17         1404  
28 17     17   8989 use namespace::clean;
  17         275755  
  17         380  
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         78556 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   9443 };
  17         47  
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 56     56   721 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 55     55 0 10238 sub BUILD ($self, @) {
  55         124  
  55         93  
115 55 100       1020 -d $self->test_dir or die 'test_dir does not exist: '.$self->test_dir;
116             }
117              
118             sub acceptance {
119 56     56 1 372658 my $self = shift;
120 56 100       383 my $options = +{ ref $_[0] eq 'CODE' ? (validate_json_string => @_) : @_ };
121              
122             die 'require one or the other of "validate_data", "validate_json_string"'
123 56 50 66     296 if not $options->{validate_data} and not $options->{validate_json_string};
124              
125             die 'cannot provide both "validate_data" and "validate_json_string"'
126 56 50 66     401 if $options->{validate_data} and $options->{validate_json_string};
127              
128 56 100       327 warn "'skip_tests' option is deprecated" if $options->{skip_tests};
129              
130 56         254 my $ctx = Test2::API::context;
131              
132 56 100 66     5891 if ($options->{add_resource} and -d $self->additional_resources) {
133 1         47 my $base = 'http://localhost:1234'; # TODO? make this customizable
134 1         24 $ctx->note('adding resources from '.$self->additional_resources.' with the base URI "'.$base.'"...');
135 3         7 $self->additional_resources->visit(
136 3     3   6 sub ($path, @) {
  3         747  
137 3 100 66     11 return if not $path->is_file or $path !~ /\.json$/;
138 2         118 my $data = $self->_json_decoder->decode($path->slurp_raw);
139 2         2222 my $file = $path->relative($self->additional_resources);
140 2         594 my $uri = $base.'/'.$file;
141 2         15 $options->{add_resource}->($uri => $data);
142             },
143 1         1102 { recurse => 1 },
144             );
145             }
146              
147 56 100       1848 $ctx->note('running tests in '.$self->test_dir.' against '
148             .($self->_has_specification ? $self->specification : 'unknown version').'...');
149 56         19731 my $tests = $self->_test_data;
150              
151             # [ { file => .., pass => .., fail => .. }, ... ]
152 56         44804 my @results;
153              
154 56         199 foreach my $one_file (@$tests) {
155 221         580 my %results;
156             next if $options->{tests} and $options->{tests}{file}
157             and not grep $_ eq $one_file->{file},
158             (ref $options->{tests}{file} eq 'ARRAY'
159 221 100 100     1171 ? $options->{tests}{file}->@* : $options->{tests}{file});
    100 100        
160              
161 210         1178 $ctx->note('');
162              
163 210         49117 foreach my $test_group ($one_file->{json}->@*) {
164             next if $options->{tests} and $options->{tests}{group_description}
165             and not grep $_ eq $test_group->{description},
166             (ref $options->{tests}{group_description} eq 'ARRAY'
167 825 100 100     4855 ? $options->{tests}{group_description}->@* : $options->{tests}{group_description});
    100 100        
168              
169 801         1580 my $todo;
170             $todo = Test2::Todo->new(reason => 'Test marked TODO via "todo_tests"')
171             if $options->{todo_tests}
172             and any {
173 74     74   170 my $o = $_;
174             (not $o->{file} or grep $_ eq $one_file->{file}, (ref $o->{file} eq 'ARRAY' ? $o->{file}->@* : $o->{file}))
175             and
176             (not $o->{group_description} or grep $_ eq $test_group->{description}, (ref $o->{group_description} eq 'ARRAY' ? $o->{group_description}->@* : $o->{group_description}))
177             and not $o->{test_description}
178 74 100 100     773 }
      100        
      100        
179 801 100 100     3289 $options->{todo_tests}->@*;
180              
181 801         3266 my $schema_fails;
182 801 50       4240 if ($self->test_schemas) {
183 0 0       0 die 'specification_version unknown: cannot evaluate schema against metaschema'
184             if not $self->_has_specification;
185              
186 0         0 my $metaspec_uri = METASCHEMA->{$self->specification};
187             my $result = $options->{validate_data}
188             ? $options->{validate_data}->($metaspec_uri, $test_group->{schema})
189             # we use the decoder here so we don't prettify the string
190 0 0       0 : $options->{validate_json_string}->($metaspec_uri, $self->_json_decoder->encode($test_group->{schema}));
191 0 0       0 if (not $result) {
192 0         0 $ctx->fail('schema for '.$one_file->{file}.': "'.$test_group->{description}.'" fails to validate against '.$metaspec_uri.':');
193 0         0 $ctx->note($self->_json_encoder->encode($result));
194 0         0 $schema_fails = 1;
195             }
196             }
197              
198 801         2909 foreach my $test ($test_group->{tests}->@*) {
199             next if $options->{tests} and $options->{tests}{test_description}
200             and not grep $_ eq $test->{description},
201             (ref $options->{tests}{test_description} eq 'ARRAY'
202 2837 100 100     11618 ? $options->{tests}{test_description}->@* : $options->{tests}{test_description});
    100 100        
203              
204 2806         4146 my $todo;
205             $todo = Test2::Todo->new(reason => 'Test marked TODO via deprecated "skip_tests"')
206             if ref $options->{skip_tests} eq 'ARRAY'
207             and grep +(($test_group->{description}.' - '.$test->{description}) =~ /$_/),
208 2806 100 100     10300 $options->{skip_tests}->@*;
209              
210             $todo = Test2::Todo->new(reason => 'Test marked TODO via "todo_tests"')
211             if $options->{todo_tests}
212             and any {
213 222     222   472 my $o = $_;
214             (not $o->{file} or grep $_ eq $one_file->{file}, (ref $o->{file} eq 'ARRAY' ? $o->{file}->@* : $o->{file}))
215             and
216             (not $o->{group_description} or grep $_ eq $test_group->{description}, (ref $o->{group_description} eq 'ARRAY' ? $o->{group_description}->@* : $o->{group_description}))
217             and
218 222 100 100     1926 (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        
219             }
220 2806 100 100     11237 $options->{todo_tests}->@*;
221              
222 2806         12277 my $result = $self->_run_test($one_file, $test_group, $test, $options);
223 2806 50       9051 $result = 0 if $schema_fails;
224              
225 2806 100       12553 ++$results{ $result ? 'pass' : $todo ? 'todo_fail' : 'fail' };
    100          
226             }
227             }
228              
229 210         2697 push @results, { file => $one_file->{file}, pass => 0, 'todo_fail' => 0, fail => 0, %results };
230             }
231              
232 56         1790 $self->_set_results(\@results);
233              
234 56 50       6385 my $diag = $self->verbose ? 'diag' : 'note';
235 56         1232 $ctx->$diag("\n\n".$self->results_text);
236 56         15655 $ctx->$diag('');
237              
238 56 100 100     13690 if ($self->test_dir !~ m{\boptional\b}
      66        
239             and grep +($_->{file} !~ m{^optional/} && $_->{todo_fail} + $_->{fail}), @results) {
240             # non-optional test failures will always be visible, even when not in verbose mode.
241 43         2104 $ctx->diag('WARNING: some non-optional tests are failing! This implementation is not fully compliant with the specification!');
242 43         9742 $ctx->diag('');
243             }
244             else {
245 13         519 $ctx->$diag('Congratulations, all non-optional tests are passing!');
246 13         3422 $ctx->$diag('');
247             }
248              
249 56         12151 $ctx->release;
250             }
251              
252 2806     2806   4387 sub _run_test ($self, $one_file, $test_group, $test, $options) {
  2806         4445  
  2806         4269  
  2806         3956  
  2806         4310  
  2806         3958  
  2806         3754  
253 2806         14312 my $test_name = $one_file->{file}.': "'.$test_group->{description}.'" - "'.$test->{description}.'"';
254              
255 2806         20726 my $pass; # ignores TODO status
256              
257             Test2::API::run_subtest($test_name,
258             sub {
259 2806     2806   880498 my ($result, $schema_before, $data_before, $schema_after, $data_after);
260             try {
261             ($schema_before, $data_before) = map $self->_json_decoder->encode($_),
262             $test_group->{schema}, $test->{data};
263              
264             $result = $options->{validate_data}
265             ? $options->{validate_data}->($test_group->{schema}, $test->{data})
266             # we use the decoder here so we don't prettify the string
267             : $options->{validate_json_string}->($test_group->{schema}, $self->_json_decoder->encode($test->{data}));
268              
269             ($schema_after, $data_after) = map $self->_json_decoder->encode($_),
270             $test_group->{schema}, $test->{data};
271              
272             my $ctx = Test2::API::context;
273              
274             # skip the ugly matrix comparison
275             my $expected = $test->{valid} ? 'true' : 'false';
276             if ($result xor $test->{valid}) {
277             my $got = $result ? 'true' : 'false';
278             $ctx->fail('evaluation result is incorrect', 'expected '.$expected.'; got '.$got);
279             $pass = 0;
280             }
281             else {
282             $ctx->ok(1, 'test passes: data is valid: '.$expected);
283             $pass = 1;
284             }
285              
286             my @mutated_data_paths = $self->_mutation_check($test->{data});
287             my @mutated_schema_paths = $self->_mutation_check($test_group->{schema});
288              
289             if ($data_before ne $data_after or @mutated_data_paths) {
290             if ($data_before ne $data_after) {
291             Test2::Tools::Compare::is($data_after, $data_before, 'evaluator did not mutate data');
292             }
293             else {
294             $ctx->fail('evaluator did not mutate data');
295             }
296              
297             $ctx->note('mutated data at location'.(@mutated_data_paths > 1 ? 's' : '').': '.join(', ', @mutated_data_paths)) if @mutated_data_paths;
298             $pass = 0;
299             }
300              
301             if ($schema_before ne $schema_after or @mutated_schema_paths) {
302             if ($schema_before ne $schema_after) {
303             Test2::Tools::Compare::is($schema_after, $schema_before, 'evaluator did not mutate schema');
304             }
305             else {
306             $ctx->fail('evaluator did not mutate schema');
307             }
308              
309             $ctx->note('mutated schema at location'.(@mutated_schema_paths > 1 ? 's' : '').': '.join(', ', @mutated_schema_paths)) if @mutated_schema_paths;
310             $pass = 0;
311             }
312              
313             $ctx->release;
314             }
315 2806         7233 catch ($e) {
316             chomp(my $exception = $e);
317             my $ctx = Test2::API::context;
318             $ctx->fail('died: '.$exception);
319             $ctx->release;
320             };
321             },
322 2806         23409 { buffered => 1, inherit_trace => 1 },
323             );
324              
325 2806         2533929 return $pass;
326             }
327              
328 5608     5608   8306 sub _mutation_check ($self, $data) {
  5608         8136  
  5608         8297  
  5608         7365  
329 5608         7992 my @error_paths;
330              
331             # [ path => data ]
332 5608         13119 my @nodes = ([ '', $data ]);
333 5608         13934 while (my $node = shift @nodes) {
334 17695 100       34405 if (not defined $node->[1]) {
335 273         819 next;
336             }
337 17422 100       41111 if (is_plain_arrayref($node->[1])) {
    100          
    100          
338 2055         10214 push @nodes, map [ $node->[0].'/'.$_, $node->[1][$_] ], 0 .. $node->[1]->$#*;
339 2055 50       7743 push @error_paths, $node->[0] if tied($node->[1]->@*);
340             }
341             elsif (is_plain_hashref($node->[1])) {
342 7120         35116 push @nodes, map [ $node->[0].'/'.(s/~/~0/gr =~ s!/!~1!gr), $node->[1]{$_} ], keys $node->[1]->%*;
343 7120 100       24866 push @error_paths, $node->[0] if tied($node->[1]->%*);
344             }
345             elsif (is_ref($node->[1])) {
346 1759         5321 next; # boolean or bignum
347             }
348             else {
349 6488         24364 my $flags = B::svref_2object(\$node->[1])->FLAGS;
350 6488 100 75     33664 push @error_paths, $node->[0]
351             if not ($flags & B::SVf_POK xor $flags & (B::SVf_IOK | B::SVf_NOK));
352             }
353             }
354              
355 5608         12020 return @error_paths;
356             }
357              
358             # used for internal serialization also
359             has _json_decoder => (
360             is => 'ro',
361             isa => HasMethods[qw(encode decode)],
362             lazy => 1,
363             default => sub { JSON::MaybeXS->new(allow_nonref => 1, utf8 => 1, allow_bignum => 1, allow_blessed => 1) },
364             );
365              
366             # used for pretty-printing diagnostics
367             has _json_encoder => (
368             is => 'ro',
369             isa => HasMethods['encode'],
370             lazy => 1,
371             default => sub {
372             my $encoder = shift->_json_decoder->convert_blessed->canonical->pretty;
373             $encoder->indent_length(2) if $encoder->can('indent_length');
374             $encoder;
375             },
376             );
377              
378             # see JSON::MaybeXS::is_bool
379             my $json_bool = InstanceOf[qw(JSON::XS::Boolean Cpanel::JSON::XS::Boolean JSON::PP::Boolean)];
380              
381             has _test_data => (
382             is => 'lazy',
383             isa => ArrayRef[Dict[
384             file => InstanceOf['Path::Tiny'],
385             json => ArrayRef[Dict[
386             # id => Optional[Str],
387             description => Str,
388             comment => Optional[Str],
389             schema => $json_bool|HashRef,
390             tests => ArrayRef[Dict[
391             # id => Optional[Str],
392             data => Any,
393             description => Str,
394             comment => Optional[Str],
395             valid => $json_bool,
396             ]],
397             ]],
398             ]],
399             );
400              
401 40     40   2634 sub _build__test_data ($self) {
  40         89  
  40         81  
402 40         118 my @test_groups;
403              
404             $self->test_dir->visit(
405             sub {
406 609     609   68604 my ($path) = @_;
407 609 100       17498 return if any { $self->test_dir->child($_)->subsumes($path) } $self->skip_dir->@*;
  10         269  
408 607 100       10856 return if not $path->is_file;
409 583 100       16529 return if $path !~ /\.json$/;
410 582         17066 my $data = $self->_json_decoder->decode($path->slurp_raw);
411 582 100       185980 return if not @$data; # placeholder files for renamed tests
412 571         15071 my $file = $path->relative($self->test_dir);
413 571         125448 push @test_groups, [
414             scalar(split('/', $file)),
415             {
416             file => $file,
417             json => $data,
418             },
419             ];
420             },
421 40         725 { recurse => $self->include_optional },
422             );
423              
424             return [
425             map $_->[1],
426 40 50       3967 sort { $a->[0] <=> $b->[0] || $a->[1]{file} cmp $b->[1]{file} }
  2235         14564  
427             @test_groups
428             ];
429             }
430              
431 32     32   371 sub _build_results_text ($self) {
  32         69  
  32         57  
432 32         69 my @lines;
433 32         600 push @lines, 'Results using '.ref($self).' '.$self->VERSION;
434              
435 32         747 my $test_dir = $self->test_dir;
436 32         342 my $orig_dir = $self->_build_test_dir;
437              
438 32         7298 my $submodule_status = path(dist_dir('Test-JSON-Schema-Acceptance'), 'submodule_status');
439 32 100 66     4400 if ($submodule_status->exists and $submodule_status->parent->subsumes($self->test_dir)) {
    50 33        
440 3         902 chomp(my ($commit, $url) = $submodule_status->lines);
441 3         1086 push @lines, 'with commit '.$commit;
442 3         16 push @lines, 'from '.$url.':';
443             }
444             elsif ($test_dir eq $orig_dir and not -d '.git') {
445 0         0 die 'submodule_status file is missing - packaging error? cannot continue';
446             }
447              
448 32   50     7667 push @lines, 'specification version: '.($self->specification//'unknown');
449              
450 32 100       492 if ($test_dir ne $orig_dir) {
451 29 50       234 if ($orig_dir->subsumes($test_dir)) {
    50          
452 0         0 $test_dir = '<base test directory>/'.substr($test_dir, length($orig_dir)+1);
453             }
454             elsif (Path::Tiny->cwd->subsumes($test_dir)) {
455 29         7064 $test_dir = $test_dir->relative;
456             }
457 29         8334 push @lines, 'using custom test directory: '.$test_dir;
458             }
459 32 100       355 push @lines, 'optional tests included: '.($self->include_optional ? 'yes' : 'no');
460 32         763 push @lines, map 'skipping directory: '.$_, $self->skip_dir->@*;
461              
462 32         391 push @lines, '';
463 32         202 my $length = max(40, map length $_->{file}, $self->results->@*);
464              
465 32         710 push @lines, sprintf('%-'.$length.'s pass todo-fail fail', 'filename');
466 32         147 push @lines, '-'x($length + 23);
467 32         215 push @lines, map sprintf('%-'.$length.'s % 5d % 4d % 4d', $_->@{qw(file pass todo_fail fail)}),
468             $self->results->@*;
469              
470 32         736 my $total = +{ map { my $type = $_; $type => sum0(map $_->{$type}, $self->results->@*) } qw(pass todo_fail fail) };
  96         174  
  96         634  
471 32         132 push @lines, '-'x($length + 23);
472 32         179 push @lines, sprintf('%-'.$length.'s % 5d % 5d % 5d', 'TOTAL', $total->@{qw(pass todo_fail fail)});
473              
474 32         970 return join("\n", @lines, '');
475             }
476              
477             1;
478              
479             __END__
480              
481             =pod
482              
483             =encoding UTF-8
484              
485             =for stopwords validators Schemas ANDed ORed TODO
486              
487             =head1 NAME
488              
489             Test::JSON::Schema::Acceptance - Acceptance testing for JSON-Schema based validators like JSON::Schema
490              
491             =head1 VERSION
492              
493             version 1.014
494              
495             =head1 SYNOPSIS
496              
497             This module allows the
498             L<JSON Schema Test Suite|https://github.com/json-schema/JSON-Schema-Test-Suite> tests to be used in
499             perl to test a module that implements the JSON Schema specification ("json-schema"). These are the
500             same tests that many modules (libraries, plugins, packages, etc.) use to confirm support of
501             json-schema. Using this module to confirm support gives assurance of interoperability with other
502             modules that run the same tests in different languages.
503              
504             In the JSON::Schema module, a test could look like the following:
505              
506             use Test::More;
507             use JSON::Schema;
508             use Test::JSON::Schema::Acceptance;
509              
510             my $accepter = Test::JSON::Schema::Acceptance->new(specification => 'draft3');
511              
512             $accepter->acceptance(
513             validate_data => sub ($schema, $input_data) {
514             return JSON::Schema->new($schema)->validate($input_data);
515             },
516             todo_tests => [ { file => 'dependencies.json' } ],
517             );
518              
519             done_testing();
520              
521             This would determine if JSON::Schema's C<validate> method returns the right result for all of the
522             cases in the JSON Schema Test Suite, except for those listed in C<$skip_tests>.
523              
524             =head1 DESCRIPTION
525              
526             L<JSON Schema|http://json-schema.org> is an IETF draft (at time of writing) which allows you to
527             define the structure of JSON.
528              
529             From the overview of the L<draft 2020-12 version of the
530             specification|https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.3>:
531              
532             =over 4
533              
534             This document proposes a new media type "application/schema+json" to identify a JSON Schema for
535             describing JSON data. It also proposes a further optional media type,
536             "application/schema-instance+json", to provide additional integration features. JSON Schemas are
537             themselves JSON documents. This, and related specifications, define keywords allowing authors to
538             describe JSON data in several ways.
539              
540             JSON Schema uses keywords to assert constraints on JSON instances or annotate those instances with
541             additional information. Additional keywords are used to apply assertions and annotations to more
542             complex JSON data structures, or based on some sort of condition.
543              
544             =back
545              
546             This module allows other perl modules (for example JSON::Schema) to test that they are JSON
547             Schema-compliant, by running the tests from the official test suite, without having to manually
548             convert them to perl tests.
549              
550             You are unlikely to want this module, unless you are attempting to write a module which implements
551             JSON Schema the specification, and want to test your compliance.
552              
553             =head1 CONSTRUCTOR
554              
555             Test::JSON::Schema::Acceptance->new(specification => $specification_version)
556              
557             Create a new instance of Test::JSON::Schema::Acceptance.
558              
559             Available options (which are also available as accessor methods on the object) are:
560              
561             =head2 specification
562              
563             This determines the draft version of the schema to confirm compliance to.
564             Possible values are:
565              
566             =over 4
567              
568             =item *
569              
570             C<draft3>
571              
572             =item *
573              
574             C<draft4>
575              
576             =item *
577              
578             C<draft6>
579              
580             =item *
581              
582             C<draft7>
583              
584             =item *
585              
586             C<draft2019-09>
587              
588             =item *
589              
590             C<draft2020-12>
591              
592             =item *
593              
594             C<latest> (alias for C<draft2020-12>)
595              
596             =item *
597              
598             C<draft-next>
599              
600             =back
601              
602             The default is C<latest>, but in the synopsis example, L<JSON::Schema> is testing draft 3
603             compliance.
604              
605             (For backwards compatibility, C<new> can be called with a single numeric argument of 3 to 7, which
606             maps to C<draft3> through C<draft7>.)
607              
608             =head2 test_dir
609              
610             Instead of specifying a draft specification to test against, which will select the most appropriate
611             tests, you can pass in the name of a directory of tests to run directly. Files in this directory
612             should be F<.json> files following the format described in
613             L<https://github.com/json-schema-org/JSON-Schema-Test-Suite/blob/master/README.md>.
614              
615             =head2 additional_resources
616              
617             A directory of additional resources which should be made available to the implementation under the
618             base URI C<http://localhost:1234>. This is automatically provided if you did not override
619             C</test_dir>; otherwise, you need to supply it yourself, if any tests require it (for example by
620             containing C<< {"$ref": "http://localhost:1234/foo.json/#a/b/c"} >>). If you supply an
621             L</add_resource> value to L</acceptance> (see below), this will be done for you.
622              
623             =head2 verbose
624              
625             Optional. When true, prints version information and test result table such that it is visible
626             during C<make test> or C<prove>.
627              
628             =head2 include_optional
629              
630             Optional. When true, tests in subdirectories (most notably F<optional/> are also included.
631              
632             =head2 skip_dir
633              
634             Optional. Pass a string or arrayref consisting of relative path name(s) to indicate directories
635             (within the test directory as specified above with C<specification> or C<test_dir>) which will be
636             skipped. Note that this is only useful currently with C<include_optional => 1>, as otherwise all
637             subdirectories would be skipped anyway.
638              
639             =head2 results
640              
641             After calling L</acceptance>, a list of test results are provided here. It is an arrayref of
642             hashrefs with four keys:
643              
644             =over 4
645              
646             =item *
647              
648             file - the filename
649              
650             =item *
651              
652             pass - the number of pass results for that file
653              
654             =item *
655              
656             todo_fail - the number of fail results for that file that were marked TODO
657              
658             =item *
659              
660             fail - the number of fail results for that file (not including TODO tests)
661              
662             =back
663              
664             =head2 results_text
665              
666             After calling L</acceptance>, a text string tabulating the test results are provided here. This is
667             the same table that is printed at the end of the test run.
668              
669             =head2 test_schemas
670              
671             =for stopwords metaschema
672              
673             Optional. A boolean that, when true, will test every schema against its
674             specification metaschema. (When set, C<specification> must also be set.)
675              
676             This normally should not be set as the official test suite has already been
677             sanity-tested, but you may want to set this in development environments if you
678             are using your own test files.
679              
680             Defaults to false.
681              
682             =head1 SUBROUTINES/METHODS
683              
684             =head2 acceptance
685              
686             =for stopwords truthy falsey
687              
688             Accepts a hash of options as its arguments.
689              
690             (Backwards-compatibility mode: accepts a subroutine which is used as C<validate_json_string>,
691             and a hashref of arguments.)
692              
693             Available options are:
694              
695             =head3 validate_data
696              
697             A subroutine reference, which is passed two arguments: the JSON Schema, and the B<inflated> data
698             structure to be validated.
699              
700             The subroutine should return truthy or falsey depending on if the schema was valid for the input or
701             not.
702              
703             Either C<validate_data> or C<validate_json_string> is required.
704              
705             =head3 validate_json_string
706              
707             A subroutine reference, which is passed two arguments: the JSON Schema, and the B<JSON string>
708             containing the data to be validated.
709              
710             The subroutine should return truthy or falsey depending on if the schema was valid for the input or
711             not.
712              
713             Either C<validate_data> or C<validate_json_string> is required.
714              
715             =head3 add_resource
716              
717             Optional. A subroutine reference, which will be called at the start of L</acceptance> multiple
718             times, with two arguments: a URI (string), and a data structure containing schema data to be
719             associated with that URI, for use in some tests that use additional resources (see above). If you do
720             not provide this option, you will be responsible for ensuring that those additional resources are
721             made available to your implementation for the successful execution of the tests that rely on them.
722              
723             For more information, see <https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.9.1.2>.
724              
725             =head3 tests
726              
727             Optional. Restricts tests to just those mentioned (the conditions are ANDed together, not ORed).
728             The syntax can take one of many forms:
729              
730             # run tests in this file
731             tests => { file => 'dependencies.json' }
732              
733             # run tests in these files
734             tests => { file => [ 'dependencies.json', 'refRemote.json' ] }
735              
736             # run tests in this file with this group description
737             tests => {
738             file => 'refRemote.json',
739             group_description => 'remote ref',
740             }
741              
742             # run tests in this file with these group descriptions
743             tests => {
744             file => 'const.json',
745             group_description => [ 'const validation', 'const with object' ],
746             }
747              
748             # run tests in this file with this group description and test description
749             tests => {
750             file => 'const.json',
751             group_description => 'const validation',
752             test_description => 'another type is invalid',
753             }
754              
755             # run tests in this file with this group description and these test descriptions
756             tests => {
757             file => 'const.json',
758             group_description => 'const validation',
759             test_description => [ 'same value is valid', 'another type is invalid' ],
760             }
761              
762             =head3 todo_tests
763              
764             Optional. Mentioned tests will run as L<"TODO"|Test::More/TODO: BLOCK>. Uses arrayrefs of
765             the same hashref structure as L</tests> above, which are ORed together.
766              
767             todo_tests => [
768             # all tests in this file are TODO
769             { file => 'dependencies.json' },
770             # just some tests in this file are TODO
771             { file => 'boolean_schema.json', test_description => 'array is invalid' },
772             # .. etc
773             ]
774              
775             =head1 ACKNOWLEDGEMENTS
776              
777             =for stopwords Perrett Signes
778              
779             Daniel Perrett <perrettdl@cpan.org> for the concept and help in design.
780              
781             Ricardo Signes <rjbs@cpan.org> for direction to and creation of Test::Fatal.
782              
783             Various others in #perl-help.
784              
785             =head1 SUPPORT
786              
787             Bugs may be submitted through L<https://github.com/karenetheridge/Test-JSON-Schema-Acceptance/issues>.
788              
789             =head1 AUTHOR
790              
791             Ben Hutton (@relequestual) <relequest@cpan.org>
792              
793             =head1 CONTRIBUTORS
794              
795             =for stopwords Karen Etheridge Daniel Perrett
796              
797             =over 4
798              
799             =item *
800              
801             Karen Etheridge <ether@cpan.org>
802              
803             =item *
804              
805             Daniel Perrett <dp13@sanger.ac.uk>
806              
807             =back
808              
809             =head1 COPYRIGHT AND LICENCE
810              
811             This software is Copyright (c) 2015 by Ben Hutton.
812              
813             This is free software, licensed under:
814              
815             The MIT (X11) License
816              
817             This distribution includes data from the L<https://json-schema.org> test suite, which carries its own
818             licence (see F<share/LICENSE>).
819              
820             =for Pod::Coverage BUILDARGS BUILD
821              
822             =cut