File Coverage

blib/lib/OpenAPI/Modern.pm
Criterion Covered Total %
statement 324 330 98.1
branch 146 158 92.4
condition 58 81 71.6
subroutine 46 46 100.0
pod 3 3 100.0
total 577 618 93.3


line stmt bran cond sub pod time code
1 5     5   614645 use strict;
  5         53  
  5         142  
2 5     5   27 use warnings;
  5         15  
  5         261  
3             package OpenAPI::Modern; # git description: v0.044-4-gcba9f81
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Validate HTTP requests and responses against an OpenAPI document
6             # KEYWORDS: validation evaluation JSON Schema OpenAPI Swagger HTTP request response
7              
8             our $VERSION = '0.045';
9              
10 5     5   129 use 5.020;
  5         20  
11 5     5   2252 use Moo;
  5         28200  
  5         25  
12 5     5   6529 use strictures 2;
  5         72  
  5         241  
13 5     5   1081 use experimental qw(signatures postderef);
  5         19  
  5         71  
14 5     5   1035 use if "$]" >= 5.022, experimental => 're_strict';
  5         11  
  5         67  
15 5     5   459 no if "$]" >= 5.031009, feature => 'indirect';
  5         20  
  5         49  
16 5     5   258 no if "$]" >= 5.033001, feature => 'multidimensional';
  5         17  
  5         27  
17 5     5   240 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  5         17  
  5         32  
18 5     5   173 use Carp 'croak';
  5         20  
  5         297  
19 5     5   1970 use Safe::Isa;
  5         1877  
  5         672  
20 5     5   2059 use Ref::Util qw(is_plain_hashref is_plain_arrayref is_ref);
  5         6873  
  5         383  
21 5     5   49 use List::Util 'first';
  5         15  
  5         341  
22 5     5   32 use Scalar::Util 'looks_like_number';
  5         12  
  5         268  
23 5     5   1919 use Feature::Compat::Try;
  5         1373  
  5         21  
24 5     5   10599 use Encode 2.89;
  5         93  
  5         417  
25 5     5   2431 use URI::Escape ();
  5         8194  
  5         197  
26 5     5   3583 use JSON::Schema::Modern 0.557;
  5         3063319  
  5         329  
27 5     5   51 use JSON::Schema::Modern::Utilities 0.531 qw(jsonp unjsonp canonical_uri E abort is_equal is_elements_unique);
  5         88  
  5         406  
28 5     5   2127 use JSON::Schema::Modern::Document::OpenAPI;
  5         15  
  5         90  
29 5     5   205 use MooX::HandlesVia;
  5         10  
  5         41  
30 5     5   601 use MooX::TypeTiny 0.002002;
  5         95  
  5         37  
31 5     5   3896 use Types::Standard 'InstanceOf';
  5         18  
  5         25  
32 5     5   7620 use constant { true => JSON::PP::true, false => JSON::PP::false };
  5         13  
  5         36  
33 5     5   3311 use Mojo::Message::Request;
  5         398350  
  5         55  
34 5     5   2796 use Mojo::Message::Response;
  5         37155  
  5         67  
35 5     5   203 use namespace::clean;
  5         12  
  5         82  
36              
37             has openapi_document => (
38             is => 'ro',
39             isa => InstanceOf['JSON::Schema::Modern::Document::OpenAPI'],
40             required => 1,
41             handles => {
42             openapi_uri => 'canonical_uri', # Mojo::URL
43             openapi_schema => 'schema', # hashref
44             document_get => 'get', # data access using a json pointer
45             },
46             );
47              
48             # held separately because $document->evaluator is a weak ref
49             has evaluator => (
50             is => 'ro',
51             isa => InstanceOf['JSON::Schema::Modern'],
52             required => 1,
53             handles => [ qw(get_media_type add_media_type) ],
54             );
55              
56             around BUILDARGS => sub ($orig, $class, @args) {
57             my $args = $class->$orig(@args);
58              
59             if (exists $args->{openapi_document}) {
60             $args->{evaluator} = $args->{openapi_document}->evaluator;
61             }
62             else {
63             # construct document out of openapi_uri, openapi_schema, evaluator, if provided.
64             croak 'missing required constructor arguments: either openapi_document, or openapi_uri'
65             if not exists $args->{openapi_uri};
66             croak 'missing required constructor arguments: either openapi_document, or openapi_schema'
67             if not exists $args->{openapi_schema};
68              
69             $args->{evaluator} //= JSON::Schema::Modern->new(validate_formats => 1, max_traversal_depth => 80);
70             $args->{openapi_document} = JSON::Schema::Modern::Document::OpenAPI->new(
71             canonical_uri => $args->{openapi_uri},
72             schema => $args->{openapi_schema},
73             evaluator => $args->{evaluator},
74             );
75              
76             # if there were errors, this will die with a JSON::Schema::Modern::Result object
77             $args->{evaluator}->add_schema($args->{openapi_document});
78             }
79              
80             return $args;
81             };
82              
83 121     121 1 396001 sub validate_request ($self, $request, $options = {}) {
  121         251  
  121         220  
  121         283  
  121         207  
84 121         3151 my $state = {
85             data_path => '/request',
86             initial_schema_uri => $self->openapi_uri, # the canonical URI as of the start or last $id, or the last traversed $ref
87             traversed_schema_path => '', # the accumulated traversal path as of the start, or last $id, or up to the last traversed $ref
88             schema_path => '', # the rest of the path, since the last $id or the last traversed $ref
89             effective_base_uri => Mojo::URL->new->scheme('https')->host($request->headers->header('Host')),
90             annotations => [],
91             };
92              
93             try {
94             croak '$request and $options->{request} are inconsistent'
95             if $request and $options->{request} and $request != $options->{request};
96             $options->{request} //= $request;
97             my $path_ok = $self->find_path($options);
98             $request = $options->{request}; # now guaranteed to be a Mojo::Message::Request
99             $state->{errors} = delete $options->{errors};
100             return $self->_result($state, 1) if not $path_ok;
101              
102             my ($path_template, $path_captures) = $options->@{qw(path_template path_captures)};
103             my $path_item = $self->openapi_document->schema->{paths}{$path_template};
104             my $method = lc $request->method;
105             my $operation = $path_item->{$method};
106              
107             $state->{schema_path} = jsonp('/paths', $path_template);
108              
109             # PARAMETERS
110             # { $in => { $name => 'path-item'|$method } } as we process each one.
111             my $request_parameters_processed;
112              
113             # first, consider parameters at the operation level.
114             # parameters at the path-item level are also considered, if not already seen at the operation level
115             foreach my $section ($method, 'path-item') {
116             foreach my $idx (0 .. (($section eq $method ? $operation : $path_item)->{parameters}//[])->$#*) {
117             my $state = { %$state, schema_path => jsonp($state->{schema_path},
118             ($section eq $method ? $method : ()), 'parameters', $idx) };
119             my $param_obj = ($section eq $method ? $operation : $path_item)->{parameters}[$idx];
120             while (my $ref = $param_obj->{'$ref'}) {
121             $param_obj = $self->_resolve_ref('parameter', $ref, $state);
122             }
123              
124             my $fc_name = $param_obj->{in} eq 'header' ? fc($param_obj->{name}) : $param_obj->{name};
125              
126             abort($state, 'duplicate %s parameter "%s"', $param_obj->{in}, $param_obj->{name})
127             if ($request_parameters_processed->{$param_obj->{in}}{$fc_name} // '') eq $section;
128             next if exists $request_parameters_processed->{$param_obj->{in}}{$fc_name};
129             $request_parameters_processed->{$param_obj->{in}}{$fc_name} = $section;
130              
131             $state->{data_path} = jsonp($state->{data_path},
132             ((grep $param_obj->{in} eq $_, qw(path query)) ? 'uri' : ()), $param_obj->{in},
133             $param_obj->{name});
134             my $valid =
135             $param_obj->{in} eq 'path' ? $self->_validate_path_parameter($state, $param_obj, $path_captures)
136             : $param_obj->{in} eq 'query' ? $self->_validate_query_parameter($state, $param_obj, $request->url)
137             : $param_obj->{in} eq 'header' ? $self->_validate_header_parameter($state, $param_obj->{name}, $param_obj, [ $request->headers->header($param_obj->{name}) // () ])
138             : $param_obj->{in} eq 'cookie' ? $self->_validate_cookie_parameter($state, $param_obj, $request)
139             : abort($state, 'unrecognized "in" value "%s"', $param_obj->{in});
140             }
141             }
142              
143             # 3.2 "Each template expression in the path MUST correspond to a path parameter that is included in
144             # the Path Item itself and/or in each of the Path Item’s Operations."
145             foreach my $path_name (sort keys $path_captures->%*) {
146             abort({ %$state, data_path => jsonp($state->{data_path}, qw(uri path), $path_name) },
147             'missing path parameter specification for "%s"', $path_name)
148             if not exists $request_parameters_processed->{path}{$path_name};
149             }
150              
151             $state->{data_path} = jsonp($state->{data_path}, 'body');
152             $state->{schema_path} = jsonp($state->{schema_path}, $method);
153              
154             if (my $body_obj = $operation->{requestBody}) {
155             $state->{schema_path} = jsonp($state->{schema_path}, 'requestBody');
156              
157             while (my $ref = $body_obj->{'$ref'}) {
158             $body_obj = $self->_resolve_ref('request-body', $ref, $state);
159             }
160              
161             if ($request->headers->content_length // $request->body_size) {
162             $self->_validate_body_content($state, $body_obj->{content}, $request);
163             }
164             elsif ($body_obj->{required}) {
165             ()= E({ %$state, keyword => 'required' }, 'request body is required but missing');
166             }
167             }
168             else {
169             ()= E($state, 'unspecified body is present in %s request', uc $method)
170             if ($method eq 'get' or $method eq 'head')
171             and $request->headers->content_length // $request->body_size;
172             }
173             }
174             catch ($e) {
175             if ($e->$_isa('JSON::Schema::Modern::Result')) {
176             return $e;
177             }
178             elsif ($e->$_isa('JSON::Schema::Modern::Error')) {
179             push @{$state->{errors}}, $e;
180             }
181             else {
182             ()= E($state, 'EXCEPTION: '.$e);
183             }
184             }
185              
186 121         13718 return $self->_result($state);
  118         26400  
187             }
188              
189 60     60 1 212026 sub validate_response ($self, $response, $options = {}) {
  60         146  
  60         112  
  60         108  
  60         110  
190             # handle the existence of HTTP::Response::request
191 60 100       215 if (my $request = $response->$_call_if_can('request')) {
192             croak '$response->request and $options->{request} are inconsistent'
193 2 50 33     75 if $request and $options->{request} and $request != $options->{request};
      33        
194 2   33     11 $options->{request} //= $request;
195             }
196              
197 60         3158 my $state = {
198             data_path => '/response',
199             initial_schema_uri => $self->openapi_uri, # the canonical URI as of the start or last $id, or the last traversed $ref
200             traversed_schema_path => '', # the accumulated traversal path as of the start, or last $id, or up to the last traversed $ref
201             schema_path => '', # the rest of the path, since the last $id or the last traversed $ref
202             annotations => [],
203             };
204              
205             try {
206             my $path_ok = $self->find_path($options);
207             $state->{errors} = delete $options->{errors};
208             return $self->_result($state, 1) if not $path_ok;
209              
210             my ($path_template, $path_captures) = $options->@{qw(path_template path_captures)};
211             my $method = lc $options->{method};
212             my $operation = $self->openapi_document->schema->{paths}{$path_template}{$method};
213              
214             return $self->_result($state) if not exists $operation->{responses};
215              
216             $state->{effective_base_uri} = Mojo::URL->new->scheme('https')->host($options->{request}->headers->host)
217             if $options->{request};
218             $state->{schema_path} = jsonp('/paths', $path_template, $method);
219              
220             $response = _convert_response($response); # now guaranteed to be a Mojo::Message::Response
221              
222             if (my $error = $response->error) {
223             ()= E($state, $response->error->{message});
224             return $self->_result($state, 1);
225             }
226              
227 106     106   773 my $response_name = first { exists $operation->{responses}{$_} }
228             $response->code, substr(sprintf('%03s', $response->code), 0, -2).'XX', 'default';
229              
230             if (not $response_name) {
231             ()= E({ %$state, keyword => 'responses' }, 'no response object found for code %s', $response->code);
232             return $self->_result($state);
233             }
234              
235             my $response_obj = $operation->{responses}{$response_name};
236             $state->{schema_path} = jsonp($state->{schema_path}, 'responses', $response_name);
237             while (my $ref = $response_obj->{'$ref'}) {
238             $response_obj = $self->_resolve_ref('response', $ref, $state);
239             }
240              
241             foreach my $header_name (sort keys(($response_obj->{headers}//{})->%*)) {
242             next if fc $header_name eq fc 'Content-Type';
243             my $state = { %$state, schema_path => jsonp($state->{schema_path}, 'headers', $header_name) };
244             my $header_obj = $response_obj->{headers}{$header_name};
245             while (my $ref = $header_obj->{'$ref'}) {
246             $header_obj = $self->_resolve_ref('header', $ref, $state);
247             }
248              
249             ()= $self->_validate_header_parameter({ %$state,
250             data_path => jsonp($state->{data_path}, 'header', $header_name) },
251             $header_name, $header_obj, [ $response->headers->header($header_name) // () ]);
252             }
253              
254             $self->_validate_body_content({ %$state, data_path => jsonp($state->{data_path}, 'body') },
255             $response_obj->{content}, $response)
256             if exists $response_obj->{content} and $response->headers->content_length // $response->body_size;
257             }
258             catch ($e) {
259             if ($e->$_isa('JSON::Schema::Modern::Result')) {
260             return $e;
261             }
262             elsif ($e->$_isa('JSON::Schema::Modern::Error')) {
263             push @{$state->{errors}}, $e;
264             }
265             else {
266             ()= E($state, 'EXCEPTION: '.$e);
267             }
268             }
269              
270 60         3444 return $self->_result($state);
  46         7546  
271             }
272              
273 246     246 1 307742 sub find_path ($self, $options) {
  246         461  
  246         432  
  246         393  
274             # now guaranteed to be a Mojo::Message::Request
275 246 100       1246 $options->{request} = _convert_request($options->{request}) if $options->{request};
276              
277             my $state = {
278             data_path => '/request/uri/path',
279             initial_schema_uri => $self->openapi_uri, # the canonical URI as of the start or last $id, or the last traversed $ref
280             traversed_schema_path => '', # the accumulated traversal path as of the start, or last $id, or up to the last traversed $ref
281             schema_path => '', # the rest of the path, since the last $id or the last traversed $ref
282             errors => $options->{errors} //= [],
283 246 100 50     5519 $options->{request} ? ( effective_base_uri => Mojo::URL->new->scheme('https')->host($options->{request}->headers->host) ) : (),
284             };
285              
286 246 100 100     20744 if ($options->{request} and my $error = $options->{request}->error) {
287 3         53 ()= E({ %$state, data_path => '/request' }, $options->{request}->error->{message});
288 3         3007 return $self->_result($state, 1);
289             }
290              
291 243         1656 my ($method, $path_template);
292              
293             # method from options
294 243 100       1050 if (exists $options->{method}) {
    100          
295 64         174 $method = lc $options->{method};
296             return E({ %$state, data_path => '/request/method' }, 'wrong HTTP method %s', $options->{request}->method)
297 64 100 66     245 if $options->{request} and lc $options->{request}->method ne $method;
298             }
299             elsif ($options->{request}) {
300 175         607 $method = $options->{method} = lc $options->{request}->method;
301             }
302              
303             # path_template and method from operation_id from options
304 241 100       1710 if (exists $options->{operation_id}) {
305 20         428 my $operation_path = $self->openapi_document->get_operationId_path($options->{operation_id});
306             return E({ %$state, keyword => 'paths' }, 'unknown operation_id "%s"', $options->{operation_id})
307 20 100       2021 if not $operation_path;
308 18 100       116 return E({ %$state, schema_path => $operation_path, keyword => 'operationId' },
309             'operation id does not have an associated path') if $operation_path !~ m{^/paths/};
310 16         68 (undef, undef, $path_template, $method) = unjsonp($operation_path);
311              
312             return E({ %$state, schema_path => jsonp('/paths', $path_template) },
313             'operation does not match provided path_template')
314 16 100 100     381 if exists $options->{path_template} and $options->{path_template} ne $path_template;
315              
316             return E({ %$state, data_path => '/request/method', schema_path => $operation_path },
317             'wrong HTTP method %s', $options->{method})
318 14 100 100     119 if $options->{method} and lc $options->{method} ne $method;
319              
320 10         35 $options->{operation_path} = $operation_path;
321 10         40 $options->{method} = lc $method;
322             }
323              
324 231 100       1094 croak 'at least one of $options->{request}, $options->{method} and $options->{operation_id} must be provided'
325             if not $method;
326              
327             # path_template from options
328 229 100       685 if (exists $options->{path_template}) {
329 178         386 $path_template = $options->{path_template};
330              
331 178         898 my $path_item = $self->openapi_document->schema->{paths}{$path_template};
332 178 100       553 return E({ %$state, keyword => 'paths' }, 'missing path-item "%s"', $path_template) if not $path_item;
333              
334             return E({ %$state, data_path => '/request/method', schema_path => jsonp('/paths', $path_template), keyword => $method },
335             'missing operation for HTTP method "%s"', $method)
336 174 100       602 if not $path_item->{$method};
337             }
338              
339             # path_template from request URI
340 223 100 100     969 if (not $path_template and $options->{request} and my $uri_path = $options->{request}->url->path) {
      66        
341 41         1120 my $schema = $self->openapi_document->schema;
342             croak 'servers not yet supported when matching request URIs'
343 41 50 33     173 if exists $schema->{servers} and $schema->{servers}->@*;
344              
345             # sorting (ascii-wise) gives us the desired results that concrete path components sort ahead of
346             # templated components, except when the concrete component is a non-ascii character or matches [|}~].
347 41         222 foreach $path_template (sort keys $schema->{paths}->%*) {
348 43         421 my $path_pattern = $path_template =~ s!\{[^/}]+\}!([^/?#]*)!gr;
349 43 100       680 next if $uri_path !~ m/^$path_pattern$/;
350              
351 36         2715 $options->{path_template} = $path_template;
352              
353             # perldoc perlvar, @-: $n coincides with "substr $_, $-[n], $+[n] - $-[n]" if "$-[n]" is defined
354 36         288 my @capture_values = map
355             Encode::decode('UTF-8', URI::Escape::uri_unescape(substr($uri_path, $-[$_], $+[$_]-$-[$_]))), 1 .. $#-;
356 36         2084 my @capture_names = ($path_template =~ m!\{([^/?#}]+)\}!g);
357 36         86 my %path_captures; @path_captures{@capture_names} = @capture_values;
  36         122  
358              
359 36         119 my $indexes = [];
360 36 100       162 return E({ %$state, keyword => 'paths' }, 'duplicate path capture name %s', $capture_names[$indexes->[0]])
361             if not is_elements_unique(\@capture_names, $indexes);
362              
363             return E({ %$state, keyword => 'paths' }, 'provided path_captures values do not match request URI')
364 34 100 66     569 if $options->{path_captures} and not is_equal($options->{path_captures}, \%path_captures);
365              
366 32         83 $options->{path_captures} = \%path_captures;
367             return E({ %$state, data_path => '/request/method', schema_path => jsonp('/paths', $path_template), keyword => $method },
368             'missing operation for HTTP method "%s"', $method)
369 32 100       152 if not exists $schema->{paths}{$path_template}{$method};
370              
371 30         182 $options->{operation_id} = $self->openapi_document->schema->{paths}{$path_template}{$method}{operationId};
372 30 100       118 delete $options->{operation_id} if not defined $options->{operation_id};
373 30         100 $options->{operation_path} = jsonp('/paths', $path_template, $method);
374 30         635 return 1;
375             }
376              
377 5         338 return E({ %$state, keyword => 'paths' }, 'no match found for URI path "%s"', $uri_path);
378             }
379              
380 182 100       722 croak 'at least one of $options->{request}, $options->{path_template} and $options->{operation_id} must be provided'
381             if not $path_template;
382              
383 180         698 $options->{operation_path} = jsonp('/paths', $path_template, $method);
384              
385             # note: we aren't doing anything special with escaped slashes. this bit of the spec is hazy.
386 180         3411 my @capture_names = ($path_template =~ m!\{([^/}]+)\}!g);
387             return E({ %$state, keyword => 'paths', _schema_path_suffix => $path_template },
388             'provided path_captures names do not match path template "%s"', $path_template)
389             if exists $options->{path_captures}
390 180 100 100     1217 and not is_equal([ sort keys $options->{path_captures}->%* ], [ sort @capture_names ]);
391              
392 176 100       9828 if (not $options->{request}) {
393             $options->@{qw(path_template operation_id)} =
394 58         349 ($path_template, $self->openapi_document->schema->{paths}{$path_template}{$method}{operationId});
395 58 100       212 delete $options->{operation_id} if not defined $options->{operation_id};
396 58         313 return 1;
397             }
398              
399             # if we're still here, we were passed path_template in options or we calculated it from
400             # operation_id, and now we verify it against path_captures and the request URI.
401 118         437 my $uri_path = $options->{request}->url->path;
402              
403             # 3.2: "The value for these path parameters MUST NOT contain any unescaped “generic syntax”
404             # characters described by [RFC3986]: forward slashes (/), question marks (?), or hashes (#)."
405 118         2318 my $path_pattern = $path_template =~ s!\{[^/}]+\}!([^/?#]*)!gr;
406             return E({ %$state, keyword => 'paths', _schema_path_suffix => $path_template },
407 118 100       1232 'provided %s does not match request URI', exists $options->{path_template} ? 'path_template' : 'operation_id')
    100          
408             if $uri_path !~ m/^$path_pattern$/;
409              
410             # perldoc perlvar, @-: $n coincides with "substr $_, $-[n], $+[n] - $-[n]" if "$-[n]" is defined
411 110         7366 my @capture_values = map
412             Encode::decode('UTF-8', URI::Escape::uri_unescape(substr($uri_path, $-[$_], $+[$_]-$-[$_]))), 1 .. $#-;
413             return E({ %$state, keyword => 'paths', _schema_path_suffix => $path_template },
414             'provided path_captures values do not match request URI')
415             if exists $options->{path_captures}
416 110 100 100     4350 and not is_equal([ map $_.'', $options->{path_captures}->@{@capture_names} ], \@capture_values);
417              
418 108         7361 my %path_captures; @path_captures{@capture_names} = @capture_values;
  108         313  
419             $options->@{qw(path_template path_captures operation_id)} =
420 108         926 ($path_template, \%path_captures, $self->openapi_document->schema->{paths}{$path_template}{$method}{operationId});
421 108 100       428 delete $options->{operation_id} if not defined $options->{operation_id};
422 108         357 $options->{operation_path} = jsonp('/paths', $path_template, $method);
423 108         1716 return 1;
424             }
425              
426             ######## NO PUBLIC INTERFACES FOLLOW THIS POINT ########
427              
428 20     20   32 sub _validate_path_parameter ($self, $state, $param_obj, $path_captures) {
  20         36  
  20         44  
  20         33  
  20         28  
  20         32  
429             # 'required' is always true for path parameters
430             return E({ %$state, keyword => 'required' }, 'missing path parameter: %s', $param_obj->{name})
431 20 100       130 if not exists $path_captures->{$param_obj->{name}};
432              
433 18         102 $self->_validate_parameter_content($state, $param_obj, \ $path_captures->{$param_obj->{name}});
434             }
435              
436 99     99   2862 sub _validate_query_parameter ($self, $state, $param_obj, $uri) {
  99         195  
  99         138  
  99         139  
  99         160  
  99         128  
437             # parse the query parameters out of uri
438 99         294 my $query_params = +{ $uri->query->pairs->@* };
439              
440             # TODO: support different styles.
441             # for now, we only support style=form and do not allow for multiple values per
442             # property (i.e. 'explode' is not checked at all.)
443             # (other possible style values: spaceDelimited, pipeDelimited, deepObject)
444              
445 99 100       5642 if (not exists $query_params->{$param_obj->{name}}) {
446             return E({ %$state, keyword => 'required' }, 'missing query parameter: %s', $param_obj->{name})
447 52 100       181 if $param_obj->{required};
448 50         525 return 1;
449             }
450              
451             # TODO: check 'allowReserved': if true, do not use percent-decoding
452             return E({ %$state, keyword => 'allowReserved' }, 'allowReserved: true is not yet supported')
453 47 100 100     265 if $param_obj->{allowReserved} // 0;
454              
455 46         222 $self->_validate_parameter_content($state, $param_obj, \ $query_params->{$param_obj->{name}});
456             }
457              
458             # validates a header, from either the request or the response
459 52     52   30069 sub _validate_header_parameter ($self, $state, $header_name, $header_obj, $headers) {
  52         110  
  52         89  
  52         107  
  52         96  
  52         95  
  52         90  
460 52 100       325 return 1 if grep fc $header_name eq fc $_, qw(Accept Content-Type Authorization);
461              
462             # NOTE: for now, we will only support a single header value.
463 49         458 @$headers = map s/^\s*//r =~ s/\s*$//r, @$headers;
464              
465 49 100       178 if (not @$headers) {
466             return E({ %$state, keyword => 'required' }, 'missing header: %s', $header_name)
467 8 100       47 if $header_obj->{required};
468 2         28 return 1;
469             }
470              
471 41         200 $self->_validate_parameter_content($state, $header_obj, \ $headers->[0]);
472             }
473              
474 2     2   6 sub _validate_cookie_parameter ($self, $state, $param_obj, $request) {
  2         4  
  2         5  
  2         6  
  2         5  
  2         4  
475 2         8 return E($state, 'cookie parameters not yet supported');
476             }
477              
478 104     104   185 sub _validate_parameter_content ($self, $state, $param_obj, $content_ref) {
  104         189  
  104         190  
  104         169  
  104         160  
  104         184  
479 104 100       293 if (exists $param_obj->{content}) {
480             abort({ %$state, keyword => 'content' }, 'more than one media type entry present')
481 24 50       105 if keys $param_obj->{content}->%* > 1; # TODO: remove, when the spec schema is updated
482 24         104 my ($media_type) = keys $param_obj->{content}->%*; # there can only be one key
483 24         74 my $schema = $param_obj->{content}{$media_type}{schema};
484              
485 24         546 my $media_type_decoder = $self->get_media_type($media_type); # case-insensitive, wildcard lookup
486 24 100       7105 if (not $media_type_decoder) {
487             # don't fail if the schema would pass on any input
488 4 100       32 return if is_plain_hashref($schema) ? !keys %$schema : $schema;
    100          
489              
490 2         36 abort({ %$state, keyword => 'content', _schema_path_suffix => $media_type},
491             'EXCEPTION: unsupported media type "%s": add support with $openapi->add_media_type(...)', $media_type)
492             }
493              
494             try {
495             $content_ref = $media_type_decoder->($content_ref);
496             }
497             catch ($e) {
498             return E({ %$state, keyword => 'content', _schema_path_suffix => $media_type },
499             'could not decode content as %s: %s', $media_type, $e =~ s/^(.*)\n/$1/r);
500             }
501              
502 20         71 $state = { %$state, schema_path => jsonp($state->{schema_path}, 'content', $media_type, 'schema') };
  14         425  
503 14         268 return $self->_evaluate_subschema($content_ref->$*, $schema, $state);
504             }
505              
506 80         489 $state = { %$state, schema_path => jsonp($state->{schema_path}, 'schema') };
507 80         1032 $self->_evaluate_subschema($content_ref->$*, $param_obj->{schema}, $state);
508             }
509              
510 84     84   1509 sub _validate_body_content ($self, $state, $content_obj, $message) {
  84         154  
  84         152  
  84         147  
  84         151  
  84         131  
511             # does not include charset
512 84   100     257 my $content_type = fc((split(/;/, $message->headers->content_type//'', 2))[0] // '');
      100        
513              
514 84 100       1933 return E({ %$state, data_path => $state->{data_path} =~ s{body}{header/Content-Type}r, keyword => 'content' },
515             'missing header: Content-Type')
516             if not length $content_type;
517              
518 140     140   573 my $media_type = (first { $content_type eq fc } keys $content_obj->%*)
519 80 100 100 38   673 // (first { m{([^/]+)/\*$} && fc($content_type) =~ m{^\F\Q$1\E/[^/]+$} } keys $content_obj->%*);
  38         550  
520 80 100 100     537 $media_type = '*/*' if not defined $media_type and exists $content_obj->{'*/*'};
521 80 100       256 return E({ %$state, keyword => 'content' }, 'incorrect Content-Type "%s"', $content_type)
522             if not defined $media_type;
523              
524 76 50       357 if (exists $content_obj->{$media_type}{encoding}) {
525 0         0 my $state = { %$state, schema_path => jsonp($state->{schema_path}, 'content', $media_type) };
526             # 4.8.14.1 "The key, being the property name, MUST exist in the schema as a property."
527 0         0 foreach my $property (sort keys $content_obj->{$media_type}{encoding}->%*) {
528             ()= E({ $state, schema_path => jsonp($state->{schema_path}, 'schema', 'properties', $property) },
529             'encoding property "%s" requires a matching property definition in the schema')
530 0 0 0     0 if not exists(($content_obj->{$media_type}{schema}{properties}//{})->{$property});
531             }
532              
533             # 4.8.14.1 "The encoding object SHALL only apply to requestBody objects when the media type is
534             # multipart or application/x-www-form-urlencoded."
535 0 0 0     0 return E({ %$state, keyword => 'encoding' }, 'encoding not yet supported')
536             if $content_type =~ m{^multipart/} or $content_type eq 'application/x-www-form-urlencoded';
537             }
538              
539             # TODO: handle Content-Encoding header; https://github.com/OAI/OpenAPI-Specification/issues/2868
540 76         296 my $content_ref = \ $message->body;
541              
542             # decode the charset, for text content
543 76 100 100     1962 if ($content_type =~ m{^text/} and my $charset = $message->content->charset) {
544             try {
545             $content_ref = \ Encode::decode($charset, $content_ref->$*, Encode::FB_CROAK | Encode::LEAVE_SRC);
546             }
547 10         325 catch ($e) {
548             return E({ %$state, keyword => 'content', _schema_path_suffix => $media_type },
549             'could not decode content as %s: %s', $charset, $e =~ s/^(.*)\n/$1/r);
550             }
551             }
552              
553 72         1731 my $schema = $content_obj->{$media_type}{schema};
554              
555             # use the original Content-Type, NOT the possibly wildcard media type from the openapi document
556             # lookup is case-insensitive and falls back to wildcard definitions
557 72         1722 my $media_type_decoder = $self->get_media_type($content_type);
558 72 100   12   26499 $media_type_decoder = sub ($content_ref) { $content_ref } if $media_type eq '*/*';
  12         28  
  12         41  
  12         42  
  12         27  
559 72 100       257 if (not $media_type_decoder) {
560             # don't fail if the schema would pass on any input
561 4 100 33     61 return if not defined $schema or is_plain_hashref($schema) ? !keys %$schema : $schema;
    50          
562              
563 4         85 abort({ %$state, keyword => 'content', _schema_path_suffix => $media_type },
564             'EXCEPTION: unsupported Content-Type "%s": add support with $openapi->add_media_type(...)', $content_type)
565             }
566              
567             try {
568             $content_ref = $media_type_decoder->($content_ref);
569             }
570             catch ($e) {
571             return E({ %$state, keyword => 'content', _schema_path_suffix => $media_type },
572             'could not decode content as %s: %s', $media_type, $e =~ s/^(.*)\n/$1/r);
573             }
574              
575 68 100       192 return if not defined $schema;
  64         1497  
576              
577 62         425 $state = { %$state, schema_path => jsonp($state->{schema_path}, 'content', $media_type, 'schema') };
578 62         1297 $self->_evaluate_subschema($content_ref->$*, $schema, $state);
579             }
580              
581             # wrap a result object around the errors
582 184     184   431 sub _result ($self, $state, $exception = 0) {
  184         359  
  184         306  
  184         369  
  184         305  
583             return JSON::Schema::Modern::Result->new(
584             output_format => $self->evaluator->output_format,
585             formatted_annotations => 0,
586             valid => !$state->{errors}->@*,
587             $exception ? ( exception => 1 ) : (),
588             !$state->{errors}->@*
589             ? (annotations => $state->{annotations}//[])
590 184 100 50     5165 : (errors => $state->{errors}),
    100          
591             );
592             }
593              
594 92     92   264 sub _resolve_ref ($self, $entity_type, $ref, $state) {
  92         162  
  92         173  
  92         147  
  92         142  
  92         151  
595 92         341 my $uri = Mojo::URL->new($ref)->to_abs($state->{initial_schema_uri});
596 92         45291 my $schema_info = $self->evaluator->_fetch_from_uri($uri);
597 92 100       63756 abort({ %$state, keyword => '$ref' }, 'EXCEPTION: unable to find resource %s', $uri)
598             if not $schema_info;
599              
600             abort({ %$state, keyword => '$ref' }, 'EXCEPTION: maximum evaluation depth exceeded')
601 82 100       593 if $state->{depth}++ > $self->evaluator->max_traversal_depth;
602              
603             abort({ %$state, keyword => '$ref' }, 'EXCEPTION: bad $ref to %s: not a "%s"', $schema_info->{canonical_uri}, $entity_type)
604 80 100       1833 if $schema_info->{document}->get_entity_at_location($schema_info->{document_path}) ne $entity_type;
605              
606 78         7527 $state->{initial_schema_uri} = $schema_info->{canonical_uri};
607 78         368 $state->{traversed_schema_path} = $state->{traversed_schema_path}.$state->{schema_path}.jsonp('/$ref');
608 78         449 $state->{schema_path} = '';
609              
610 78         931 return $schema_info->{schema};
611             }
612              
613             # evaluates data against the subschema at the current state location
614 156     156   278 sub _evaluate_subschema ($self, $data, $schema, $state) {
  156         258  
  156         289  
  156         241  
  156         243  
  156         285  
615             # boolean schema
616 156 100       444 if (not is_plain_hashref($schema)) {
617 18 100       74 return 1 if $schema;
618              
619 16         180 my @location = unjsonp($state->{data_path});
620 16 50       364 my $location =
    100          
    100          
    100          
621             $location[-1] eq 'body' ? join(' ', @location[-2..-1])
622             : $location[-2] eq 'query' ? 'query parameter'
623             : $location[-2] eq 'path' ? 'path parameter' # this should never happen
624             : $location[-2] eq 'header' ? join(' ', @location[-3..-2])
625             : $location[-2]; # cookie
626 16         53 return E($state, '%s not permitted', $location);
627             }
628              
629 138 100       434 return 1 if !keys(%$schema); # schema is {}
630              
631             # treat numeric-looking data as a string, unless "type" explicitly requests number or integer.
632 132 100 66     1556 if (is_plain_hashref($schema) and exists $schema->{type} and not is_plain_arrayref($schema->{type})
    100 66        
      100        
      66        
      100        
633             and grep $schema->{type} eq $_, qw(number integer) and looks_like_number($data)) {
634 18         45 $data = $data+0;
635             }
636             elsif (defined $data and not is_ref($data)) {
637 80         192 $data = $data.'';
638             }
639              
640             # TODO: also handle multi-valued elements like headers and query parameters, when type=array requested
641             # (and possibly coerce their numeric-looking elements as well)
642              
643             my $result = $self->evaluator->evaluate(
644             $data, canonical_uri($state),
645             {
646             data_path => $state->{data_path},
647             traversed_schema_path => $state->{traversed_schema_path}.$state->{schema_path},
648             effective_base_uri => $state->{effective_base_uri},
649 132         602 collect_annotations => 1,
650             },
651             );
652              
653 132         501131 push $state->{errors}->@*, $result->errors;
654 132         11563 push $state->{annotations}->@*, $result->annotations;
655              
656 132         12267 return $result;
657             }
658              
659             # results may be unsatisfactory if not a valid HTTP request.
660 180     180   306 sub _convert_request ($request) {
  180         289  
  180         304  
661 180 100       1185 return $request if $request->isa('Mojo::Message::Request');
662 92 50       379 if ($request->isa('HTTP::Request')) {
663 92         338 my $req = Mojo::Message::Request->new;
664 92 100       767 if (not defined $request->headers->content_length) {
665 88         3162 my $length = length $request->content_ref->$*;
666 88 100       1379 $req->headers->content_length($length) if $length;
667             }
668 92         2213 $req->parse($request->as_string);
669 92         76534 return $req;
670             }
671 0         0 croak 'unknown type '.ref($request);
672             }
673              
674             # results may be unsatisfactory if not a valid HTTP response.
675 49     49   93 sub _convert_response ($response) {
  49         94  
  49         79  
676 49 100       327 return $response if $response->isa('Mojo::Message::Response');
677 25 50       95 if ($response->isa('HTTP::Response')) {
678 25         85 my $res = Mojo::Message::Response->new;
679 25 100       229 if (not defined $response->headers->content_length) {
680 20         829 my $length = length $response->content_ref->$*;
681 20 100       341 $res->headers->content_length($length) if $length;
682             }
683 25         843 $res->parse($response->as_string);
684 25         16546 return $res;
685             }
686 0           croak 'unknown type '.ref($response);
687             }
688              
689             1;
690              
691             __END__
692              
693             =pod
694              
695             =encoding UTF-8
696              
697             =head1 NAME
698              
699             OpenAPI::Modern - Validate HTTP requests and responses against an OpenAPI document
700              
701             =head1 VERSION
702              
703             version 0.045
704              
705             =head1 SYNOPSIS
706              
707             my $openapi = OpenAPI::Modern->new(
708             openapi_uri => '/api',
709             openapi_schema => YAML::PP->new(boolean => 'JSON::PP')->load_string(<<'YAML'));
710             openapi: 3.1.0
711             info:
712             title: Test API
713             version: 1.2.3
714             paths:
715             /foo/{foo_id}:
716             parameters:
717             - name: foo_id
718             in: path
719             required: true
720             schema:
721             pattern: ^[a-z]+$
722             post:
723             operationId: my_foo_request
724             parameters:
725             - name: My-Request-Header
726             in: header
727             required: true
728             schema:
729             pattern: ^[0-9]+$
730             requestBody:
731             required: true
732             content:
733             application/json:
734             schema:
735             type: object
736             properties:
737             hello:
738             type: string
739             pattern: ^[0-9]+$
740             responses:
741             200:
742             description: success
743             headers:
744             My-Response-Header:
745             required: true
746             schema:
747             pattern: ^[0-9]+$
748             content:
749             application/json:
750             schema:
751             type: object
752             required: [ status ]
753             properties:
754             status:
755             const: ok
756             YAML
757              
758             say 'request:';
759             my $request = POST '/foo/bar',
760             'My-Request-Header' => '123', 'Content-Type' => 'application/json', Host => 'example.com',
761             Content => '{"hello": 123}';
762             my $results = $openapi->validate_request($request);
763             say $results;
764             say ''; # newline
765             say JSON::MaybeXS->new(convert_blessed => 1, canonical => 1, pretty => 1, indent_length => 2)->encode($results);
766              
767             say 'response:';
768             my $response = Mojo::Message::Response->new(code => 200, message => 'OK');
769             $response->headers->content_type('application/json');
770             $response->headers->header('My-Response-Header', '123');
771             $response->body('{"status": "ok"}');
772             $results = $openapi->validate_response($response, { request => $request });
773             say $results;
774             say ''; # newline
775             say JSON::MaybeXS->new(convert_blessed => 1, canonical => 1, pretty => 1, indent_length => 2)->encode($results);
776              
777             prints:
778              
779             request:
780             '/request/body/hello': got integer, not string
781             '/request/body': not all properties are valid
782              
783             {
784             "errors" : [
785             {
786             "absoluteKeywordLocation" : "https://example.com/api#/paths/~1foo~1%7Bfoo_id%7D/post/requestBody/content/application~1json/schema/properties/hello/type",
787             "error" : "got integer, not string",
788             "instanceLocation" : "/request/body/hello",
789             "keywordLocation" : "/paths/~1foo~1{foo_id}/post/requestBody/content/application~1json/schema/properties/hello/type"
790             },
791             {
792             "absoluteKeywordLocation" : "https://example.com/api#/paths/~1foo~1%7Bfoo_id%7D/post/requestBody/content/application~1json/schema/properties",
793             "error" : "not all properties are valid",
794             "instanceLocation" : "/request/body",
795             "keywordLocation" : "/paths/~1foo~1{foo_id}/post/requestBody/content/application~1json/schema/properties"
796             }
797             ],
798             "valid" : false
799             }
800              
801             response:
802             valid
803              
804             {
805             "valid" : true
806             }
807              
808             =head1 DESCRIPTION
809              
810             This module provides various tools for working with an
811             L<OpenAPI Specification v3.1 document|https://spec.openapis.org/oas/v3.1.0#openapi-document> within
812             your application. The JSON Schema evaluator is fully specification-compliant; the OpenAPI evaluator
813             aims to be but some features are not yet available. My belief is that missing features are better
814             than features that seem to work but actually cut corners for simplicity.
815              
816             =for Pod::Coverage BUILDARGS
817              
818             =for stopwords schemas jsonSchemaDialect metaschema subschema perlish operationId
819              
820             =head1 CONSTRUCTOR ARGUMENTS
821              
822             If construction of the object is not successful, for example the document has a syntax error, the
823             call to C<new()> will throw an exception. Be careful about examining this exception, for it might be
824             a L<JSON::Schema::Modern::Result> object, which has a boolean overload of false when it contains
825             errors! But you never do C<if ($@) { ... }>, right?
826              
827             =head2 openapi_uri
828              
829             The URI that identifies the OpenAPI document.
830             Ignored if L</openapi_document> is provided.
831              
832             If it is not absolute, it is resolved at runtime against the request's C<Host> header (when available)
833             and the https scheme is assumed.
834              
835             =head2 openapi_schema
836              
837             The data structure describing the OpenAPI v3.1 document (as specified at
838             L<https://spec.openapis.org/oas/v3.1.0>). Ignored if L</openapi_document> is provided.
839              
840             =head2 openapi_document
841              
842             The L<JSON::Schema::Modern::Document::OpenAPI> document that holds the OpenAPI information to be
843             used for validation. If it is not provided to the constructor, then L</openapi_uri> and
844             L</openapi_schema> B<MUST> be provided, and L</evaluator> will also be used if provided.
845              
846             =head2 evaluator
847              
848             The L<JSON::Schema::Modern> object to use for all URI resolution and JSON Schema evaluation.
849             Ignored if L</openapi_document> is provided. Optional.
850              
851             =head1 ACCESSORS/METHODS
852              
853             =head2 openapi_uri
854              
855             The URI that identifies the OpenAPI document.
856              
857             =head2 openapi_schema
858              
859             The data structure describing the OpenAPI document. See L<the specification/https://spec.openapis.org/oas/v3.1.0>.
860              
861             =head2 openapi_document
862              
863             The L<JSON::Schema::Modern::Document::OpenAPI> document that holds the OpenAPI information to be
864             used for validation.
865              
866             =head2 evaluator
867              
868             The L<JSON::Schema::Modern> object to use for all URI resolution and JSON Schema evaluation.
869              
870             =head2 validate_request
871              
872             $result = $openapi->validate_request(
873             $request,
874             # optional second argument can contain any combination of:
875             my $options = {
876             path_template => '/foo/{arg1}/bar/{arg2}',
877             operation_id => 'my_operation_id',
878             path_captures => { arg1 => 1, arg2 => 2 },
879             method => 'get',
880             },
881             );
882              
883             Validates an L<HTTP::Request> or L<Mojo::Message::Request>
884             object against the corresponding OpenAPI v3.1 document, returning a
885             L<JSON::Schema::Modern::Result> object.
886              
887             The second argument is an optional hashref that contains extra information about the request,
888             corresponding to the values expected by L</find_path> below. It is populated with some information
889             about the request:
890             save it and pass it to a later L</validate_response> (corresponding to a response for this request)
891             to improve performance.
892              
893             =head2 validate_response
894              
895             $result = $openapi->validate_response(
896             $response,
897             {
898             path_template => '/foo/{arg1}/bar/{arg2}',
899             request => $request,
900             },
901             );
902              
903             Validates an L<HTTP::Response> or L<Mojo::Message::Response>
904             object against the corresponding OpenAPI v3.1 document, returning a
905             L<JSON::Schema::Modern::Result> object.
906              
907             The second argument is an optional hashref that contains extra information about the request
908             corresponding to the response, as in L</find_path>.
909              
910             C<request> is also accepted as a key in the hashref, representing the original request object that
911             corresponds to this response (as not all HTTP libraries link to the request in the response object).
912              
913             =head2 find_path
914              
915             $result = $self->find_path($options);
916              
917             Uses information in the request to determine the relevant parts of the OpenAPI specification.
918             C<request> should be provided if available, but additional data can be used instead
919             (which is populated by earlier L</validate_request> or L</find_path> calls to the same request).
920              
921             The single argument is a hashref that contains information about the request. Possible values
922             include:
923              
924             =over 4
925              
926             =item *
927              
928             C<request>: the object representing the HTTP request. Should be provided when available.
929              
930             =item *
931              
932             C<path_template>: a string representing the request URI, with placeholders in braces (e.g. C</pets/{petId}>); see L<https://spec.openapis.org/oas/v3.1.0#paths-object>.
933              
934             =item *
935              
936             C<operation_id>: a string corresponding to the L<operationId|https://swagger.io/docs/specification/paths-and-operations/#operationid> at a particular path-template and HTTP location under C</paths>
937              
938             =item *
939              
940             C<path_captures>: a hashref mapping placeholders in the path to their actual values in the request URI
941              
942             =item *
943              
944             C<method>: the HTTP method used by the request (used case-insensitively)
945              
946             =back
947              
948             All of these values are optional (unless C<request> is omitted), and will be derived from the
949             request URI as needed (albeit less
950             efficiently than if they were provided). All passed-in values MUST be consistent with each other and
951             the request URI.
952              
953             When successful, the options hash will be populated with keys C<path_template>, C<path_captures>,
954             C<method>, and C<operation_id>,
955             and the return value is true.
956             When not successful, the options hash will be populated with key C<errors>, an arrayref containing
957             a L<JSON::Schema::Modern::Error> object, and the return value is false. Other values may also be
958             populated if they can be successfully calculated.
959              
960             In addition, this value is populated in the options hash (when available):
961              
962             * C<operation_path>: a json pointer string indicating the document location of the operation that
963             was just evaluated against the request
964             * C<request> (not necessarily what was passed in: this is always a L<Mojo::Message::Request>)
965              
966             Note that the L<C</servers>|https://spec.openapis.org/oas/v3.1.0#server-object> section of the
967             OpenAPI document is not used for path matching at this time, for either scheme and host matching nor
968             path prefixes.
969              
970             =head2 canonical_uri
971              
972             An accessor that delegates to L<JSON::Schema::Modern::Document/canonical_uri>.
973              
974             =head2 schema
975              
976             An accessor that delegates to L<JSON::Schema::Modern::Document/schema>.
977              
978             =head2 get_media_type
979              
980             An accessor that delegates to L<JSON::Schema::Modern/get_media_type>.
981              
982             =head2 add_media_type
983              
984             A setter that delegates to L<JSON::Schema::Modern/add_media_type>.
985              
986             =head1 ON THE USE OF JSON SCHEMAS
987              
988             Embedded JSON Schemas, through the use of the C<schema> keyword, are fully draft2020-12-compliant,
989             as per the spec, and implemented with L<JSON::Schema::Modern>. Unless overridden with the use of the
990             L<jsonSchemaDialect|https://spec.openapis.org/oas/v3.1.0#specifying-schema-dialects> keyword, their
991             metaschema is L<https://spec.openapis.org/oas/3.1/dialect/base>, which allows for use of the
992             OpenAPI-specific keywords (C<discriminator>, C<xml>, C<externalDocs>, and C<example>), as defined in
993             L<the specification/https://spec.openapis.org/oas/v3.1.0#schema-object>. Format validation is turned
994             B<on>, and the use of content* keywords is off (see
995             L<JSON::Schema::Modern/validate_content_schemas>).
996              
997             References (with the C<$ref>) keyword may reference any position within the entire OpenAPI document;
998             as such, json pointers are relative to the B<root> of the document, not the root of the subschema
999             itself. References to other documents are also permitted, provided those documents have been loaded
1000             into the evaluator in advance (see L<JSON::Schema::Modern/add_schema>).
1001              
1002             Values are generally treated as strings for the purpose of schema evaluation. However, if the top
1003             level of the schema contains C<"type": "number"> or C<"type": "integer">, then the value will be
1004             (attempted to be) coerced into a number before being passed to the JSON Schema evaluator.
1005             Type coercion will B<not> be done if the C<type> keyword is omitted.
1006             This lets you use numeric keywords such as C<maximum> and C<multipleOf> in your schemas.
1007             It also resolves inconsistencies that can arise when request and response objects are created
1008             manually in a test environment (as opposed to being parsed from incoming network traffic) and can
1009             therefore inadvertently contain perlish numbers rather than strings.
1010              
1011             =head1 LIMITATIONS
1012              
1013             Only certain permutations of OpenAPI documents are supported at this time:
1014              
1015             =over 4
1016              
1017             =item *
1018              
1019             for all parameter types, only C<explode: true> is supported
1020              
1021             =item *
1022              
1023             for path parameters, only C<style: simple> is supported
1024              
1025             =item *
1026              
1027             for query parameters, only C<style: form> is supported
1028              
1029             =item *
1030              
1031             cookie parameters are not checked at all yet
1032              
1033             =item *
1034              
1035             for query and header parameters, only the first value of each name is considered
1036              
1037             =back
1038              
1039             =head1 SEE ALSO
1040              
1041             =over 4
1042              
1043             =item *
1044              
1045             L<Mojolicious::Plugin::OpenAPI::Modern>
1046              
1047             =item *
1048              
1049             L<JSON::Schema::Modern::Document::OpenAPI>
1050              
1051             =item *
1052              
1053             L<JSON::Schema::Modern>
1054              
1055             =item *
1056              
1057             L<https://json-schema.org>
1058              
1059             =item *
1060              
1061             L<https://www.openapis.org/>
1062              
1063             =item *
1064              
1065             L<https://oai.github.io/Documentation/>
1066              
1067             =item *
1068              
1069             L<https://spec.openapis.org/oas/v3.1.0>
1070              
1071             =back
1072              
1073             =head1 SUPPORT
1074              
1075             Bugs may be submitted through L<https://github.com/karenetheridge/OpenAPI-Modern/issues>.
1076              
1077             I am also usually active on irc, as 'ether' at C<irc.perl.org> and C<irc.libera.chat>.
1078              
1079             You can also find me on the L<JSON Schema Slack server|https://json-schema.slack.com> and L<OpenAPI
1080             Slack server|https://open-api.slack.com>, which are also great resources for finding help.
1081              
1082             =head1 AUTHOR
1083              
1084             Karen Etheridge <ether@cpan.org>
1085              
1086             =head1 COPYRIGHT AND LICENCE
1087              
1088             This software is copyright (c) 2021 by Karen Etheridge.
1089              
1090             This is free software; you can redistribute it and/or modify it under
1091             the same terms as the Perl 5 programming language system itself.
1092              
1093             Some schema files have their own licence, in share/oas/LICENSE.
1094              
1095             =cut