File Coverage

blib/lib/JSONSchema/Validator/OAS30.pm
Criterion Covered Total %
statement 234 246 95.1
branch 102 132 77.2
condition 38 59 64.4
subroutine 26 26 100.0
pod 5 6 83.3
total 405 469 86.3


line stmt bran cond sub pod time code
1             package JSONSchema::Validator::OAS30;
2              
3             # ABSTRACT: Validator for OpenAPI Specification 3.0
4              
5 6     6   45 use strict;
  6         18  
  6         186  
6 6     6   31 use warnings;
  6         12  
  6         185  
7 6     6   37 use Carp 'croak';
  6         13  
  6         284  
8              
9 6     6   45 use JSONSchema::Validator::JSONPointer;
  6         24  
  6         677  
10 6     6   49 use JSONSchema::Validator::Error 'error';
  6         15  
  6         253  
11 6     6   2743 use JSONSchema::Validator::Constraints::OAS30;
  6         19  
  6         255  
12 6     6   42 use JSONSchema::Validator::URIResolver;
  6         32  
  6         145  
13 6     6   29 use JSONSchema::Validator::Util 'json_decode';
  6         10  
  6         270  
14              
15 6     6   29 use parent 'JSONSchema::Validator::Draft4';
  6         14  
  6         25  
16              
17 6     6   372 use constant SPECIFICATION => 'OAS30';
  6         13  
  6         415  
18 6     6   38 use constant ID => 'https://spec.openapis.org/oas/3.0/schema/2019-04-02';
  6         30  
  6         339  
19 6     6   39 use constant ID_FIELD => '';
  6         12  
  6         16867  
20              
21             sub new {
22 39     39 1 45454 my ($class, %params) = @_;
23              
24 39         114 $params{using_id_with_ref} = 0;
25 39         216 my $self = $class->create(%params);
26              
27 39   100     159 my $validate_deprecated = $params{validate_deprecated} // 1;
28 39         88 $self->{validate_deprecated} = $validate_deprecated;
29              
30 39   50     260 my $constraints = JSONSchema::Validator::Constraints::OAS30->new(validator => $self, strict => $params{strict} // 0);
31 39         128 $self->{constraints} = $constraints;
32              
33 39         131 return $self;
34             }
35              
36 9     9 1 102 sub validate_deprecated { shift->{validate_deprecated} }
37              
38             sub validate_schema {
39 110     110 1 656 my ($self, $instance, %params) = @_;
40              
41 110   66     334 my $schema = $params{schema} // $self->schema;
42 110   50     338 my $instance_path = $params{instance_path} // '/';
43 110   50     361 my $schema_path = $params{schema_path} // '/';
44 110         172 my $direction = $params{direction};
45 110         170 my $scope = $params{scope};
46              
47 110 50       238 croak 'param "direction" is required' unless $direction;
48 110 50 66     330 croak '"direction" must have one of values: "request", "response"'
49             if $direction ne 'request' && $direction ne 'response';
50              
51 110 50       214 croak 'No schema specified' unless defined $schema;
52              
53 110 100       222 push @{$self->scopes}, $scope if $scope;
  32         78  
54              
55 110         1486 my ($errors, $warnings) = ([], []);
56 110         517 my $result = $self->_validate_schema($instance, $schema, $instance_path, $schema_path, {
57             errors => $errors,
58             warnings => $warnings,
59             direction => $direction
60             }
61             );
62              
63 110 100       343 pop @{$self->scopes} if $scope;
  32         77  
64              
65 110         587 return $result, $errors, $warnings;
66             }
67              
68             sub _schema_keys {
69 499     499   1055 my ($self, $schema, $instance_path, $data) = @_;
70             # if ref exists other preperties MUST be ignored
71 499 100       1187 return '$ref' if $schema->{'$ref'};
72              
73 437 100 100     935 return ('deprecated') if $schema->{deprecated} && !$self->validate_deprecated;
74              
75 435 100       1131 if (grep { $_ eq 'discriminator' } keys %$schema) {
  900         2153  
76 52   100     223 my $status = $data->{discriminator}{$instance_path} // 0;
77 52 100       175 return ('discriminator') unless $status;
78              
79             # status is 1
80 24         60 return grep { $_ ne 'discriminator' } keys %$schema;
  96         227  
81             }
82              
83 383         1217 return keys %$schema;
84             }
85              
86             sub validate_request {
87 21     21 1 28952 my ($self, %params) = @_;
88              
89 21   33     82 my $method = lc($params{method} or croak 'param "method" is required');
90 21 50       53 my $openapi_path = $params{openapi_path} or croak 'param "openapi_path" is required';
91              
92 21         62 my $get_user_param = $self->_wrap_params($params{parameters});
93              
94 21   50     60 my $user_body = $params{parameters}{body} // []; # [exists, content-type, value]
95              
96 21         49 my $base_ptr = $self->json_pointer->xget('paths', $openapi_path);
97 21 50       89 return 1, [], [] unless $base_ptr;
98              
99 21         75 my $schema_params = {query => {}, header => {}, path => {}, cookie => {}};
100              
101             # Common Parameter Object
102 21         63 my $common_params_ptr = $base_ptr->xget('parameters');
103 21         68 $self->_fill_parameters($schema_params, $common_params_ptr);
104              
105             # Operation Object
106 21         52 my $operation_ptr = $base_ptr->xget($method);
107 21 50       52 return 1, [], [] unless $operation_ptr;
108              
109 21         98 my ($result, $context) = (1, {errors => [], warnings => [], direction => 'request'});
110 21 100       51 if ($operation_ptr->xget('deprecated')) {
111 1         8 push @{$context->{warnings}}, error(message => "method $method of $openapi_path is deprecated");
  1         9  
112 1 50       7 return $result, $context->{errors}, $context->{warnings} unless $self->validate_deprecated;
113             }
114              
115             # Parameter Object
116 20         65 my $params_ptr = $operation_ptr->xget('parameters');
117 20         74 $self->_fill_parameters($schema_params, $params_ptr);
118              
119             # validate path, query, header, cookie
120 20         65 my $r = $self->_validate_params($context, $schema_params, $get_user_param);
121 20 100       54 $result = 0 unless $r;
122              
123             # validate body
124 20         50 my $body_ptr = $operation_ptr->xget('requestBody');
125 20         53 $r = $self->_validate_body($context, $user_body, $body_ptr);
126 20 100       58 $result = 0 unless $r;
127              
128 20         282 return $result, $context->{errors}, $context->{warnings};
129             }
130              
131             sub validate_response {
132 21     21 1 28956 my ($self, %params) = @_;
133              
134 21   33     86 my $method = lc($params{method} or croak 'param "method" is required');
135 21 50       59 my $openapi_path = $params{openapi_path} or croak 'param "openapi_path" is required';
136 21 50       50 my $http_status = $params{status} or croak 'param "status" is required';
137              
138 21         62 my $get_user_param = $self->_wrap_params($params{parameters});
139              
140 21   50     60 my $user_body = $params{parameters}{body} // []; # [exists, content-type, value]
141              
142 21         53 my $base_ptr = $self->json_pointer->xget('paths', $openapi_path, $method);
143 21 50       82 return 1, [], [] unless $base_ptr;
144              
145 21         1094 my ($result, $context) = (1, {errors => [], warnings => [], direction => 'response'});
146 21 100       57 if ($base_ptr->xget('deprecated')) {
147 1         11 push @{$context->{warnings}}, error(message => "method $method of $openapi_path is deprecated");
  1         8  
148 1 50       6 return $result, $context->{errors}, $context->{warnings} unless $self->validate_deprecated;
149             }
150              
151 20         61 my $responses_ptr = $base_ptr->xget('responses');
152 20 50       44 return $result, $context->{errors}, $context->{warnings} unless $responses_ptr;
153              
154 20         43 my $status_ptr = $responses_ptr->xget($http_status);
155 20 100       43 $status_ptr = $responses_ptr->xget('default') unless $status_ptr;
156              
157 20 50       44 unless ($status_ptr) {
158 0         0 push @{$context->{errors}}, error(message => "unspecified response with status code $http_status");
  0         0  
159 0         0 return 0, $context->{errors}, $context->{warnings};
160             }
161              
162 20         49 my $schema_params = {header => {}};
163 20         47 $self->_fill_parameters($schema_params, $status_ptr->xget('headers'));
164              
165             # validate headers
166 20         61 my $r = $self->_validate_params($context, $schema_params, $get_user_param);
167 20 50       43 $result = 0 unless $r;
168              
169             # validate body
170 20         46 my ($exists, $content_type, $data) = @$user_body;
171 20 100       68 return $result, $context->{errors}, $context->{warnings} unless $exists;
172              
173 15         34 ($r, my $errors, my $warnings) = $self->_validate_content($context, $status_ptr, $content_type, $data);
174 15 100       52 unless ($r) {
175 7         13 push @{$context->{errors}}, error(message => 'response body error', context => $errors);
  7         30  
176 7         13 $result = 0;
177             }
178 15 50       40 push @{$context->{warnings}}, error(message => 'response body warning', context => $warnings) if @$warnings;
  0         0  
179              
180 15         132 return $result, $context->{errors}, $context->{warnings};
181             }
182              
183             sub _validate_body {
184 20     20   44 my ($self, $ctx, $user_body, $body_ptr) = @_;
185              
186             # body in specification is omit
187 20 100       52 return 1 unless $body_ptr;
188              
189             # skip validation if user not specify body
190 14 50 33     52 return 1 unless $user_body && @$user_body;
191              
192 14         30 my ($exists, $content_type, $data) = @$user_body;
193              
194 14 100       28 unless ($exists) {
195 1         5 my $required = $body_ptr->xget('required');
196 1 50       4 if ($required) {
197 0         0 push @{$ctx->{errors}}, error(message => q{body is required});
  0         0  
198 0         0 return 0;
199             }
200 1         13 return 1;
201             }
202              
203 13         35 my ($result, $errors, $warnings) = $self->_validate_content($ctx, $body_ptr, $content_type, $data);
204 13 100       38 push @{$ctx->{errors}}, error(message => 'request body error', context => $errors) if @$errors;
  4         23  
205 13 50       30 push @{$ctx->{warnings}}, error(message => 'request body warning', context => $warnings) if @$warnings;
  0         0  
206 13         35 return $result;
207             }
208              
209             sub _validate_params {
210 40     40   92 my ($self, $ctx, $schema_params, $get_user_param) = @_;
211 40         60 my $result = 1;
212 40         131 for my $type (keys %$schema_params) {
213 100 100       142 next unless %{$schema_params->{$type}};
  100         247  
214             # skip validation if user not specify getter for such params type
215 45 100       125 next unless $get_user_param->{$type};
216 39         101 my $r = $self->_validate_type_params($ctx, $type, $schema_params->{$type}, $get_user_param->{$type});
217 39 100       98 $result = 0 unless $r;
218             }
219 40         97 return $result;
220             }
221              
222             sub _validate_type_params {
223 39     39   88 my ($self, $ctx, $type, $params, $get_user_param) = @_;
224              
225 39         63 my $result = 1;
226 39         108 for my $param (keys %$params) {
227 66         107 my $data_ptr = $params->{$param};
228              
229 66         137 my ($exists, $value) = $get_user_param->($param);
230 66 100 100     230 ($exists, $value) = $get_user_param->(lc $param) if !$exists && $type eq 'header';
231              
232 66 100       133 unless ($exists) {
233 17 100       40 if ($data_ptr->xget('required')) {
234 2         10 push @{$ctx->{errors}}, error(message => qq{$type param "$param" is required});
  2         12  
235 2         5 $result = 0;
236             }
237 17         50 next;
238             }
239              
240 49 100       123 if ($data_ptr->xget('deprecated')) {
241 1         5 push @{$ctx->{warnings}}, error(message => qq{$type param "$param" is deprecated});
  1         8  
242 1 50       4 next unless $self->validate_deprecated;
243             }
244              
245 48 50 66     154 next unless $data_ptr->xget('schema') || $data_ptr->xget('content');
246              
247 48 50       183 $value = [$value] if ref $value ne 'ARRAY';
248              
249 48         98 for my $v (@$value) {
250 48         86 my ($r, $errors, $warnings);
251              
252 48 100       113 if ($data_ptr->xget('schema')) {
    50          
253 39         99 my $schema_ptr = $data_ptr->xget('schema');
254             ($r, $errors, $warnings) = $self->validate_schema($v,
255             schema => $schema_ptr->value,
256             path => '/',
257             direction => $ctx->{direction},
258 39         99 scope => $schema_ptr->scope
259             );
260             } elsif ($data_ptr->xget('content')) {
261 9         28 ($r, $errors, $warnings) = $self->_validate_content($ctx, $data_ptr, undef, $v);
262             }
263              
264 48 100       143 unless ($r) {
265 5         12 push @{$ctx->{errors}}, error(message => qq{$type param "$param" has error}, context => $errors);
  5         32  
266 5         12 $result = 0;
267             }
268 48 50       170 push @{$ctx->{warnings}}, error(message => qq{$type param "$param" has warning}, context => $warnings) if @$warnings;
  0         0  
269             }
270             }
271              
272 39         84 return $result;
273             }
274              
275             # ptr - JSONSchema::Validator::JSONPointer
276             # content_type - string|null
277             # data - string|HASH|ARRAY
278             sub _validate_content {
279 37     37   81 my ($self, $ctx, $ptr, $content_type, $data) = @_;
280              
281 37         95 my $content_ptr = $ptr->xget('content');
282             # content in body is required but in params is optional
283 37 100       92 return 1, [], [] unless $content_ptr;
284              
285 36         59 my $ctype_ptr;
286 36 100       72 if ($content_type) {
287 27         55 $ctype_ptr = $content_ptr->xget($content_type);
288 27 50       66 unless ($ctype_ptr) {
289 0         0 return 0, [error(message => qq{content with content-type $content_type is omit in schema})], [];
290             }
291             } else {
292 9         28 my $mtype_map = $content_ptr->value;
293 9         30 my @keys = $content_ptr->keys(raw => 1);
294 9 50       23 return 0, [error(message => qq{schema must has exactly one content_type})], [] unless scalar(@keys) == 1;
295              
296 9         16 $content_type = $keys[0];
297 9         30 $ctype_ptr = $content_ptr->xget($content_type);
298             }
299              
300 36 100       90 unless (ref $data) {
301 9 50       40 if (index($content_type, 'application/json') != -1) {
302 9         14 eval { $data = json_decode($data); };
  9         28  
303             }
304             # do we need to support other content-type?
305             }
306              
307 36         82 my $schema_ptr = $ctype_ptr->xget('schema');
308 36         99 my $schema_prop_ptr = $schema_ptr->xget('properties');
309              
310 36 100 66     98 if (
      66        
      100        
      66        
311             $schema_prop_ptr &&
312             $content_type &&
313             (
314             index($content_type ,'application/x-www-form-urlencoded') != -1 ||
315             index($content_type, 'multipart/') != -1
316             ) &&
317             ref $data eq 'HASH'
318             ) {
319 5         21 for my $property_name ($schema_prop_ptr->keys(raw => 1)) {
320 12         31 my $property_ctype_ptr = $ctype_ptr->xget('encoding', $property_name, 'contentType');
321 12 100       40 my $property_ctype = $property_ctype_ptr ? $property_ctype_ptr->value : '';
322 12 100       28 unless ($property_ctype) {
323 9         21 my $prop_type_ptr = $schema_prop_ptr->xget($property_name, 'type');
324 9 100 66     27 $property_ctype = $prop_type_ptr && $prop_type_ptr->value eq 'object' ? 'application/json' : '';
325             }
326              
327 12 50 66     56 if (
      33        
328             index($property_ctype, 'application/json') != -1 &&
329             exists $data->{$property_name} &&
330             !ref $data->{$property_name}
331             ) {
332 5         11 eval {
333 5         17 $data->{$property_name} = json_decode($data->{$property_name});
334             };
335             }
336             # do we need to support other content-type?
337             }
338             }
339              
340             return $self->validate_schema($data,
341             schema => $schema_ptr->value,
342             path => '/',
343             direction => $ctx->{direction},
344 36         102 scope => $schema_ptr->scope
345             );
346             }
347              
348             sub _fill_parameters {
349 61     61   121 my ($self, $hash, $ptr) = @_;
350 61 100       131 return unless ref $ptr->value;
351              
352 37 100       80 if (ref $ptr->value eq 'ARRAY') {
    50          
353 25         73 for my $p ($ptr->keys) {
354 61         152 my $param_ptr = $ptr->get($p);
355 61         137 my $param = $param_ptr->value;
356              
357 61         136 my ($name, $in) = @$param{qw/name in/};
358              
359 61         192 $hash->{$in}{$name} = $param_ptr;
360             }
361             } elsif (ref $ptr->value eq 'HASH') {
362             # currently used for headers in response
363 12         21 my $in = 'header';
364 12         35 for my $name ($ptr->keys(raw => 1)) {
365 12         30 my $param_ptr = $ptr->xget($name);
366 12         41 $hash->{$in}{$name} = $param_ptr;
367             }
368             }
369             }
370              
371             sub _wrap_params {
372 42     42   87 my ($self, $parameters) = @_;
373              
374 42         70 my $get_user_param = {};
375 42         103 for my $type (qw/path query header cookie/) {
376 168 100       384 next unless $parameters->{$type};
377              
378 69 50       200 if (ref $parameters->{$type} eq 'CODE') {
    50          
379 0         0 $get_user_param->{$type} = $parameters->{$type};
380             } elsif (ref $parameters->{$type} eq 'HASH') {
381 69         110 my $data = $parameters->{$type};
382 69 100       200 $data = +{ map { lc $_ => $data->{$_} } keys %$data } if $type eq 'header';
  14         81  
383             $get_user_param->{$type} = sub {
384 75     75   122 my $param = shift;
385 75         231 return (exists($data->{$param}), $data->{$param});
386             }
387 69         264 } else {
388 0         0 croak qq{param "$type" must be hashref or coderef};
389             }
390             }
391              
392 42         82 return $get_user_param;
393             }
394              
395             sub json_pointer {
396 42     42 0 69 my $self = shift;
397 42         144 return JSONSchema::Validator::JSONPointer->new(
398             scope => $self->scope,
399             value => $self->schema,
400             validator => $self
401             );
402             }
403              
404             1;
405              
406             __END__