Filename | /Users/ether/.perlbrew/libs/36.0@std/lib/perl5/OpenAPI/Modern.pm |
Statements | Executed 74 statements in 14.1ms |
Calls | P | F | Exclusive Time |
Inclusive Time |
Subroutine |
---|---|---|---|---|---|
1 | 1 | 1 | 8.48ms | 619ms | BEGIN@26 | OpenAPI::Modern::
1 | 1 | 1 | 4.14ms | 23.9ms | BEGIN@11 | OpenAPI::Modern::
1 | 1 | 1 | 3.43ms | 21.1ms | BEGIN@24 | OpenAPI::Modern::
1 | 1 | 1 | 2.88ms | 15.8ms | BEGIN@28 | OpenAPI::Modern::
1 | 1 | 1 | 2.43ms | 3.95ms | BEGIN@20 | OpenAPI::Modern::
1 | 1 | 1 | 2.19ms | 3.29ms | BEGIN@12 | OpenAPI::Modern::
1 | 1 | 1 | 1.72ms | 8.98ms | BEGIN@13 | OpenAPI::Modern::
1 | 1 | 1 | 1.53ms | 1.65ms | BEGIN@25 | OpenAPI::Modern::
1 | 1 | 1 | 1.44ms | 3.75ms | BEGIN@19 | OpenAPI::Modern::
1 | 1 | 1 | 809µs | 887µs | BEGIN@14 | OpenAPI::Modern::
1 | 1 | 1 | 803µs | 2.54ms | BEGIN@23 | OpenAPI::Modern::
1 | 1 | 1 | 664µs | 104s | __ANON__[:79] | OpenAPI::Modern::
1 | 1 | 1 | 68µs | 72µs | BEGIN@1.2 | main::
1 | 1 | 1 | 28µs | 28µs | BEGIN@10 | OpenAPI::Modern::
1 | 1 | 1 | 16µs | 73µs | BEGIN@32 | OpenAPI::Modern::
1 | 1 | 1 | 15µs | 69µs | BEGIN@27 | OpenAPI::Modern::
1 | 1 | 1 | 13µs | 38µs | BEGIN@15 | OpenAPI::Modern::
1 | 1 | 1 | 13µs | 518µs | BEGIN@30 | OpenAPI::Modern::
1 | 1 | 1 | 11µs | 29µs | BEGIN@16 | OpenAPI::Modern::
1 | 1 | 1 | 9µs | 25µs | BEGIN@17 | OpenAPI::Modern::
1 | 1 | 1 | 8µs | 76µs | BEGIN@21 | OpenAPI::Modern::
1 | 1 | 1 | 8µs | 75µs | BEGIN@29 | OpenAPI::Modern::
1 | 1 | 1 | 8µs | 457µs | BEGIN@33 | OpenAPI::Modern::
1 | 1 | 1 | 7µs | 43µs | BEGIN@2.3 | main::
1 | 1 | 1 | 6µs | 384µs | BEGIN@31 | OpenAPI::Modern::
1 | 1 | 1 | 5µs | 30µs | BEGIN@18 | OpenAPI::Modern::
1 | 1 | 1 | 5µs | 14µs | BEGIN@22 | OpenAPI::Modern::
1 | 1 | 1 | 0s | 0s | __ANON__ (xsub) | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | __ANON__[:206] | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | __ANON__[:476] | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | __ANON__[:477] | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | __ANON__[:515] | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | _body_size | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | _content_charset | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | _content_ref | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | _content_type | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | _evaluate_subschema | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | _header | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | _query_pairs | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | _request_uri | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | _resolve_ref | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | _result | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | _validate_body_content | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | _validate_cookie_parameter | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | _validate_header_parameter | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | _validate_parameter_content | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | _validate_path_parameter | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | _validate_query_parameter | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | find_path | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | validate_request | OpenAPI::Modern::
0 | 0 | 0 | 0s | 0s | validate_response | OpenAPI::Modern::
Line | State ments |
Time on line |
Calls | Time in subs |
Code |
---|---|---|---|---|---|
1 | 2 | 44µs | 2 | 76µ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 # spent 72µs making 1 call to main::BEGIN@1.2
# spent 4µs making 1 call to strict::import |
2 | 2 | 52µs | 2 | 79µ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 # spent 43µs making 1 call to main::BEGIN@2.3
# spent 36µs making 1 call to warnings::import |
3 | package 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 | |||||
8 | 1 | 0s | our $VERSION = '0.031'; | ||
9 | |||||
10 | 2 | 52µs | 1 | 28µs | # spent 28µs within OpenAPI::Modern::BEGIN@10 which was called:
# once (28µs+0s) by main::BEGIN@2 at line 10 # spent 28µs making 1 call to OpenAPI::Modern::BEGIN@10 |
11 | 2 | 757µs | 2 | 26.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 # spent 23.9ms making 1 call to OpenAPI::Modern::BEGIN@11
# spent 2.28ms making 1 call to Moo::import |
12 | 3 | 1.06ms | 3 | 4.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 # 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 |
13 | 2 | 680µs | 2 | 9.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 # spent 8.98ms making 1 call to OpenAPI::Modern::BEGIN@13
# spent 160µs making 1 call to experimental::import |
14 | 2 | 662µs | 2 | 891µ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 # spent 887µs making 1 call to OpenAPI::Modern::BEGIN@14
# spent 4µs making 1 call to if::import |
15 | 2 | 32µs | 2 | 40µ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 # spent 38µs making 1 call to OpenAPI::Modern::BEGIN@15
# spent 2µs making 1 call to if::unimport |
16 | 2 | 31µs | 2 | 31µ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 # spent 29µs making 1 call to OpenAPI::Modern::BEGIN@16
# spent 2µs making 1 call to if::unimport |
17 | 2 | 20µs | 2 | 27µ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 # spent 25µs making 1 call to OpenAPI::Modern::BEGIN@17
# spent 2µs making 1 call to if::unimport |
18 | 2 | 20µs | 2 | 55µ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 # spent 30µs making 1 call to OpenAPI::Modern::BEGIN@18
# spent 25µs making 1 call to Exporter::import |
19 | 2 | 1.09ms | 2 | 5.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 # spent 3.75ms making 1 call to OpenAPI::Modern::BEGIN@19
# spent 2.20ms making 1 call to Exporter::import |
20 | 2 | 935µs | 2 | 4.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 # spent 3.95ms making 1 call to OpenAPI::Modern::BEGIN@20
# spent 63µs making 1 call to Exporter::import |
21 | 2 | 17µs | 2 | 82µ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 # spent 76µs making 1 call to OpenAPI::Modern::BEGIN@21
# spent 6µs making 1 call to List::Util::import |
22 | 2 | 16µs | 2 | 23µ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 # spent 14µs making 1 call to OpenAPI::Modern::BEGIN@22
# spent 9µs making 1 call to Exporter::import |
23 | 2 | 621µs | 2 | 2.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 # spent 2.54ms making 1 call to OpenAPI::Modern::BEGIN@23
# spent 32µs making 1 call to Feature::Compat::Try::import |
24 | 3 | 491µs | 3 | 21.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 # 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 |
25 | 2 | 534µs | 1 | 1.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 # spent 1.65ms making 1 call to OpenAPI::Modern::BEGIN@25 |
26 | 3 | 819µs | 3 | 619ms | # spent 619ms (8.48+611) within OpenAPI::Modern::BEGIN@26 which was called:
# once (8.48ms+611ms) by main::BEGIN@2 at line 26 # 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__ |
27 | 3 | 39µs | 3 | 123µ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 # 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 |
28 | 2 | 718µs | 2 | 15.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 # spent 15.8ms making 1 call to OpenAPI::Modern::BEGIN@28
# spent 4µs making 1 call to Mojo::Base::import |
29 | 2 | 30µs | 2 | 142µ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 # spent 75µs making 1 call to OpenAPI::Modern::BEGIN@29
# spent 67µs making 1 call to MooX::HandlesVia::import |
30 | 3 | 47µs | 3 | 1.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 # 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 |
31 | 2 | 40µs | 2 | 762µ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 # spent 384µs making 1 call to OpenAPI::Modern::BEGIN@31
# spent 378µs making 1 call to Exporter::Tiny::import |
32 | 2 | 28µs | 4 | 130µ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 # 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 |
33 | 2 | 4.52ms | 2 | 906µ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 # spent 457µs making 1 call to OpenAPI::Modern::BEGIN@33
# spent 449µs making 1 call to namespace::clean::import |
34 | |||||
35 | 1 | 10µs | 2 | 2.72ms | has 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 | ||||
47 | 1 | 6µs | 2 | 1.36ms | has 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 | |||||
54 | 4 | 526µ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] | ||
55 | 1 | 5µs | 1 | 10µs | my $args = $class->$orig(@args); # spent 10µs making 1 call to Moo::Object::BUILDARGS |
56 | |||||
57 | 1 | 2µ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' | ||||
63 | 1 | 0s | if not exists $args->{openapi_uri}; | ||
64 | croak 'missing required constructor arguments: either openapi_document, or openapi_schema' | ||||
65 | 1 | 0s | if not exists $args->{openapi_schema}; | ||
66 | |||||
67 | 1 | 6µs | 1 | 12.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}, | ||||
72 | 1 | 23µs | 1 | 5.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 | ||||
75 | 1 | 21µs | 1 | 7.91ms | $args->{evaluator}->add_schema($args->{openapi_document}); # spent 7.91ms making 1 call to JSON::Schema::Modern::add_schema |
76 | } | ||||
77 | |||||
78 | 1 | 45µs | return $args; | ||
79 | 1 | 7µs | 1 | 225µs | }; # spent 225µs making 1 call to Moo::around |
80 | |||||
81 | sub 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 | |||||
182 | sub 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 | |||||
252 | sub 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 | |||||
387 | sub _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 | |||||
395 | sub _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 | ||||
418 | sub _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 | |||||
433 | sub _validate_cookie_parameter ($self, $state, $param_obj, $request) { | ||||
434 | return E($state, 'cookie parameters not yet supported'); | ||||
435 | } | ||||
436 | |||||
437 | sub _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 | |||||
469 | sub _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 | ||||
553 | sub _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 | |||||
565 | sub _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 | ||||
582 | sub _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 | ||||
631 | sub _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!) | ||||
638 | sub _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 | ||||
647 | sub _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 | ||||
654 | sub _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 | |||||
660 | sub _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 | |||||
666 | sub _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 | |||||
672 | sub _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 | |||||
683 | 1 | 17µs | 1; | ||
684 | |||||
685 | 1 | 38µs | 1 | 645µs | __END__ # spent 645µs making 1 call to B::Hooks::EndOfScope::XS::__ANON__[B/Hooks/EndOfScope/XS.pm:26] |
# spent 0s within OpenAPI::Modern::__ANON__ which was called:
# once (0s+0s) by OpenAPI::Modern::BEGIN@26 at line 26 |