File Coverage

blib/lib/JSONSchema/Validator/OAS30.pm
Criterion Covered Total %
statement 236 248 95.1
branch 102 130 78.4
condition 38 59 64.4
subroutine 26 26 100.0
pod 5 6 83.3
total 407 469 86.7


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