| Filename | /Users/ether/.perlbrew/libs/36.0@std/lib/perl5/OpenAPI/Modern.pm |
| Statements | Executed 74 statements in 11.7ms |
| Calls | P | F | Exclusive Time |
Inclusive Time |
Subroutine |
|---|---|---|---|---|---|
| 1 | 1 | 1 | 9.41ms | 585ms | OpenAPI::Modern::BEGIN@26 |
| 1 | 1 | 1 | 5.37ms | 25.6ms | OpenAPI::Modern::BEGIN@11 |
| 1 | 1 | 1 | 2.29ms | 12.2ms | OpenAPI::Modern::BEGIN@24 |
| 1 | 1 | 1 | 2.25ms | 13.9ms | OpenAPI::Modern::BEGIN@28 |
| 1 | 1 | 1 | 2.16ms | 8.26ms | OpenAPI::Modern::BEGIN@13 |
| 1 | 1 | 1 | 2.00ms | 3.38ms | OpenAPI::Modern::BEGIN@12 |
| 1 | 1 | 1 | 1.45ms | 2.61ms | OpenAPI::Modern::BEGIN@20 |
| 1 | 1 | 1 | 978µs | 1.07ms | OpenAPI::Modern::BEGIN@25 |
| 1 | 1 | 1 | 920µs | 2.20ms | OpenAPI::Modern::BEGIN@19 |
| 1 | 1 | 1 | 773µs | 2.05ms | OpenAPI::Modern::BEGIN@23 |
| 1 | 1 | 1 | 383µs | 431µs | OpenAPI::Modern::BEGIN@14 |
| 1 | 1 | 1 | 85µs | 108s | OpenAPI::Modern::__ANON__[:79] |
| 1 | 1 | 1 | 28µs | 28µs | OpenAPI::Modern::BEGIN@10 |
| 1 | 1 | 1 | 27µs | 31µs | main::BEGIN@1.2 |
| 1 | 1 | 1 | 22µs | 138µs | OpenAPI::Modern::BEGIN@27 |
| 1 | 1 | 1 | 11µs | 56µs | OpenAPI::Modern::BEGIN@32 |
| 1 | 1 | 1 | 10µs | 63µs | OpenAPI::Modern::BEGIN@21 |
| 1 | 1 | 1 | 9µs | 302µs | OpenAPI::Modern::BEGIN@30 |
| 1 | 1 | 1 | 8µs | 14µs | OpenAPI::Modern::BEGIN@16 |
| 1 | 1 | 1 | 7µs | 26µs | OpenAPI::Modern::BEGIN@15 |
| 1 | 1 | 1 | 7µs | 17µs | OpenAPI::Modern::BEGIN@17 |
| 1 | 1 | 1 | 6µs | 266µs | OpenAPI::Modern::BEGIN@31 |
| 1 | 1 | 1 | 6µs | 72µs | main::BEGIN@2.3 |
| 1 | 1 | 1 | 5µs | 27µs | OpenAPI::Modern::BEGIN@18 |
| 1 | 1 | 1 | 5µs | 16µs | OpenAPI::Modern::BEGIN@22 |
| 1 | 1 | 1 | 5µs | 319µs | OpenAPI::Modern::BEGIN@33 |
| 1 | 1 | 1 | 3µs | 54µs | OpenAPI::Modern::BEGIN@29 |
| 1 | 1 | 1 | 1µs | 1µs | OpenAPI::Modern::__ANON__ (xsub) |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::__ANON__[:206] |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::__ANON__[:483] |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::__ANON__[:484] |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::__ANON__[:522] |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::_body_size |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::_content_charset |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::_content_ref |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::_content_type |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::_evaluate_subschema |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::_header |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::_query_pairs |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::_request_uri |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::_resolve_ref |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::_result |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::_validate_body_content |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::_validate_cookie_parameter |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::_validate_header_parameter |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::_validate_parameter_content |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::_validate_path_parameter |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::_validate_query_parameter |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::find_path |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::validate_request |
| 0 | 0 | 0 | 0s | 0s | OpenAPI::Modern::validate_response |
| Line | State ments |
Time on line |
Calls | Time in subs |
Code |
|---|---|---|---|---|---|
| 1 | 2 | 32µs | 2 | 35µs | # spent 31µs (27+4) within main::BEGIN@1.2 which was called:
# once (27µs+4µs) by main::BEGIN@2 at line 1 # spent 31µs making 1 call to main::BEGIN@1.2
# spent 4µs making 1 call to strict::import |
| 2 | 2 | 107µs | 2 | 138µs | # spent 72µs (6+66) within main::BEGIN@2.3 which was called:
# once (6µs+66µs) by main::BEGIN@2 at line 2 # spent 72µs making 1 call to main::BEGIN@2.3
# spent 66µs making 1 call to warnings::import |
| 3 | package OpenAPI::Modern; # git description: v0.033-3-gf2ff940 | ||||
| 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 | 1µs | our $VERSION = '0.034'; | ||
| 9 | |||||
| 10 | 2 | 58µ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 | 1.07ms | 2 | 29.3ms | # spent 25.6ms (5.37+20.3) within OpenAPI::Modern::BEGIN@11 which was called:
# once (5.37ms+20.3ms) by main::BEGIN@2 at line 11 # spent 25.6ms making 1 call to OpenAPI::Modern::BEGIN@11
# spent 3.71ms making 1 call to Moo::import |
| 12 | 3 | 679µs | 3 | 4.64ms | # spent 3.38ms (2.00+1.38) within OpenAPI::Modern::BEGIN@12 which was called:
# once (2.00ms+1.38ms) by main::BEGIN@2 at line 12 # spent 3.38ms making 1 call to OpenAPI::Modern::BEGIN@12
# spent 1.23ms making 1 call to strictures::import
# spent 35µs making 1 call to strictures::VERSION |
| 13 | 2 | 991µs | 2 | 8.44ms | # spent 8.26ms (2.16+6.10) within OpenAPI::Modern::BEGIN@13 which was called:
# once (2.16ms+6.10ms) by main::BEGIN@2 at line 13 # spent 8.26ms making 1 call to OpenAPI::Modern::BEGIN@13
# spent 178µs making 1 call to experimental::import |
| 14 | 2 | 265µs | 2 | 433µs | # spent 431µs (383+48) within OpenAPI::Modern::BEGIN@14 which was called:
# once (383µs+48µs) by main::BEGIN@2 at line 14 # spent 431µs making 1 call to OpenAPI::Modern::BEGIN@14
# spent 2µs making 1 call to if::import |
| 15 | 2 | 19µs | 2 | 28µs | # spent 26µs (7+19) within OpenAPI::Modern::BEGIN@15 which was called:
# once (7µs+19µs) by main::BEGIN@2 at line 15 # spent 26µs making 1 call to OpenAPI::Modern::BEGIN@15
# spent 2µs making 1 call to if::unimport |
| 16 | 2 | 19µs | 2 | 14µs | # spent 14µs (8+6) within OpenAPI::Modern::BEGIN@16 which was called:
# once (8µs+6µs) by main::BEGIN@2 at line 16 # spent 14µs making 1 call to OpenAPI::Modern::BEGIN@16
# spent 0s making 1 call to if::unimport |
| 17 | 2 | 13µs | 2 | 18µs | # spent 17µs (7+10) within OpenAPI::Modern::BEGIN@17 which was called:
# once (7µs+10µs) by main::BEGIN@2 at line 17 # spent 17µs making 1 call to OpenAPI::Modern::BEGIN@17
# spent 1µs making 1 call to if::unimport |
| 18 | 2 | 15µs | 2 | 49µs | # spent 27µs (5+22) within OpenAPI::Modern::BEGIN@18 which was called:
# once (5µs+22µs) by main::BEGIN@2 at line 18 # spent 27µs making 1 call to OpenAPI::Modern::BEGIN@18
# spent 22µs making 1 call to Exporter::import |
| 19 | 2 | 547µs | 2 | 3.40ms | # spent 2.20ms (920µs+1.28) within OpenAPI::Modern::BEGIN@19 which was called:
# once (920µs+1.28ms) by main::BEGIN@2 at line 19 # spent 2.20ms making 1 call to OpenAPI::Modern::BEGIN@19
# spent 1.20ms making 1 call to Exporter::import |
| 20 | 2 | 489µs | 2 | 2.68ms | # spent 2.61ms (1.45+1.16) within OpenAPI::Modern::BEGIN@20 which was called:
# once (1.45ms+1.16ms) by main::BEGIN@2 at line 20 # spent 2.61ms making 1 call to OpenAPI::Modern::BEGIN@20
# spent 68µs making 1 call to Exporter::import |
| 21 | 2 | 18µs | 2 | 73µs | # spent 63µs (10+53) within OpenAPI::Modern::BEGIN@21 which was called:
# once (10µs+53µs) by main::BEGIN@2 at line 21 # spent 63µs making 1 call to OpenAPI::Modern::BEGIN@21
# spent 10µs making 1 call to List::Util::import |
| 22 | 2 | 12µs | 2 | 27µs | # spent 16µs (5+11) within OpenAPI::Modern::BEGIN@22 which was called:
# once (5µs+11µs) by main::BEGIN@2 at line 22 # spent 16µs making 1 call to OpenAPI::Modern::BEGIN@22
# spent 11µs making 1 call to Exporter::import |
| 23 | 2 | 552µs | 2 | 2.09ms | # spent 2.05ms (773µs+1.28) within OpenAPI::Modern::BEGIN@23 which was called:
# once (773µs+1.28ms) by main::BEGIN@2 at line 23 # spent 2.05ms making 1 call to OpenAPI::Modern::BEGIN@23
# spent 42µs making 1 call to Feature::Compat::Try::import |
| 24 | 3 | 234µs | 3 | 12.3ms | # spent 12.2ms (2.29+9.93) within OpenAPI::Modern::BEGIN@24 which was called:
# once (2.29ms+9.93ms) by main::BEGIN@2 at line 24 # spent 12.2ms making 1 call to OpenAPI::Modern::BEGIN@24
# spent 40µs making 1 call to Exporter::import
# spent 10µs making 1 call to UNIVERSAL::VERSION |
| 25 | 2 | 188µs | 1 | 1.07ms | # spent 1.07ms (978µs+96µs) within OpenAPI::Modern::BEGIN@25 which was called:
# once (978µs+96µs) by main::BEGIN@2 at line 25 # spent 1.07ms making 1 call to OpenAPI::Modern::BEGIN@25 |
| 26 | 3 | 564µs | 3 | 585ms | # spent 585ms (9.41+575) within OpenAPI::Modern::BEGIN@26 which was called:
# once (9.41ms+575ms) by main::BEGIN@2 at line 26 # spent 585ms making 1 call to OpenAPI::Modern::BEGIN@26
# spent 7µs making 1 call to UNIVERSAL::VERSION
# spent 1µs making 1 call to OpenAPI::Modern::__ANON__ |
| 27 | 3 | 55µs | 3 | 254µs | # spent 138µs (22+116) within OpenAPI::Modern::BEGIN@27 which was called:
# once (22µs+116µs) by main::BEGIN@2 at line 27 # spent 138µs making 1 call to OpenAPI::Modern::BEGIN@27
# spent 107µs making 1 call to Exporter::import
# spent 9µs making 1 call to UNIVERSAL::VERSION |
| 28 | 2 | 272µs | 2 | 13.9ms | # spent 13.9ms (2.25+11.6) within OpenAPI::Modern::BEGIN@28 which was called:
# once (2.25ms+11.6ms) by main::BEGIN@2 at line 28 # spent 13.9ms making 1 call to OpenAPI::Modern::BEGIN@28
# spent 4µs making 1 call to Mojo::Base::import |
| 29 | 2 | 17µs | 2 | 105µs | # spent 54µs (3+51) within OpenAPI::Modern::BEGIN@29 which was called:
# once (3µs+51µs) by main::BEGIN@2 at line 29 # spent 54µs making 1 call to OpenAPI::Modern::BEGIN@29
# spent 51µs making 1 call to MooX::HandlesVia::import |
| 30 | 3 | 32µs | 3 | 595µs | # spent 302µs (9+293) within OpenAPI::Modern::BEGIN@30 which was called:
# once (9µs+293µs) by main::BEGIN@2 at line 30 # spent 302µs making 1 call to OpenAPI::Modern::BEGIN@30
# spent 285µs making 1 call to MooX::TypeTiny::import
# spent 8µs making 1 call to UNIVERSAL::VERSION |
| 31 | 2 | 31µs | 2 | 526µs | # spent 266µs (6+260) within OpenAPI::Modern::BEGIN@31 which was called:
# once (6µs+260µs) by main::BEGIN@2 at line 31 # spent 266µs making 1 call to OpenAPI::Modern::BEGIN@31
# spent 260µs making 1 call to Exporter::Tiny::import |
| 32 | 2 | 20µs | 4 | 101µs | # spent 56µs (11+45) within OpenAPI::Modern::BEGIN@32 which was called:
# once (11µs+45µs) by main::BEGIN@2 at line 32 # spent 56µs making 1 call to OpenAPI::Modern::BEGIN@32
# spent 43µ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 | 5.18ms | 2 | 633µs | # spent 319µs (5+314) within OpenAPI::Modern::BEGIN@33 which was called:
# once (5µs+314µs) by main::BEGIN@2 at line 33 # spent 319µs making 1 call to OpenAPI::Modern::BEGIN@33
# spent 314µs making 1 call to namespace::clean::import |
| 34 | |||||
| 35 | 1 | 10µs | 2 | 4.03ms | has openapi_document => ( # spent 3.39ms making 1 call to OpenAPI::Modern::has
# spent 643µ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 | 11µs | 2 | 2.80ms | has evaluator => ( # spent 2.68ms making 1 call to OpenAPI::Modern::has
# spent 118µ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 | 6µs | # spent 108s (85µs+108) within OpenAPI::Modern::__ANON__[/Users/ether/.perlbrew/libs/36.0@std/lib/perl5/OpenAPI/Modern.pm:79] which was called:
# once (85µs+108s) 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 | 4µs | 1 | 5µs | my $args = $class->$orig(@args); # spent 5µs making 1 call to Moo::Object::BUILDARGS |
| 56 | |||||
| 57 | 1 | 1µ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 | 1µs | 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 | 7µs | 1 | 17.1ms | $args->{evaluator} //= JSON::Schema::Modern->new(validate_formats => 1, max_traversal_depth => 80); # spent 17.1ms 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 | 16µs | 1 | 6.17ms | ); # spent 6.17ms 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 | 5µs | 1 | 9.92ms | $args->{evaluator}->add_schema($args->{openapi_document}); # spent 9.92ms making 1 call to JSON::Schema::Modern::add_schema |
| 76 | } | ||||
| 77 | |||||
| 78 | 1 | 12µs | return $args; | ||
| 79 | 1 | 5µs | 1 | 241µs | }; # spent 241µ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 | # sorting (ascii-wise) gives us the desired results that concrete path components sort ahead of | ||||
| 316 | # templated components, except when the concrete component is a non-ascii character or matches [|}~]. | ||||
| 317 | foreach $path_template (sort keys $schema->{paths}->%*) { | ||||
| 318 | my $path_pattern = $path_template =~ s!\{[^/}]+\}!([^/?#]*)!gr; | ||||
| 319 | next if $uri_path !~ m/^$path_pattern$/; | ||||
| 320 | |||||
| 321 | $options->{path_template} = $path_template; | ||||
| 322 | |||||
| 323 | # perldoc perlvar, @-: $n coincides with "substr $_, $-[n], $+[n] - $-[n]" if "$-[n]" is defined | ||||
| 324 | my @capture_values = map | ||||
| 325 | Encode::decode('UTF-8', URI::Escape::uri_unescape(substr($uri_path, $-[$_], $+[$_]-$-[$_]))), 1 .. $#-; | ||||
| 326 | my @capture_names = ($path_template =~ m!\{([^/?#}]+)\}!g); | ||||
| 327 | my %path_captures; @path_captures{@capture_names} = @capture_values; | ||||
| 328 | |||||
| 329 | my $indexes = []; | ||||
| 330 | return E({ %$state, keyword => 'paths' }, 'duplicate path capture name %s', $capture_names[$indexes->[0]]) | ||||
| 331 | if not is_elements_unique(\@capture_names, $indexes); | ||||
| 332 | |||||
| 333 | return E({ %$state, keyword => 'paths' }, 'provided path_captures values do not match request URI') | ||||
| 334 | if $options->{path_captures} and not is_equal($options->{path_captures}, \%path_captures); | ||||
| 335 | |||||
| 336 | $options->{path_captures} = \%path_captures; | ||||
| 337 | return E({ %$state, data_path => '/request/method', schema_path => jsonp('/paths', $path_template), keyword => $method }, | ||||
| 338 | 'missing operation for HTTP method "%s"', $method) | ||||
| 339 | if not exists $schema->{paths}{$path_template}{$method}; | ||||
| 340 | |||||
| 341 | $options->{operation_id} = $self->openapi_document->schema->{paths}{$path_template}{$method}{operationId}; | ||||
| 342 | delete $options->{operation_id} if not defined $options->{operation_id}; | ||||
| 343 | return 1; | ||||
| 344 | } | ||||
| 345 | |||||
| 346 | return E({ %$state, keyword => 'paths' }, 'no match found for URI path "%s"', $uri_path); | ||||
| 347 | } | ||||
| 348 | |||||
| 349 | croak 'at least one of request, $options->{path_template} and $options->{operation_id} must be provided' | ||||
| 350 | if not $path_template; | ||||
| 351 | |||||
| 352 | # note: we aren't doing anything special with escaped slashes. this bit of the spec is hazy. | ||||
| 353 | my @capture_names = ($path_template =~ m!\{([^/}]+)\}!g); | ||||
| 354 | return E({ %$state, keyword => 'paths', _schema_path_suffix => $path_template }, | ||||
| 355 | 'provided path_captures names do not match path template "%s"', $path_template) | ||||
| 356 | if exists $options->{path_captures} | ||||
| 357 | and not is_equal([ sort keys $options->{path_captures}->%* ], [ sort @capture_names ]); | ||||
| 358 | |||||
| 359 | if (not $request) { | ||||
| 360 | $options->@{qw(path_template operation_id)} = | ||||
| 361 | ($path_template, $self->openapi_document->schema->{paths}{$path_template}{$method}{operationId}); | ||||
| 362 | delete $options->{operation_id} if not defined $options->{operation_id}; | ||||
| 363 | return 1; | ||||
| 364 | } | ||||
| 365 | |||||
| 366 | # if we're still here, we were passed path_template in options or we calculated it from | ||||
| 367 | # operation_id, and now we verify it against path_captures and the request URI. | ||||
| 368 | my $uri_path = _request_uri($request)->path; | ||||
| 369 | |||||
| 370 | # 3.2: "The value for these path parameters MUST NOT contain any unescaped “generic syntax” | ||||
| 371 | # characters described by [RFC3986]: forward slashes (/), question marks (?), or hashes (#)." | ||||
| 372 | my $path_pattern = $path_template =~ s!\{[^/}]+\}!([^/?#]*)!gr; | ||||
| 373 | return E({ %$state, keyword => 'paths', _schema_path_suffix => $path_template }, | ||||
| 374 | 'provided %s does not match request URI', exists $options->{path_template} ? 'path_template' : 'operation_id') | ||||
| 375 | if $uri_path !~ m/^$path_pattern$/; | ||||
| 376 | |||||
| 377 | # perldoc perlvar, @-: $n coincides with "substr $_, $-[n], $+[n] - $-[n]" if "$-[n]" is defined | ||||
| 378 | my @capture_values = map | ||||
| 379 | Encode::decode('UTF-8', URI::Escape::uri_unescape(substr($uri_path, $-[$_], $+[$_]-$-[$_]))), 1 .. $#-; | ||||
| 380 | return E({ %$state, keyword => 'paths', _schema_path_suffix => $path_template }, | ||||
| 381 | 'provided path_captures values do not match request URI') | ||||
| 382 | if exists $options->{path_captures} | ||||
| 383 | and not is_equal([ map $_.'', $options->{path_captures}->@{@capture_names} ], \@capture_values); | ||||
| 384 | |||||
| 385 | my %path_captures; @path_captures{@capture_names} = @capture_values; | ||||
| 386 | $options->@{qw(path_template path_captures operation_id)} = | ||||
| 387 | ($path_template, \%path_captures, $self->openapi_document->schema->{paths}{$path_template}{$method}{operationId}); | ||||
| 388 | delete $options->{operation_id} if not defined $options->{operation_id}; | ||||
| 389 | return 1; | ||||
| 390 | } | ||||
| 391 | |||||
| 392 | ######## NO PUBLIC INTERFACES FOLLOW THIS POINT ######## | ||||
| 393 | |||||
| 394 | sub _validate_path_parameter ($self, $state, $param_obj, $path_captures) { | ||||
| 395 | # 'required' is always true for path parameters | ||||
| 396 | return E({ %$state, keyword => 'required' }, 'missing path parameter: %s', $param_obj->{name}) | ||||
| 397 | if not exists $path_captures->{$param_obj->{name}}; | ||||
| 398 | |||||
| 399 | $self->_validate_parameter_content($state, $param_obj, \ $path_captures->{$param_obj->{name}}); | ||||
| 400 | } | ||||
| 401 | |||||
| 402 | sub _validate_query_parameter ($self, $state, $param_obj, $uri) { | ||||
| 403 | # parse the query parameters out of uri | ||||
| 404 | my $query_params = { _query_pairs($uri) }; | ||||
| 405 | |||||
| 406 | # TODO: support different styles. | ||||
| 407 | # for now, we only support style=form and do not allow for multiple values per | ||||
| 408 | # property (i.e. 'explode' is not checked at all.) | ||||
| 409 | # (other possible style values: spaceDelimited, pipeDelimited, deepObject) | ||||
| 410 | |||||
| 411 | if (not exists $query_params->{$param_obj->{name}}) { | ||||
| 412 | return E({ %$state, keyword => 'required' }, 'missing query parameter: %s', $param_obj->{name}) | ||||
| 413 | if $param_obj->{required}; | ||||
| 414 | return 1; | ||||
| 415 | } | ||||
| 416 | |||||
| 417 | # TODO: check 'allowReserved': if true, do not use percent-decoding | ||||
| 418 | return E({ %$state, keyword => 'allowReserved' }, 'allowReserved: true is not yet supported') | ||||
| 419 | if $param_obj->{allowReserved} // 0; | ||||
| 420 | |||||
| 421 | $self->_validate_parameter_content($state, $param_obj, \ $query_params->{$param_obj->{name}}); | ||||
| 422 | } | ||||
| 423 | |||||
| 424 | # validates a header, from either the request or the response | ||||
| 425 | sub _validate_header_parameter ($self, $state, $header_name, $header_obj, $headers) { | ||||
| 426 | return 1 if grep fc $header_name eq fc $_, qw(Accept Content-Type Authorization); | ||||
| 427 | |||||
| 428 | # NOTE: for now, we will only support a single header value. | ||||
| 429 | @$headers = map s/^\s*//r =~ s/\s*$//r, @$headers; | ||||
| 430 | |||||
| 431 | if (not @$headers) { | ||||
| 432 | return E({ %$state, keyword => 'required' }, 'missing header: %s', $header_name) | ||||
| 433 | if $header_obj->{required}; | ||||
| 434 | return 1; | ||||
| 435 | } | ||||
| 436 | |||||
| 437 | $self->_validate_parameter_content($state, $header_obj, \ $headers->[0]); | ||||
| 438 | } | ||||
| 439 | |||||
| 440 | sub _validate_cookie_parameter ($self, $state, $param_obj, $request) { | ||||
| 441 | return E($state, 'cookie parameters not yet supported'); | ||||
| 442 | } | ||||
| 443 | |||||
| 444 | sub _validate_parameter_content ($self, $state, $param_obj, $content_ref) { | ||||
| 445 | if (exists $param_obj->{content}) { | ||||
| 446 | abort({ %$state, keyword => 'content' }, 'more than one media type entry present') | ||||
| 447 | if keys $param_obj->{content}->%* > 1; # TODO: remove, when the spec schema is updated | ||||
| 448 | my ($media_type) = keys $param_obj->{content}->%*; # there can only be one key | ||||
| 449 | my $schema = $param_obj->{content}{$media_type}{schema}; | ||||
| 450 | |||||
| 451 | my $media_type_decoder = $self->get_media_type($media_type); # case-insensitive, wildcard lookup | ||||
| 452 | if (not $media_type_decoder) { | ||||
| 453 | # don't fail if the schema would pass on any input | ||||
| 454 | return if is_plain_hashref($schema) ? !keys %$schema : $schema; | ||||
| 455 | |||||
| 456 | abort({ %$state, keyword => 'content', _schema_path_suffix => $media_type}, | ||||
| 457 | 'EXCEPTION: unsupported media type "%s": add support with $openapi->add_media_type(...)', $media_type) | ||||
| 458 | } | ||||
| 459 | |||||
| 460 | try { | ||||
| 461 | $content_ref = $media_type_decoder->($content_ref); | ||||
| 462 | } | ||||
| 463 | catch ($e) { | ||||
| 464 | return E({ %$state, keyword => 'content', _schema_path_suffix => $media_type }, | ||||
| 465 | 'could not decode content as %s: %s', $media_type, $e =~ s/^(.*)\n/$1/r); | ||||
| 466 | } | ||||
| 467 | |||||
| 468 | $state = { %$state, schema_path => jsonp($state->{schema_path}, 'content', $media_type, 'schema') }; | ||||
| 469 | return $self->_evaluate_subschema($content_ref->$*, $schema, $state); | ||||
| 470 | } | ||||
| 471 | |||||
| 472 | $state = { %$state, schema_path => jsonp($state->{schema_path}, 'schema') }; | ||||
| 473 | $self->_evaluate_subschema($content_ref->$*, $param_obj->{schema}, $state); | ||||
| 474 | } | ||||
| 475 | |||||
| 476 | sub _validate_body_content ($self, $state, $content_obj, $message) { | ||||
| 477 | my $content_type = _content_type($message); | ||||
| 478 | |||||
| 479 | return E({ %$state, data_path => $state->{data_path} =~ s{body}{header/Content-Type}r, keyword => 'content' }, | ||||
| 480 | 'missing header: Content-Type') | ||||
| 481 | if not length $content_type; | ||||
| 482 | |||||
| 483 | my $media_type = (first { $content_type eq fc } keys $content_obj->%*) | ||||
| 484 | // (first { m{([^/]+)/\*$} && fc($content_type) =~ m{^\F\Q$1\E/[^/]+$} } keys $content_obj->%*); | ||||
| 485 | $media_type = '*/*' if not defined $media_type and exists $content_obj->{'*/*'}; | ||||
| 486 | return E({ %$state, keyword => 'content' }, 'incorrect Content-Type "%s"', $content_type) | ||||
| 487 | if not defined $media_type; | ||||
| 488 | |||||
| 489 | if (exists $content_obj->{$media_type}{encoding}) { | ||||
| 490 | my $state = { %$state, schema_path => jsonp($state->{schema_path}, 'content', $media_type) }; | ||||
| 491 | # 4.8.14.1 "The key, being the property name, MUST exist in the schema as a property." | ||||
| 492 | foreach my $property (sort keys $content_obj->{$media_type}{encoding}->%*) { | ||||
| 493 | ()= E({ $state, schema_path => jsonp($state->{schema_path}, 'schema', 'properties', $property) }, | ||||
| 494 | 'encoding property "%s" requires a matching property definition in the schema') | ||||
| 495 | if not exists(($content_obj->{$media_type}{schema}{properties}//{})->{$property}); | ||||
| 496 | } | ||||
| 497 | |||||
| 498 | # 4.8.14.1 "The encoding object SHALL only apply to requestBody objects when the media type is | ||||
| 499 | # multipart or application/x-www-form-urlencoded." | ||||
| 500 | return E({ %$state, keyword => 'encoding' }, 'encoding not yet supported') | ||||
| 501 | if $content_type =~ m{^multipart/} or $content_type eq 'application/x-www-form-urlencoded'; | ||||
| 502 | } | ||||
| 503 | |||||
| 504 | # TODO: handle Content-Encoding header; https://github.com/OAI/OpenAPI-Specification/issues/2868 | ||||
| 505 | my $content_ref = _content_ref($message); | ||||
| 506 | |||||
| 507 | # decode the charset | ||||
| 508 | if (my $charset = _content_charset($message)) { | ||||
| 509 | try { | ||||
| 510 | $content_ref = \ Encode::decode($charset, $content_ref->$*, Encode::FB_CROAK | Encode::LEAVE_SRC); | ||||
| 511 | } | ||||
| 512 | catch ($e) { | ||||
| 513 | return E({ %$state, keyword => 'content', _schema_path_suffix => $media_type }, | ||||
| 514 | 'could not decode content as %s: %s', $charset, $e =~ s/^(.*)\n/$1/r); | ||||
| 515 | } | ||||
| 516 | } | ||||
| 517 | |||||
| 518 | my $schema = $content_obj->{$media_type}{schema}; | ||||
| 519 | |||||
| 520 | # use the original Content-Type, NOT the possibly wildcard media type from the document | ||||
| 521 | my $media_type_decoder = $self->get_media_type($content_type); # case-insensitive, wildcard lookup | ||||
| 522 | $media_type_decoder = sub ($content_ref) { $content_ref } if $media_type eq '*/*'; | ||||
| 523 | if (not $media_type_decoder) { | ||||
| 524 | # don't fail if the schema would pass on any input | ||||
| 525 | return if not defined $schema or is_plain_hashref($schema) ? !keys %$schema : $schema; | ||||
| 526 | |||||
| 527 | abort({ %$state, keyword => 'content', _schema_path_suffix => $media_type }, | ||||
| 528 | 'EXCEPTION: unsupported Content-Type "%s": add support with $openapi->add_media_type(...)', $content_type) | ||||
| 529 | } | ||||
| 530 | |||||
| 531 | try { | ||||
| 532 | $content_ref = $media_type_decoder->($content_ref); | ||||
| 533 | } | ||||
| 534 | catch ($e) { | ||||
| 535 | return E({ %$state, keyword => 'content', _schema_path_suffix => $media_type }, | ||||
| 536 | 'could not decode content as %s: %s', $media_type, $e =~ s/^(.*)\n/$1/r); | ||||
| 537 | } | ||||
| 538 | |||||
| 539 | return if not defined $schema; | ||||
| 540 | |||||
| 541 | $state = { %$state, schema_path => jsonp($state->{schema_path}, 'content', $media_type, 'schema') }; | ||||
| 542 | $self->_evaluate_subschema($content_ref->$*, $schema, $state); | ||||
| 543 | } | ||||
| 544 | |||||
| 545 | # wrap a result object around the errors | ||||
| 546 | sub _result ($self, $state, $exception = 0) { | ||||
| 547 | return JSON::Schema::Modern::Result->new( | ||||
| 548 | output_format => $self->evaluator->output_format, | ||||
| 549 | formatted_annotations => 0, | ||||
| 550 | valid => !$state->{errors}->@*, | ||||
| 551 | $exception ? ( exception => 1 ) : (), | ||||
| 552 | !$state->{errors}->@* | ||||
| 553 | ? (annotations => $state->{annotations}//[]) | ||||
| 554 | : (errors => $state->{errors}), | ||||
| 555 | ); | ||||
| 556 | } | ||||
| 557 | |||||
| 558 | sub _resolve_ref ($self, $ref, $state) { | ||||
| 559 | my $uri = Mojo::URL->new($ref)->to_abs($state->{initial_schema_uri}); | ||||
| 560 | my $schema_info = $self->evaluator->_fetch_from_uri($uri); | ||||
| 561 | abort({ %$state, keyword => '$ref' }, 'EXCEPTION: unable to find resource %s', $uri) | ||||
| 562 | if not $schema_info; | ||||
| 563 | |||||
| 564 | abort($state, 'EXCEPTION: maximum evaluation depth exceeded') | ||||
| 565 | if $state->{depth}++ > $self->evaluator->max_traversal_depth; | ||||
| 566 | |||||
| 567 | $state->{initial_schema_uri} = $schema_info->{canonical_uri}; | ||||
| 568 | $state->{traversed_schema_path} = $state->{traversed_schema_path}.$state->{schema_path}.jsonp('/$ref'); | ||||
| 569 | $state->{schema_path} = ''; | ||||
| 570 | |||||
| 571 | return $schema_info->{schema}; | ||||
| 572 | } | ||||
| 573 | |||||
| 574 | # evaluates data against the subschema at the current state location | ||||
| 575 | sub _evaluate_subschema ($self, $data, $schema, $state) { | ||||
| 576 | # boolean schema | ||||
| 577 | if (not is_plain_hashref($schema)) { | ||||
| 578 | return 1 if $schema; | ||||
| 579 | |||||
| 580 | my @location = unjsonp($state->{data_path}); | ||||
| 581 | my $location = | ||||
| 582 | $location[-1] eq 'body' ? join(' ', @location[-2..-1]) | ||||
| 583 | : $location[-2] eq 'query' ? 'query parameter' | ||||
| 584 | : $location[-2] eq 'path' ? 'path parameter' # this should never happen | ||||
| 585 | : $location[-2] eq 'header' ? join(' ', @location[-3..-2]) | ||||
| 586 | : $location[-2]; # cookie | ||||
| 587 | return E($state, '%s not permitted', $location); | ||||
| 588 | } | ||||
| 589 | |||||
| 590 | return 1 if !keys(%$schema); # schema is {} | ||||
| 591 | |||||
| 592 | # treat numeric-looking data as a string, unless "type" explicitly requests number or integer. | ||||
| 593 | if (is_plain_hashref($schema) and exists $schema->{type} and not is_plain_arrayref($schema->{type}) | ||||
| 594 | and grep $schema->{type} eq $_, qw(number integer) and looks_like_number($data)) { | ||||
| 595 | $data = $data+0; | ||||
| 596 | } | ||||
| 597 | elsif (defined $data and not is_ref($data)) { | ||||
| 598 | $data = $data.''; | ||||
| 599 | } | ||||
| 600 | |||||
| 601 | # TODO: also handle multi-valued elements like headers and query parameters, when type=array requested | ||||
| 602 | # (and possibly coerce their numeric-looking elements as well) | ||||
| 603 | |||||
| 604 | my $result = $self->evaluator->evaluate( | ||||
| 605 | $data, canonical_uri($state), | ||||
| 606 | { | ||||
| 607 | data_path => $state->{data_path}, | ||||
| 608 | traversed_schema_path => $state->{traversed_schema_path}.$state->{schema_path}, | ||||
| 609 | effective_base_uri => $state->{effective_base_uri}, | ||||
| 610 | collect_annotations => 1, | ||||
| 611 | }, | ||||
| 612 | ); | ||||
| 613 | |||||
| 614 | push $state->{errors}->@*, $result->errors; | ||||
| 615 | push $state->{annotations}->@*, $result->annotations; | ||||
| 616 | |||||
| 617 | return $result; | ||||
| 618 | } | ||||
| 619 | |||||
| 620 | # returned object supports ->path | ||||
| 621 | sub _request_uri ($request) { | ||||
| 622 | $request->isa('HTTP::Request') ? $request->uri | ||||
| 623 | : $request->isa('Mojo::Message::Request') ? $request->url | ||||
| 624 | : croak 'unknown type '.ref($request); | ||||
| 625 | } | ||||
| 626 | |||||
| 627 | # returns a list of key-value pairs (beware of treating as a hash!) | ||||
| 628 | sub _query_pairs ($uri) { | ||||
| 629 | $uri->isa('URI') ? $uri->query_form | ||||
| 630 | : $uri->isa('Mojo::URL') ? $uri->query->pairs->@* | ||||
| 631 | : croak 'unknown type '.ref($uri); | ||||
| 632 | } | ||||
| 633 | |||||
| 634 | # note: this assumes that the header values were already normalized on creation, | ||||
| 635 | # as sanitizing on read is bypassed | ||||
| 636 | # beware: the lwp version is list/scalar-context-sensitive | ||||
| 637 | sub _header ($message, $header_name) { | ||||
| 638 | $message->isa('HTTP::Message') ? $message->headers->header($header_name) | ||||
| 639 | : $message->isa('Mojo::Message') ? $message->content->headers->header($header_name) // () | ||||
| 640 | : croak 'unknown type '.ref($message); | ||||
| 641 | } | ||||
| 642 | |||||
| 643 | # normalized, with extensions stripped | ||||
| 644 | sub _content_type ($message) { | ||||
| 645 | $message->isa('HTTP::Message') ? fc $message->headers->content_type | ||||
| 646 | : $message->isa('Mojo::Message') ? fc((split(/;/, $message->headers->content_type//'', 2))[0] // '') | ||||
| 647 | : croak 'unknown type '.ref($message); | ||||
| 648 | } | ||||
| 649 | |||||
| 650 | sub _content_charset ($message) { | ||||
| 651 | $message->isa('HTTP::Message') ? $message->headers->content_type_charset | ||||
| 652 | : $message->isa('Mojo::Message') ? $message->content->charset | ||||
| 653 | : croak 'unknown type '.ref($message); | ||||
| 654 | } | ||||
| 655 | |||||
| 656 | sub _body_size ($message) { | ||||
| 657 | $message->isa('HTTP::Message') ? $message->headers->content_length // length $message->content_ref->$* | ||||
| 658 | : $message->isa('Mojo::Message') ? $message->headers->content_length // $message->body_size | ||||
| 659 | : croak 'unknown type '.ref($message); | ||||
| 660 | } | ||||
| 661 | |||||
| 662 | sub _content_ref ($message) { | ||||
| 663 | $message->isa('HTTP::Message') ? $message->content_ref | ||||
| 664 | : $message->isa('Mojo::Message') ? \$message->body | ||||
| 665 | : croak 'unknown type '.ref($message); | ||||
| 666 | } | ||||
| 667 | |||||
| 668 | # wrappers that aren't needed (yet), because they are the same across all supported classes: | ||||
| 669 | # $request->method | ||||
| 670 | # $response->code | ||||
| 671 | # $uri->path | ||||
| 672 | |||||
| 673 | 1 | 21µs | 1; | ||
| 674 | |||||
| 675 | 1 | 115µs | 1 | 1.15ms | __END__ # spent 1.15ms making 1 call to B::Hooks::EndOfScope::XS::__ANON__[B/Hooks/EndOfScope/XS.pm:26] |
# spent 1µs within OpenAPI::Modern::__ANON__ which was called:
# once (1µs+0s) by OpenAPI::Modern::BEGIN@26 at line 26 |