File Coverage

blib/lib/OpenAPI/Modern.pm
Criterion Covered Total %
statement 330 336 98.2
branch 146 158 92.4
condition 58 81 71.6
subroutine 48 48 100.0
pod 3 3 100.0
total 585 626 93.4


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