File Coverage

lib/APISchema/Validator.pm
Criterion Covered Total %
statement 74 75 98.6
branch 17 18 94.4
condition 6 8 75.0
subroutine 18 18 100.0
pod 0 3 0.0
total 115 122 94.2


line stmt bran cond sub pod time code
1             package APISchema::Validator;
2 3     3   4101 use strict;
  3         8  
  3         100  
3 3     3   16 use warnings;
  3         6  
  3         82  
4 3     3   58 use 5.014;
  3         22  
5              
6             # cpan
7 3     3   413 use Class::Load qw(load_class);
  3         18382  
  3         193  
8             use Class::Accessor::Lite::Lazy (
9 3         24 ro => [qw(fetch_resource_method)],
10             ro_lazy => [qw(validator_class)],
11 3     3   906 );
  3         2794  
12              
13             # lib
14 3     3   417 use APISchema::Resource;
  3         5  
  3         65  
15 3     3   633 use APISchema::Validator::Decoder;
  3         6  
  3         90  
16 3     3   666 use APISchema::Validator::Result;
  3         6  
  3         184  
17              
18             use constant +{
19 3         1963 DEFAULT_VALIDATOR_CLASS => 'Valiemon',
20             TARGETS => [qw(header parameter body)],
21             DEFAULT_ENCODING_SPEC => {
22             'application/json' => 'json',
23             'application/x-www-form-urlencoded' => 'url_parameter',
24             # TODO yaml, xml
25             },
26 3     3   18 };
  3         4  
27              
28             sub _build_validator_class {
29 30     30   1103 return DEFAULT_VALIDATOR_CLASS;
30             }
31              
32             sub _new {
33 65     65   114 my $class = shift;
34 65 50 33     431 return bless { @_ == 1 && ref($_[0]) eq 'HASH' ? %{$_[0]} : @_ }, $class;
  0         0  
35             }
36              
37             sub for_request {
38 34     34 0 2822 my $class = shift;
39 34         115 return $class->_new(@_, fetch_resource_method => 'canonical_request_resource');
40             }
41              
42             sub for_response {
43 31     31 0 2799 my $class = shift;
44 31         114 return $class->_new(@_, fetch_resource_method => 'canonical_response_resource');
45             }
46              
47 94     94   594 sub _valid_result { APISchema::Validator::Result->new_valid(@_) }
48 40     40   181 sub _error_result { APISchema::Validator::Result->new_error(@_) }
49              
50             sub _resolve_encoding {
51 46     46   130 my ($content_type, $encoding_spec) = @_;
52             # TODO handle charset?
53 46         126 $content_type = $content_type =~ s/\s*;.*$//r;
54 46   100     174 $encoding_spec //= DEFAULT_ENCODING_SPEC;
55              
56 46 100       113 if (ref $encoding_spec) {
57 33         73 $encoding_spec = $encoding_spec->{$content_type};
58 33 100       111 return ( undef, { message => "Wrong content-type: $content_type" } )
59             unless $encoding_spec;
60             }
61              
62 38         69 my $method = $encoding_spec;
63             return ( undef, {
64 38 100       250 message => "Unknown decoding method: $method",
65             content_type => $content_type,
66             } )
67             unless APISchema::Validator::Decoder->new->can($method);
68              
69 36         398 return ($method, undef);
70             }
71              
72             sub _validate {
73 57     57   166 my ($validator_class, $decode, $target, $spec) = @_;
74              
75 57         100 my $obj = eval { APISchema::Validator::Decoder->new->$decode($target) };
  57         214  
76 57 100       1026 return { message => "Failed to parse $decode" } if $@;
77              
78 53         137 my $validator = $validator_class->new($spec->definition);
79 53         1045 my ($valid, $err) = $validator->validate($obj);
80              
81             return {
82 53 100       132050 attribute => $err->attribute,
83             position => $err->position,
84             expected => $err->expected,
85             actual => $err->actual,
86 26         518 message => "Contents do not match resource '@{[$spec->title]}'",
87             } unless $valid;
88              
89 27         147 return; # avoid returning the last conditional value
90             }
91              
92             sub validate {
93 67     67 0 17604 my ($self, $route_name, $target, $schema) = @_;
94              
95 67         108 my @target_keys = @{+TARGETS};
  67         217  
96 67         185 my $valid = _valid_result(@target_keys);
97              
98 67 100       629 my $route = $schema->get_route_by_name($route_name)
99             or return $valid;
100 65         225 my $method = $self->fetch_resource_method;
101 65         530 my $resource_root = $schema->get_resource_root;
102             my $resource_spec = $route->$method(
103             $resource_root,
104 65 100       864 $target->{status_code} ? [ $target->{status_code} ] : [],
105             [ @target_keys ],
106             );
107 65         732 @target_keys = grep { $resource_spec->{$_} } @target_keys;
  195         351  
108              
109 65   100     219 my $body_encoding = $resource_spec->{body} && do {
110             my ($enc, $err) = _resolve_encoding(
111             $target->{content_type} // '',
112             $resource_spec->{encoding},
113             );
114             if ($err) {
115             return _error_result(body => $err);
116             }
117             $enc;
118             };
119              
120 55         198 my $encoding = {
121             body => $body_encoding,
122             parameter => 'url_parameter',
123             header => 'perl',
124             };
125              
126 55         248 my $validator_class = $self->validator_class;
127 55         385 load_class $validator_class;
128 55         27130 my $result = APISchema::Validator::Result->new;
129 55         359 $result->merge($_) for map {
130 57         135 my $field = $_;
131 57         212 my $err = _validate($validator_class, map { $_->{$field} } (
  171         388  
132             $encoding, $target, $resource_spec,
133             ));
134             $err ? _error_result($field => {
135             %$err,
136 57 100       609 encoding => $encoding->{$_},
137             }) : _valid_result($field);
138             } @target_keys;
139              
140 55         507 return $result;
141             }
142              
143             1;
144             __END__