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