File Coverage

blib/lib/Test/JSON/Entails.pm
Criterion Covered Total %
statement 70 70 100.0
branch 26 28 92.8
condition 5 7 71.4
subroutine 10 10 100.0
pod 0 1 0.0
total 111 116 95.6


line stmt bran cond sub pod time code
1 4     4   77280 use strict;
  4         11  
  4         154  
2 4     4   23 use warnings;
  4         8  
  4         247  
3             package Test::JSON::Entails;
4             {
5             $Test::JSON::Entails::VERSION = '0.2';
6             }
7             #ABSTRACT: Test whether one JSON or Perl structure entails/subsumes another
8              
9              
10 4     4   33 use base 'Test::Builder::Module';
  4         14  
  4         581  
11             our @EXPORT = qw(entails subsumes);
12              
13 4     4   25 use Carp;
  4         6  
  4         416  
14 4     4   3921 use JSON::Any;
  4         109345  
  4         26  
15 4     4   34292 use Scalar::Util qw(reftype);
  4         11  
  4         3721  
16              
17             my $JSON = JSON::Any->new;
18              
19             sub entails ($$;$) {
20 18     18 0 4004 my ($input, $entailed, $test_name) = @_;
21 18         94 my $test = __PACKAGE__->builder;
22              
23 18 50 33     321 croak "usage: entails(input,entailed,test_name)"
24             unless defined $input && defined $entailed;
25              
26 18         29 my @objects;
27 18         67 foreach my $item ( [ input => $input ], [ entailed => $entailed ] ) {
28 33         52 my $object = $item->[1];
29 33 100       79 unless ( ref $object ) {
30 5         9 $object= eval { $JSON->decode( $object ) };
  5         21  
31 5 100       162 if ( my $error = $@ ) {
32 2         9 $test->ok( 0, $test_name );
33 2         1476 $test->diag("$item->[0] was not valid JSON");
34 2         169 return;
35             }
36             }
37 31 100       116 if ( reftype $object ne 'HASH' ) {
38 4         15 $test->ok( 0, $test_name );
39 4         3625 $test->diag("$item->[0] was not JSON object or HASH reference");
40 4         334 return;
41             }
42 27         66 push @objects, $object;
43             }
44            
45 12         33 ($input, $entailed) = @objects;
46              
47 12         33 my $error = _hash_entails( @objects, "/" );
48 12 100       28 if ($error) {
49 7         24 $test->ok(0, $test_name);
50 7         4414 $test->diag($error);
51             } else {
52 5         21 $test->ok(1, $test_name);
53             }
54             }
55              
56             sub _hash_entails {
57 17     17   24 my ($input, $entailed, $path) = @_;
58              
59 17         48 foreach my $k ( keys %$entailed ) {
60 17 100       43 if (!exists $input->{$k}) {
61 2         11 return "missing $path$k";
62             }
63 15         73 my $error =_deep_entails( $input->{$k}, $entailed->{$k}, $path.$k );
64 15 100       67 return $error if $error;
65             }
66              
67 8         19 return;
68             }
69              
70             *subsumes = *entails;
71              
72             sub _array_entails {
73 4     4   39 my ($got, $expect, $path) = @_;
74              
75             # TODO: compare unordered?
76 4         9 my $g = scalar @$got;
77 4         7 my $e = scalar @$expect;
78              
79 4 100       9 if ($e > $g) {
80 1         5 return "$path\[" . ($e - $g + 1) . '] missing';
81             }
82            
83 3         9 for(my $i=0; $i<$e; $i++) {
84 4         22 my $error = _deep_entails( $got->[$i], $expect->[$i], "$path\[".($i+1)."]" );
85 4 100       15 return $error if $error;
86             }
87              
88 2         5 return;
89             }
90              
91             sub _deep_entails {
92 19     19   31 my ($got, $expect, $path) = @_;
93              
94 19   100     81 my $type = lc(reftype($expect) || "scalar");
95 19   100     71 my $intype = lc(reftype($got) || "scalar");
96              
97 19 100       41 if ($intype ne $type) {
98 2         20 return "$path must be $type, found $intype";
99             }
100              
101 17         18 my $error;
102 17 100       47 if ($type eq 'scalar') {
    100          
    50          
103             # TODO: comparision may be overloaded, do we want to use _unoverload_str instead?
104 8 100       26 if ( $got ne $expect ) {
105 2         12 return "$path differ:\n got: '$got'\n expected: '$expect'";
106             }
107             } elsif ($type eq 'array') {
108 4         19 $error = _array_entails( $got, $expect, $path );
109             } elsif ($type eq 'hash') {
110 5         15 $error = _hash_entails( $got, $expect, "$path/" );
111             }
112              
113 15         32 return $error;
114             }
115              
116             1;
117              
118              
119             __END__