← Index
NYTProf Performance Profile   « line view »
For ../prof.pl
  Run on Wed Dec 14 15:33:55 2022
Reported on Wed Dec 14 15:40:02 2022

Filename/Users/ether/.perlbrew/libs/36.0@std/lib/perl5/OpenAPI/Modern.pm
StatementsExecuted 74 statements in 14.1ms
Subroutines
Calls P F Exclusive
Time
Inclusive
Time
Subroutine
1118.48ms619msOpenAPI::Modern::::BEGIN@26OpenAPI::Modern::BEGIN@26
1114.14ms23.9msOpenAPI::Modern::::BEGIN@11OpenAPI::Modern::BEGIN@11
1113.43ms21.1msOpenAPI::Modern::::BEGIN@24OpenAPI::Modern::BEGIN@24
1112.88ms15.8msOpenAPI::Modern::::BEGIN@28OpenAPI::Modern::BEGIN@28
1112.43ms3.95msOpenAPI::Modern::::BEGIN@20OpenAPI::Modern::BEGIN@20
1112.19ms3.29msOpenAPI::Modern::::BEGIN@12OpenAPI::Modern::BEGIN@12
1111.72ms8.98msOpenAPI::Modern::::BEGIN@13OpenAPI::Modern::BEGIN@13
1111.53ms1.65msOpenAPI::Modern::::BEGIN@25OpenAPI::Modern::BEGIN@25
1111.44ms3.75msOpenAPI::Modern::::BEGIN@19OpenAPI::Modern::BEGIN@19
111809µs887µsOpenAPI::Modern::::BEGIN@14OpenAPI::Modern::BEGIN@14
111803µs2.54msOpenAPI::Modern::::BEGIN@23OpenAPI::Modern::BEGIN@23
111664µs104sOpenAPI::Modern::::__ANON__[:79]OpenAPI::Modern::__ANON__[:79]
11168µs72µsmain::::BEGIN@1.2 main::BEGIN@1.2
11128µs28µsOpenAPI::Modern::::BEGIN@10OpenAPI::Modern::BEGIN@10
11116µs73µsOpenAPI::Modern::::BEGIN@32OpenAPI::Modern::BEGIN@32
11115µs69µsOpenAPI::Modern::::BEGIN@27OpenAPI::Modern::BEGIN@27
11113µs38µsOpenAPI::Modern::::BEGIN@15OpenAPI::Modern::BEGIN@15
11113µs518µsOpenAPI::Modern::::BEGIN@30OpenAPI::Modern::BEGIN@30
11111µs29µsOpenAPI::Modern::::BEGIN@16OpenAPI::Modern::BEGIN@16
1119µs25µsOpenAPI::Modern::::BEGIN@17OpenAPI::Modern::BEGIN@17
1118µs76µsOpenAPI::Modern::::BEGIN@21OpenAPI::Modern::BEGIN@21
1118µs75µsOpenAPI::Modern::::BEGIN@29OpenAPI::Modern::BEGIN@29
1118µs457µsOpenAPI::Modern::::BEGIN@33OpenAPI::Modern::BEGIN@33
1117µs43µsmain::::BEGIN@2.3 main::BEGIN@2.3
1116µs384µsOpenAPI::Modern::::BEGIN@31OpenAPI::Modern::BEGIN@31
1115µs30µsOpenAPI::Modern::::BEGIN@18OpenAPI::Modern::BEGIN@18
1115µs14µsOpenAPI::Modern::::BEGIN@22OpenAPI::Modern::BEGIN@22
1110s0sOpenAPI::Modern::::__ANON__OpenAPI::Modern::__ANON__ (xsub)
0000s0sOpenAPI::Modern::::__ANON__[:206]OpenAPI::Modern::__ANON__[:206]
0000s0sOpenAPI::Modern::::__ANON__[:476]OpenAPI::Modern::__ANON__[:476]
0000s0sOpenAPI::Modern::::__ANON__[:477]OpenAPI::Modern::__ANON__[:477]
0000s0sOpenAPI::Modern::::__ANON__[:515]OpenAPI::Modern::__ANON__[:515]
0000s0sOpenAPI::Modern::::_body_sizeOpenAPI::Modern::_body_size
0000s0sOpenAPI::Modern::::_content_charsetOpenAPI::Modern::_content_charset
0000s0sOpenAPI::Modern::::_content_refOpenAPI::Modern::_content_ref
0000s0sOpenAPI::Modern::::_content_typeOpenAPI::Modern::_content_type
0000s0sOpenAPI::Modern::::_evaluate_subschemaOpenAPI::Modern::_evaluate_subschema
0000s0sOpenAPI::Modern::::_headerOpenAPI::Modern::_header
0000s0sOpenAPI::Modern::::_query_pairsOpenAPI::Modern::_query_pairs
0000s0sOpenAPI::Modern::::_request_uriOpenAPI::Modern::_request_uri
0000s0sOpenAPI::Modern::::_resolve_refOpenAPI::Modern::_resolve_ref
0000s0sOpenAPI::Modern::::_resultOpenAPI::Modern::_result
0000s0sOpenAPI::Modern::::_validate_body_contentOpenAPI::Modern::_validate_body_content
0000s0sOpenAPI::Modern::::_validate_cookie_parameterOpenAPI::Modern::_validate_cookie_parameter
0000s0sOpenAPI::Modern::::_validate_header_parameterOpenAPI::Modern::_validate_header_parameter
0000s0sOpenAPI::Modern::::_validate_parameter_contentOpenAPI::Modern::_validate_parameter_content
0000s0sOpenAPI::Modern::::_validate_path_parameterOpenAPI::Modern::_validate_path_parameter
0000s0sOpenAPI::Modern::::_validate_query_parameterOpenAPI::Modern::_validate_query_parameter
0000s0sOpenAPI::Modern::::find_pathOpenAPI::Modern::find_path
0000s0sOpenAPI::Modern::::validate_requestOpenAPI::Modern::validate_request
0000s0sOpenAPI::Modern::::validate_responseOpenAPI::Modern::validate_response
Call graph for these subroutines as a Graphviz dot language file.
Line State
ments
Time
on line
Calls Time
in subs
Code
1244µs276µs
# spent 72µs (68+4) within main::BEGIN@1.2 which was called: # once (68µs+4µs) by main::BEGIN@2 at line 1
use strict;
# spent 72µs making 1 call to main::BEGIN@1.2 # spent 4µs making 1 call to strict::import
2252µs279µs
# spent 43µs (7+36) within main::BEGIN@2.3 which was called: # once (7µs+36µs) by main::BEGIN@2 at line 2
use warnings;
# spent 43µs making 1 call to main::BEGIN@2.3 # spent 36µs making 1 call to warnings::import
3package OpenAPI::Modern; # git description: v0.030-4-g3022412
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
810sour $VERSION = '0.031';
9
10252µs128µs
# spent 28µs within OpenAPI::Modern::BEGIN@10 which was called: # once (28µs+0s) by main::BEGIN@2 at line 10
use 5.020;
# spent 28µs making 1 call to OpenAPI::Modern::BEGIN@10
112757µs226.2ms
# spent 23.9ms (4.14+19.8) within OpenAPI::Modern::BEGIN@11 which was called: # once (4.14ms+19.8ms) by main::BEGIN@2 at line 11
use Moo;
# spent 23.9ms making 1 call to OpenAPI::Modern::BEGIN@11 # spent 2.28ms making 1 call to Moo::import
1231.06ms34.29ms
# spent 3.29ms (2.19+1.09) within OpenAPI::Modern::BEGIN@12 which was called: # once (2.19ms+1.09ms) by main::BEGIN@2 at line 12
use strictures 2;
# spent 3.29ms making 1 call to OpenAPI::Modern::BEGIN@12 # spent 975µs making 1 call to strictures::import # spent 30µs making 1 call to strictures::VERSION
132680µs29.14ms
# spent 8.98ms (1.72+7.26) within OpenAPI::Modern::BEGIN@13 which was called: # once (1.72ms+7.26ms) by main::BEGIN@2 at line 13
use experimental qw(signatures postderef);
# spent 8.98ms making 1 call to OpenAPI::Modern::BEGIN@13 # spent 160µs making 1 call to experimental::import
142662µs2891µs
# spent 887µs (809+78) within OpenAPI::Modern::BEGIN@14 which was called: # once (809µs+78µs) by main::BEGIN@2 at line 14
use if "$]" >= 5.022, experimental => 're_strict';
# spent 887µs making 1 call to OpenAPI::Modern::BEGIN@14 # spent 4µs making 1 call to if::import
15232µs240µs
# spent 38µs (13+25) within OpenAPI::Modern::BEGIN@15 which was called: # once (13µs+25µs) by main::BEGIN@2 at line 15
no if "$]" >= 5.031009, feature => 'indirect';
# spent 38µs making 1 call to OpenAPI::Modern::BEGIN@15 # spent 2µs making 1 call to if::unimport
16231µs231µs
# spent 29µs (11+18) within OpenAPI::Modern::BEGIN@16 which was called: # once (11µs+18µs) by main::BEGIN@2 at line 16
no if "$]" >= 5.033001, feature => 'multidimensional';
# spent 29µs making 1 call to OpenAPI::Modern::BEGIN@16 # spent 2µs making 1 call to if::unimport
17220µs227µs
# spent 25µs (9+16) within OpenAPI::Modern::BEGIN@17 which was called: # once (9µs+16µs) by main::BEGIN@2 at line 17
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
# spent 25µs making 1 call to OpenAPI::Modern::BEGIN@17 # spent 2µs making 1 call to if::unimport
18220µs255µs
# spent 30µs (5+25) within OpenAPI::Modern::BEGIN@18 which was called: # once (5µs+25µs) by main::BEGIN@2 at line 18
use Carp 'croak';
# spent 30µs making 1 call to OpenAPI::Modern::BEGIN@18 # spent 25µs making 1 call to Exporter::import
1921.09ms25.95ms
# spent 3.75ms (1.44+2.31) within OpenAPI::Modern::BEGIN@19 which was called: # once (1.44ms+2.31ms) by main::BEGIN@2 at line 19
use Safe::Isa;
# spent 3.75ms making 1 call to OpenAPI::Modern::BEGIN@19 # spent 2.20ms making 1 call to Exporter::import
202935µs24.01ms
# spent 3.95ms (2.43+1.52) within OpenAPI::Modern::BEGIN@20 which was called: # once (2.43ms+1.52ms) by main::BEGIN@2 at line 20
use Ref::Util qw(is_plain_hashref is_plain_arrayref is_ref);
# spent 3.95ms making 1 call to OpenAPI::Modern::BEGIN@20 # spent 63µs making 1 call to Exporter::import
21217µs282µs
# spent 76µs (8+68) within OpenAPI::Modern::BEGIN@21 which was called: # once (8µs+68µs) by main::BEGIN@2 at line 21
use List::Util 'first';
# spent 76µs making 1 call to OpenAPI::Modern::BEGIN@21 # spent 6µs making 1 call to List::Util::import
22216µs223µs
# spent 14µs (5+9) within OpenAPI::Modern::BEGIN@22 which was called: # once (5µs+9µs) by main::BEGIN@2 at line 22
use Scalar::Util 'looks_like_number';
# spent 14µs making 1 call to OpenAPI::Modern::BEGIN@22 # spent 9µs making 1 call to Exporter::import
232621µs22.58ms
# spent 2.54ms (803µs+1.74) within OpenAPI::Modern::BEGIN@23 which was called: # once (803µs+1.74ms) by main::BEGIN@2 at line 23
use Feature::Compat::Try;
# spent 2.54ms making 1 call to OpenAPI::Modern::BEGIN@23 # spent 32µs making 1 call to Feature::Compat::Try::import
243491µs321.2ms
# spent 21.1ms (3.43+17.7) within OpenAPI::Modern::BEGIN@24 which was called: # once (3.43ms+17.7ms) by main::BEGIN@2 at line 24
use Encode 2.89;
# spent 21.1ms making 1 call to OpenAPI::Modern::BEGIN@24 # spent 64µs making 1 call to Exporter::import # spent 15µs making 1 call to UNIVERSAL::VERSION
252534µs11.65ms
# spent 1.65ms (1.53+122µs) within OpenAPI::Modern::BEGIN@25 which was called: # once (1.53ms+122µs) by main::BEGIN@2 at line 25
use URI::Escape ();
# spent 1.65ms making 1 call to OpenAPI::Modern::BEGIN@25
263819µs3619ms
# spent 619ms (8.48+611) within OpenAPI::Modern::BEGIN@26 which was called: # once (8.48ms+611ms) by main::BEGIN@2 at line 26
use JSON::Schema::Modern 0.551;
# spent 619ms making 1 call to OpenAPI::Modern::BEGIN@26 # spent 11µs making 1 call to UNIVERSAL::VERSION # spent 0s making 1 call to OpenAPI::Modern::__ANON__
27339µs3123µs
# spent 69µs (15+54) within OpenAPI::Modern::BEGIN@27 which was called: # once (15µs+54µs) by main::BEGIN@2 at line 27
use JSON::Schema::Modern::Utilities 0.531 qw(jsonp unjsonp canonical_uri E abort is_equal);
# spent 69µs making 1 call to OpenAPI::Modern::BEGIN@27 # spent 48µs making 1 call to Exporter::import # spent 6µs making 1 call to UNIVERSAL::VERSION
282718µs215.8ms
# spent 15.8ms (2.88+12.9) within OpenAPI::Modern::BEGIN@28 which was called: # once (2.88ms+12.9ms) by main::BEGIN@2 at line 28
use JSON::Schema::Modern::Document::OpenAPI;
# spent 15.8ms making 1 call to OpenAPI::Modern::BEGIN@28 # spent 4µs making 1 call to Mojo::Base::import
29230µs2142µs
# spent 75µs (8+67) within OpenAPI::Modern::BEGIN@29 which was called: # once (8µs+67µs) by main::BEGIN@2 at line 29
use MooX::HandlesVia;
# spent 75µs making 1 call to OpenAPI::Modern::BEGIN@29 # spent 67µs making 1 call to MooX::HandlesVia::import
30347µs31.02ms
# spent 518µs (13+505) within OpenAPI::Modern::BEGIN@30 which was called: # once (13µs+505µs) by main::BEGIN@2 at line 30
use MooX::TypeTiny 0.002002;
# spent 518µs making 1 call to OpenAPI::Modern::BEGIN@30 # spent 494µs making 1 call to MooX::TypeTiny::import # spent 11µs making 1 call to UNIVERSAL::VERSION
31240µs2762µs
# spent 384µs (6+378) within OpenAPI::Modern::BEGIN@31 which was called: # once (6µs+378µs) by main::BEGIN@2 at line 31
use Types::Standard 'InstanceOf';
# spent 384µs making 1 call to OpenAPI::Modern::BEGIN@31 # spent 378µs making 1 call to Exporter::Tiny::import
32228µs4130µs
# spent 73µs (16+57) within OpenAPI::Modern::BEGIN@32 which was called: # once (16µs+57µs) by main::BEGIN@2 at line 32
use constant { true => JSON::PP::true, false => JSON::PP::false };
# spent 73µs making 1 call to OpenAPI::Modern::BEGIN@32 # spent 55µs making 1 call to constant::import # spent 1µs making 1 call to JSON::PP::false # spent 1µs making 1 call to JSON::PP::true
3324.52ms2906µs
# spent 457µs (8+449) within OpenAPI::Modern::BEGIN@33 which was called: # once (8µs+449µs) by main::BEGIN@2 at line 33
use namespace::clean;
# spent 457µs making 1 call to OpenAPI::Modern::BEGIN@33 # spent 449µs making 1 call to namespace::clean::import
34
35110µs22.72mshas openapi_document => (
# spent 2.31ms making 1 call to OpenAPI::Modern::has # spent 408µs making 1 call to Types::Standard::InstanceOf
36 is => 'ro',
37 isa => InstanceOf['JSON::Schema::Modern::Document::OpenAPI'],
38 required => 1,
39 handles => {
40 openapi_uri => 'canonical_uri', # Mojo::URL
41 openapi_schema => 'schema', # hashref
42 document_get => 'get', # data access using a json pointer
43 },
44);
45
46# held separately because $document->evaluator is a weak ref
4716µs21.36mshas evaluator => (
# spent 1.29ms making 1 call to OpenAPI::Modern::has # spent 71µs making 1 call to Types::Standard::InstanceOf
48 is => 'ro',
49 isa => InstanceOf['JSON::Schema::Modern'],
50 required => 1,
51 handles => [ qw(get_media_type add_media_type) ],
52);
53
544526µs
# spent 104s (664µs+104) within OpenAPI::Modern::__ANON__[/Users/ether/.perlbrew/libs/36.0@std/lib/perl5/OpenAPI/Modern.pm:79] which was called: # once (664µs+104s) by OpenAPI::Modern::__ANON__[(eval 411)[/Users/ether/.perlbrew/libs/36.0@std/lib/perl5/Class/Method/Modifiers.pm:89]:1] at line 1 of (eval 411)[Class/Method/Modifiers.pm:89]
around BUILDARGS => sub ($orig, $class, @args) {
5515µs110µs my $args = $class->$orig(@args);
# spent 10µs making 1 call to Moo::Object::BUILDARGS
56
5712µs if (exists $args->{openapi_document}) {
58 $args->{evaluator} = $args->{openapi_document}->evaluator;
59 }
60 else {
61 # construct document out of openapi_uri, openapi_schema, evaluator, if provided.
62 croak 'missing required constructor arguments: either openapi_document, or openapi_uri'
6310s if not exists $args->{openapi_uri};
64 croak 'missing required constructor arguments: either openapi_document, or openapi_schema'
6510s if not exists $args->{openapi_schema};
66
6716µs112.0ms $args->{evaluator} //= JSON::Schema::Modern->new(validate_formats => 1, max_traversal_depth => 80);
# spent 12.0ms making 1 call to JSON::Schema::Modern::new
68 $args->{openapi_document} = JSON::Schema::Modern::Document::OpenAPI->new(
69 canonical_uri => $args->{openapi_uri},
70 schema => $args->{openapi_schema},
71 evaluator => $args->{evaluator},
72123µs15.61ms );
# spent 5.61ms making 1 call to JSON::Schema::Modern::Document::OpenAPI::new
73
74 # if there were errors, this will die with a JSON::Schema::Modern::Result object
75121µs17.91ms $args->{evaluator}->add_schema($args->{openapi_document});
# spent 7.91ms making 1 call to JSON::Schema::Modern::add_schema
76 }
77
78145µs return $args;
7917µs1225µs};
# spent 225µs making 1 call to Moo::around
80
81sub validate_request ($self, $request, $options = {}) {
82 my $state = {
83 data_path => '/request',
84 initial_schema_uri => $self->openapi_uri, # the canonical URI as of the start or last $id, or the last traversed $ref
85 traversed_schema_path => '', # the accumulated traversal path as of the start, or last $id, or up to the last traversed $ref
86 schema_path => '', # the rest of the path, since the last $id or the last traversed $ref
87 effective_base_uri => Mojo::URL->new->host(scalar _header($request, 'Host'))->scheme('https'),
88 annotations => [],
89 };
90
91 try {
92 my $path_ok = $self->find_path($request, $options);
93 $state->{errors} = delete $options->{errors};
94 return $self->_result($state, 1) if not $path_ok;
95
96 my ($path_template, $path_captures) = $options->@{qw(path_template path_captures)};
97 my $path_item = $self->openapi_document->schema->{paths}{$path_template};
98 my $method = lc $request->method;
99 my $operation = $path_item->{$method};
100
101 $state->{schema_path} = jsonp('/paths', $path_template);
102
103 # PARAMETERS
104 # { $in => { $name => 'path-item'|$method } } as we process each one.
105 my $request_parameters_processed;
106
107 # first, consider parameters at the operation level.
108 # parameters at the path-item level are also considered, if not already seen at the operation level
109 foreach my $section ($method, 'path-item') {
110 foreach my $idx (0 .. (($section eq $method ? $operation : $path_item)->{parameters}//[])->$#*) {
111 my $state = { %$state, schema_path => jsonp($state->{schema_path},
112 ($section eq $method ? $method : ()), 'parameters', $idx) };
113 my $param_obj = ($section eq $method ? $operation : $path_item)->{parameters}[$idx];
114 while (my $ref = $param_obj->{'$ref'}) {
115 $param_obj = $self->_resolve_ref($ref, $state);
116 }
117
118 my $fc_name = $param_obj->{in} eq 'header' ? fc($param_obj->{name}) : $param_obj->{name};
119
120 abort($state, 'duplicate %s parameter "%s"', $param_obj->{in}, $param_obj->{name})
121 if ($request_parameters_processed->{$param_obj->{in}}{$fc_name} // '') eq $section;
122 next if exists $request_parameters_processed->{$param_obj->{in}}{$fc_name};
123 $request_parameters_processed->{$param_obj->{in}}{$fc_name} = $section;
124
125 $state->{data_path} = jsonp($state->{data_path},
126 ((grep $param_obj->{in} eq $_, qw(path query)) ? 'uri' : ()), $param_obj->{in},
127 $param_obj->{name});
128 my $valid =
129 $param_obj->{in} eq 'path' ? $self->_validate_path_parameter($state, $param_obj, $path_captures)
130 : $param_obj->{in} eq 'query' ? $self->_validate_query_parameter($state, $param_obj, _request_uri($request))
131 : $param_obj->{in} eq 'header' ? $self->_validate_header_parameter($state, $param_obj->{name}, $param_obj, [ _header($request, $param_obj->{name}) ])
132 : $param_obj->{in} eq 'cookie' ? $self->_validate_cookie_parameter($state, $param_obj, $request)
133 : abort($state, 'unrecognized "in" value "%s"', $param_obj->{in});
134 }
135 }
136
137 # 3.2 "Each template expression in the path MUST correspond to a path parameter that is included in
138 # the Path Item itself and/or in each of the Path Item’s Operations."
139 foreach my $path_name (sort keys $path_captures->%*) {
140 abort({ %$state, data_path => jsonp($state->{data_path}, qw(uri path), $path_name) },
141 'missing path parameter specification for "%s"', $path_name)
142 if not exists $request_parameters_processed->{path}{$path_name};
143 }
144
145 $state->{data_path} = jsonp($state->{data_path}, 'body');
146 $state->{schema_path} = jsonp($state->{schema_path}, $method);
147
148 if (my $body_obj = $operation->{requestBody}) {
149 $state->{schema_path} = jsonp($state->{schema_path}, 'requestBody');
150
151 while (my $ref = $body_obj->{'$ref'}) {
152 $body_obj = $self->_resolve_ref($ref, $state);
153 }
154
155 if (_body_size($request)) {
156 ()= $self->_validate_body_content($state, $body_obj->{content}, $request);
157 }
158 elsif ($body_obj->{required}) {
159 ()= E({ %$state, keyword => 'required' }, 'request body is required but missing');
160 }
161 }
162 else {
163 ()= E($state, 'unspecified body is present in %s request', uc $method)
164 if ($method eq 'get' or $method eq 'head') and _body_size($request);
165 }
166 }
167 catch ($e) {
168 if ($e->$_isa('JSON::Schema::Modern::Result')) {
169 return $e;
170 }
171 elsif ($e->$_isa('JSON::Schema::Modern::Error')) {
172 push @{$state->{errors}}, $e;
173 }
174 else {
175 ()= E($state, 'EXCEPTION: '.$e);
176 }
177 }
178
179 return $self->_result($state);
180}
181
182sub validate_response ($self, $response, $options = {}) {
183 my $state = {
184 data_path => '/response',
185 initial_schema_uri => $self->openapi_uri, # the canonical URI as of the start or last $id, or the last traversed $ref
186 traversed_schema_path => '', # the accumulated traversal path as of the start, or last $id, or up to the last traversed $ref
187 schema_path => '', # the rest of the path, since the last $id or the last traversed $ref
188 annotations => [],
189 };
190
191 try {
192 my $path_ok = $self->find_path($response->$_call_if_can('request') // $options->{request}, $options);
193 $state->{errors} = delete $options->{errors};
194 return $self->_result($state, 1) if not $path_ok;
195
196 my ($path_template, $path_captures) = $options->@{qw(path_template path_captures)};
197 my $method = lc $options->{method};
198 my $operation = $self->openapi_document->schema->{paths}{$path_template}{$method};
199
200 return $self->_result($state) if not exists $operation->{responses};
201
202 $state->{effective_base_uri} = Mojo::URL->new->host(scalar _header($options->{request}, 'Host'))->scheme('https')
203 if $options->{request};
204 $state->{schema_path} = jsonp('/paths', $path_template, $method);
205
206 my $response_name = first { exists $operation->{responses}{$_} }
207 $response->code, substr(sprintf('%03s', $response->code), 0, -2).'XX', 'default';
208
209 if (not $response_name) {
210 ()= E({ %$state, keyword => 'responses' }, 'no response object found for code %s', $response->code);
211 return $self->_result($state);
212 }
213
214 my $response_obj = $operation->{responses}{$response_name};
215 $state->{schema_path} = jsonp($state->{schema_path}, 'responses', $response_name);
216 while (my $ref = $response_obj->{'$ref'}) {
217 $response_obj = $self->_resolve_ref($ref, $state);
218 }
219
220 foreach my $header_name (sort keys(($response_obj->{headers}//{})->%*)) {
221 next if fc $header_name eq fc 'Content-Type';
222 my $state = { %$state, schema_path => jsonp($state->{schema_path}, 'headers', $header_name) };
223 my $header_obj = $response_obj->{headers}{$header_name};
224 while (my $ref = $header_obj->{'$ref'}) {
225 $header_obj = $self->_resolve_ref($ref, $state);
226 }
227
228 ()= $self->_validate_header_parameter({ %$state,
229 data_path => jsonp($state->{data_path}, 'header', $header_name) },
230 $header_name, $header_obj, [ _header($response, $header_name) ]);
231 }
232
233 ()= $self->_validate_body_content({ %$state, data_path => jsonp($state->{data_path}, 'body') },
234 $response_obj->{content}, $response)
235 if exists $response_obj->{content} and _body_size($response);
236 }
237 catch ($e) {
238 if ($e->$_isa('JSON::Schema::Modern::Result')) {
239 return $e;
240 }
241 elsif ($e->$_isa('JSON::Schema::Modern::Error')) {
242 push @{$state->{errors}}, $e;
243 }
244 else {
245 ()= E($state, 'EXCEPTION: '.$e);
246 }
247 }
248
249 return $self->_result($state);
250}
251
252sub find_path ($self, $request, $options) {
253 my $state = {
254 data_path => '/request/uri/path',
255 initial_schema_uri => $self->openapi_uri, # the canonical URI as of the start or last $id, or the last traversed $ref
256 traversed_schema_path => '', # the accumulated traversal path as of the start, or last $id, or up to the last traversed $ref
257 schema_path => '', # the rest of the path, since the last $id or the last traversed $ref
258 errors => $options->{errors} //= [],
259 $request ? ( effective_base_uri => Mojo::URL->new->host(scalar _header($request, 'Host'))->scheme('https') ) : (),
260 };
261
262 my ($method, $path_template);
263
264 # method from options
265 if (exists $options->{method}) {
266 $method = lc $options->{method};
267 return E({ %$state, data_path => '/request/method' }, 'wrong HTTP method %s', $request->method)
268 if $request and lc $request->method ne $method;
269 }
270 elsif ($request) {
271 $method = $options->{method} = lc $request->method;
272 }
273
274 # path_template and method from operation_id from options
275 if (exists $options->{operation_id}) {
276 my $operation_path = $self->openapi_document->get_operationId_path($options->{operation_id});
277 return E({ %$state, keyword => 'paths' }, 'unknown operation_id "%s"', $options->{operation_id})
278 if not $operation_path;
279 return E({ %$state, schema_path => $operation_path, keyword => 'operationId' },
280 'operation id does not have an associated path') if $operation_path !~ m{^/paths/};
281 (undef, undef, $path_template, $method) = unjsonp($operation_path);
282
283 return E({ %$state, schema_path => jsonp('/paths', $path_template) },
284 'operation does not match provided path_template')
285 if exists $options->{path_template} and $options->{path_template} ne $path_template;
286
287 return E({ %$state, data_path => '/request/method', schema_path => $operation_path },
288 'wrong HTTP method %s', $options->{method})
289 if $options->{method} and lc $options->{method} ne $method;
290
291 $options->{method} = lc $method;
292 }
293
294 croak 'at least one of request, $options->{method} and $options->{operation_id} must be provided'
295 if not $method;
296
297 # path_template from options
298 if (exists $options->{path_template}) {
299 $path_template = $options->{path_template};
300
301 my $path_item = $self->openapi_document->schema->{paths}{$path_template};
302 return E({ %$state, keyword => 'paths' }, 'missing path-item "%s"', $path_template) if not $path_item;
303
304 return E({ %$state, data_path => '/request/method', schema_path => jsonp('/paths', $path_template), keyword => $method },
305 'missing operation for HTTP method "%s"', $method)
306 if not $path_item->{$method};
307 }
308
309 # path_template from request URI
310 if (not $path_template and $request and my $uri_path = _request_uri($request)->path) {
311 my $schema = $self->openapi_document->schema;
312 croak 'servers not yet supported when matching request URIs'
313 if exists $schema->{servers} and $schema->{servers}->@*;
314
315 foreach $path_template (sort keys $schema->{paths}->%*) {
316 my $path_pattern = $path_template =~ s!\{[^/}]+\}!([^/?#]*)!gr;
317 next if $uri_path !~ m/^$path_pattern$/;
318
319 # perldoc perlvar, @-: $n coincides with "substr $_, $-[n], $+[n] - $-[n]" if "$-[n]" is defined
320 my @capture_values = map
321 Encode::decode('UTF-8', URI::Escape::uri_unescape(substr($uri_path, $-[$_], $+[$_]-$-[$_]))), 1 .. $#-;
322 my @capture_names = ($path_template =~ m!\{([^/?#}]+)\}!g);
323 my %path_captures; @path_captures{@capture_names} = @capture_values;
324
325 $options->{path_template} = $path_template;
326 return E({ %$state, keyword => 'paths' }, 'provided path_captures values do not match request URI')
327 if $options->{path_captures} and not is_equal($options->{path_captures}, \%path_captures);
328
329 $options->{path_captures} = \%path_captures;
330 return E({ %$state, data_path => '/request/method', schema_path => jsonp('/paths', $path_template), keyword => $method },
331 'missing operation for HTTP method "%s"', $method)
332 if not exists $schema->{paths}{$path_template}{$method};
333
334 $options->{operation_id} = $self->openapi_document->schema->{paths}{$path_template}{$method}{operationId};
335 delete $options->{operation_id} if not defined $options->{operation_id};
336 return 1;
337 }
338
339 return E({ %$state, keyword => 'paths' }, 'no match found for URI path "%s"', $uri_path);
340 }
341
342 croak 'at least one of request, $options->{path_template} and $options->{operation_id} must be provided'
343 if not $path_template;
344
345 # note: we aren't doing anything special with escaped slashes. this bit of the spec is hazy.
346 my @capture_names = ($path_template =~ m!\{([^/}]+)\}!g);
347 return E({ %$state, keyword => 'paths', _schema_path_suffix => $path_template },
348 'provided path_captures names do not match path template "%s"', $path_template)
349 if exists $options->{path_captures}
350 and not is_equal([ sort keys $options->{path_captures}->%*], [ sort @capture_names ]);
351
352 if (not $request) {
353 $options->@{qw(path_template operation_id)} =
354 ($path_template, $self->openapi_document->schema->{paths}{$path_template}{$method}{operationId});
355 delete $options->{operation_id} if not defined $options->{operation_id};
356 return 1;
357 }
358
359 # if we're still here, we were passed path_template in options or we calculated it from
360 # operation_id, and now we verify it against path_captures and the request URI.
361 my $uri_path = _request_uri($request)->path;
362
363 # 3.2: "The value for these path parameters MUST NOT contain any unescaped “generic syntax”
364 # characters described by [RFC3986]: forward slashes (/), question marks (?), or hashes (#)."
365 my $path_pattern = $path_template =~ s!\{[^/}]+\}!([^/?#]*)!gr;
366 return E({ %$state, keyword => 'paths', _schema_path_suffix => $path_template },
367 'provided %s does not match request URI', exists $options->{path_template} ? 'path_template' : 'operation_id')
368 if $uri_path !~ m/^$path_pattern$/;
369
370 # perldoc perlvar, @-: $n coincides with "substr $_, $-[n], $+[n] - $-[n]" if "$-[n]" is defined
371 my @capture_values = map
372 Encode::decode('UTF-8', URI::Escape::uri_unescape(substr($uri_path, $-[$_], $+[$_]-$-[$_]))), 1 .. $#-;
373 return E({ %$state, keyword => 'paths', _schema_path_suffix => $path_template },
374 'provided path_captures values do not match request URI')
375 if exists $options->{path_captures}
376 and not is_equal([ map $_.'', $options->{path_captures}->@{@capture_names} ], \@capture_values);
377
378 my %path_captures; @path_captures{@capture_names} = @capture_values;
379 $options->@{qw(path_template path_captures operation_id)} =
380 ($path_template, \%path_captures, $self->openapi_document->schema->{paths}{$path_template}{$method}{operationId});
381 delete $options->{operation_id} if not defined $options->{operation_id};
382 return 1;
383}
384
385######## NO PUBLIC INTERFACES FOLLOW THIS POINT ########
386
387sub _validate_path_parameter ($self, $state, $param_obj, $path_captures) {
388 # 'required' is always true for path parameters
389 return E({ %$state, keyword => 'required' }, 'missing path parameter: %s', $param_obj->{name})
390 if not exists $path_captures->{$param_obj->{name}};
391
392 $self->_validate_parameter_content($state, $param_obj, \ $path_captures->{$param_obj->{name}});
393}
394
395sub _validate_query_parameter ($self, $state, $param_obj, $uri) {
396 # parse the query parameters out of uri
397 my $query_params = { _query_pairs($uri) };
398
399 # TODO: support different styles.
400 # for now, we only support style=form and do not allow for multiple values per
401 # property (i.e. 'explode' is not checked at all.)
402 # (other possible style values: spaceDelimited, pipeDelimited, deepObject)
403
404 if (not exists $query_params->{$param_obj->{name}}) {
405 return E({ %$state, keyword => 'required' }, 'missing query parameter: %s', $param_obj->{name})
406 if $param_obj->{required};
407 return 1;
408 }
409
410 # TODO: check 'allowReserved': if true, do not use percent-decoding
411 return E({ %$state, keyword => 'allowReserved' }, 'allowReserved: true is not yet supported')
412 if $param_obj->{allowReserved} // 0;
413
414 $self->_validate_parameter_content($state, $param_obj, \ $query_params->{$param_obj->{name}});
415}
416
417# validates a header, from either the request or the response
418sub _validate_header_parameter ($self, $state, $header_name, $header_obj, $headers) {
419 return 1 if grep fc $header_name eq fc $_, qw(Accept Content-Type Authorization);
420
421 # NOTE: for now, we will only support a single header value.
422 @$headers = map s/^\s*//r =~ s/\s*$//r, @$headers;
423
424 if (not @$headers) {
425 return E({ %$state, keyword => 'required' }, 'missing header: %s', $header_name)
426 if $header_obj->{required};
427 return 1;
428 }
429
430 $self->_validate_parameter_content($state, $header_obj, \ $headers->[0]);
431}
432
433sub _validate_cookie_parameter ($self, $state, $param_obj, $request) {
434 return E($state, 'cookie parameters not yet supported');
435}
436
437sub _validate_parameter_content ($self, $state, $param_obj, $content_ref) {
438 if (exists $param_obj->{content}) {
439 abort({ %$state, keyword => 'content' }, 'more than one media type entry present')
440 if keys $param_obj->{content}->%* > 1; # TODO: remove, when the spec schema is updated
441 my ($media_type) = keys $param_obj->{content}->%*; # there can only be one key
442 my $schema = $param_obj->{content}{$media_type}{schema};
443
444 my $media_type_decoder = $self->get_media_type($media_type); # case-insensitive, wildcard lookup
445 if (not $media_type_decoder) {
446 # don't fail if the schema would pass on any input
447 return if is_plain_hashref($schema) ? !keys %$schema : $schema;
448
449 abort({ %$state, keyword => 'content', _schema_path_suffix => $media_type},
450 'EXCEPTION: unsupported media type "%s": add support with $openapi->add_media_type(...)', $media_type)
451 }
452
453 try {
454 $content_ref = $media_type_decoder->($content_ref);
455 }
456 catch ($e) {
457 return E({ %$state, keyword => 'content', _schema_path_suffix => $media_type },
458 'could not decode content as %s: %s', $media_type, $e =~ s/^(.*)\n/$1/r);
459 }
460
461 $state = { %$state, schema_path => jsonp($state->{schema_path}, 'content', $media_type, 'schema') };
462 return $self->_evaluate_subschema($content_ref->$*, $schema, $state);
463 }
464
465 $state = { %$state, schema_path => jsonp($state->{schema_path}, 'schema') };
466 $self->_evaluate_subschema($content_ref->$*, $param_obj->{schema}, $state);
467}
468
469sub _validate_body_content ($self, $state, $content_obj, $message) {
470 my $content_type = _content_type($message);
471
472 return E({ %$state, data_path => $state->{data_path} =~ s{body}{header/Content-Type}r, keyword => 'content' },
473 'missing header: Content-Type')
474 if not length $content_type;
475
476 my $media_type = (first { $content_type eq fc } keys $content_obj->%*)
477 // (first { m{([^/]+)/\*$} && fc($content_type) =~ m{^\F\Q$1\E/[^/]+$} } keys $content_obj->%*);
478 $media_type = '*/*' if not defined $media_type and exists $content_obj->{'*/*'};
479 return E({ %$state, keyword => 'content' }, 'incorrect Content-Type "%s"', $content_type)
480 if not defined $media_type;
481
482 if (exists $content_obj->{$media_type}{encoding}) {
483 my $state = { %$state, schema_path => jsonp($state->{schema_path}, 'content', $media_type) };
484 # 4.8.14.1 "The key, being the property name, MUST exist in the schema as a property."
485 foreach my $property (sort keys $content_obj->{$media_type}{encoding}->%*) {
486 ()= E({ $state, schema_path => jsonp($state->{schema_path}, 'schema', 'properties', $property) },
487 'encoding property "%s" requires a matching property definition in the schema')
488 if not exists(($content_obj->{$media_type}{schema}{properties}//{})->{$property});
489 }
490
491 # 4.8.14.1 "The encoding object SHALL only apply to requestBody objects when the media type is
492 # multipart or application/x-www-form-urlencoded."
493 return E({ %$state, keyword => 'encoding' }, 'encoding not yet supported')
494 if $content_type =~ m{^multipart/} or $content_type eq 'application/x-www-form-urlencoded';
495 }
496
497 # TODO: handle Content-Encoding header; https://github.com/OAI/OpenAPI-Specification/issues/2868
498 my $content_ref = _content_ref($message);
499
500 # decode the charset
501 if (my $charset = _content_charset($message)) {
502 try {
503 $content_ref = \ Encode::decode($charset, $content_ref->$*, Encode::FB_CROAK | Encode::LEAVE_SRC);
504 }
505 catch ($e) {
506 return E({ %$state, keyword => 'content', _schema_path_suffix => $media_type },
507 'could not decode content as %s: %s', $charset, $e =~ s/^(.*)\n/$1/r);
508 }
509 }
510
511 my $schema = $content_obj->{$media_type}{schema};
512
513 # use the original Content-Type, NOT the possibly wildcard media type from the document
514 my $media_type_decoder = $self->get_media_type($content_type); # case-insensitive, wildcard lookup
515 $media_type_decoder = sub ($content_ref) { $content_ref } if $media_type eq '*/*';
516 if (not $media_type_decoder) {
517 # don't fail if the schema would pass on any input
518 return if not defined $schema or is_plain_hashref($schema) ? !keys %$schema : $schema;
519
520 abort({ %$state, keyword => 'content', _schema_path_suffix => $media_type },
521 'EXCEPTION: unsupported Content-Type "%s": add support with $openapi->add_media_type(...)', $content_type)
522 }
523
524 try {
525 $content_ref = $media_type_decoder->($content_ref);
526 }
527 catch ($e) {
528 return E({ %$state, keyword => 'content', _schema_path_suffix => $media_type },
529 'could not decode content as %s: %s', $media_type, $e =~ s/^(.*)\n/$1/r);
530 }
531
532 return 1 if not defined $schema;
533
534 $state = { %$state, schema_path => jsonp($state->{schema_path}, 'content', $media_type, 'schema') };
535 my $result = $self->_evaluate_subschema($content_ref->$*, $schema, $state);
536
537 return 1 if not is_ref($result); # schema is an empty hash or boolean true
538
539 my $type = (split('/', $state->{data_path}, 3))[1];
540 my $keyword = $type eq 'request' ? 'readOnly' : $type eq 'response' ? 'writeOnly' : die "unknown type $type";
541
542 foreach my $annotation (grep $_->keyword eq $keyword && $_->annotation, $result->annotations) {
543 push $state->{errors}->@*, JSON::Schema::Modern::Error->new(
544 (map +($_ => $annotation->$_), qw(keyword instance_location keyword_location absolute_keyword_location)),
545 error => ($keyword =~ s/O/-o/r).' value is present',
546 );
547 }
548
549 return !!$result;
550}
551
552# wrap a result object around the errors
553sub _result ($self, $state, $exception = 0) {
554 return JSON::Schema::Modern::Result->new(
555 output_format => $self->evaluator->output_format,
556 formatted_annotations => 0,
557 valid => !$state->{errors}->@*,
558 $exception ? ( exception => 1 ) : (),
559 !$state->{errors}->@*
560 ? (annotations => $state->{annotations}//[])
561 : (errors => $state->{errors}),
562 );
563}
564
565sub _resolve_ref ($self, $ref, $state) {
566 my $uri = Mojo::URL->new($ref)->to_abs($state->{initial_schema_uri});
567 my $schema_info = $self->evaluator->_fetch_from_uri($uri);
568 abort({ %$state, keyword => '$ref' }, 'EXCEPTION: unable to find resource %s', $uri)
569 if not $schema_info;
570
571 abort($state, 'EXCEPTION: maximum evaluation depth exceeded')
572 if $state->{depth}++ > $self->evaluator->max_traversal_depth;
573
574 $state->{initial_schema_uri} = $schema_info->{canonical_uri};
575 $state->{traversed_schema_path} = $state->{traversed_schema_path}.$state->{schema_path}.jsonp('/$ref');
576 $state->{schema_path} = '';
577
578 return $schema_info->{schema};
579}
580
581# evaluates data against the subschema at the current state location
582sub _evaluate_subschema ($self, $data, $schema, $state) {
583 return 1 if is_plain_hashref($schema) ? !keys(%$schema) : $schema; # true schema
584
585 if (is_plain_hashref($schema)) {
586 return 1 if !keys(%$schema);
587 }
588 else {
589 return 1 if $schema;
590
591 my @location = unjsonp($state->{data_path});
592 my $location =
593 $location[-1] eq 'body' ? join(' ', @location[-2..-1])
594 : $location[-2] eq 'query' ? 'query parameter'
595 : $location[-2] eq 'path' ? 'path parameter' # this should never happen
596 : $location[-2] eq 'header' ? join(' ', @location[-3..-2])
597 : $location[-2]; # cookie
598 return E($state, '%s not permitted', $location);
599 return E($state, 'thingy not permitted');
600 }
601
602 # treat numeric-looking data as a string, unless "type" explicitly requests number or integer.
603 if (is_plain_hashref($schema) and exists $schema->{type} and not is_plain_arrayref($schema->{type})
604 and grep $schema->{type} eq $_, qw(number integer) and looks_like_number($data)) {
605 $data = $data+0;
606 }
607 elsif (defined $data and not is_ref($data)) {
608 $data = $data.'';
609 }
610
611 # TODO: also handle multi-valued elements like headers and query parameters, when type=array requested
612 # (and possibly coerce their numeric-looking elements as well)
613
614 my $result = $self->evaluator->evaluate(
615 $data, canonical_uri($state),
616 {
617 data_path => $state->{data_path},
618 traversed_schema_path => $state->{traversed_schema_path}.$state->{schema_path},
619 effective_base_uri => $state->{effective_base_uri},
620 collect_annotations => 1,
621 },
622 );
623
624 push $state->{errors}->@*, $result->errors;
625 push $state->{annotations}->@*, $result->annotations;
626
627 return $result;
628}
629
630# returned object supports ->path
631sub _request_uri ($request) {
632 $request->isa('HTTP::Request') ? $request->uri
633 : $request->isa('Mojo::Message::Request') ? $request->url
634 : croak 'unknown type '.ref($request);
635}
636
637# returns a list of key-value pairs (beware of treating as a hash!)
638sub _query_pairs ($uri) {
639 $uri->isa('URI') ? $uri->query_form
640 : $uri->isa('Mojo::URL') ? $uri->query->pairs->@*
641 : croak 'unknown type '.ref($uri);
642}
643
644# note: this assumes that the header values were already normalized on creation,
645# as sanitizing on read is bypassed
646# beware: the lwp version is list/scalar-context-sensitive
647sub _header ($message, $header_name) {
648 $message->isa('HTTP::Message') ? $message->headers->header($header_name)
649 : $message->isa('Mojo::Message') ? $message->content->headers->header($header_name) // ()
650 : croak 'unknown type '.ref($message);
651}
652
653# normalized, with extensions stripped
654sub _content_type ($message) {
655 $message->isa('HTTP::Message') ? fc $message->headers->content_type
656 : $message->isa('Mojo::Message') ? fc((split(/;/, $message->headers->content_type//'', 2))[0] // '')
657 : croak 'unknown type '.ref($message);
658}
659
660sub _content_charset ($message) {
661 $message->isa('HTTP::Message') ? $message->headers->content_type_charset
662 : $message->isa('Mojo::Message') ? $message->content->charset
663 : croak 'unknown type '.ref($message);
664}
665
666sub _body_size ($message) {
667 $message->isa('HTTP::Message') ? $message->headers->content_length // length $message->content_ref->$*
668 : $message->isa('Mojo::Message') ? $message->headers->content_length // $message->body_size
669 : croak 'unknown type '.ref($message);
670}
671
672sub _content_ref ($message) {
673 $message->isa('HTTP::Message') ? $message->content_ref
674 : $message->isa('Mojo::Message') ? \$message->body
675 : croak 'unknown type '.ref($message);
676}
677
678# wrappers that aren't needed (yet), because they are the same across all supported classes:
679# $request->method
680# $response->code
681# $uri->path
682
683117µs1;
684
685138µs1645µs__END__
 
# spent 0s within OpenAPI::Modern::__ANON__ which was called: # once (0s+0s) by OpenAPI::Modern::BEGIN@26 at line 26
sub OpenAPI::Modern::__ANON__; # xsub