=pod
=encoding UTF-8
=head1 NAME
OpenAPI::Modern - Validate HTTP requests and responses against an OpenAPI v3.1 document
=head1 VERSION
version 0.067
=head1 SYNOPSIS
my $openapi = OpenAPI::Modern->new( openapi_uri => '/api', openapi_schema => YAML::PP->new(boolean => 'JSON::PP')->load_string(<<'YAML')); openapi: 3.1.0 info: title: Test API version: 1.2.3 paths: /foo/{foo_id}: parameters:
name: foo_id in: path required: true schema: pattern: ^[a-z]+$ post: operationId: my_foo_request parameters:
say 'request:'; my $request = POST '/foo/bar', 'My-Request-Header' => '123', 'Content-Type' => 'application/json', Host => 'example.com', Content => '{"hello": 123}'; my $results = $openapi->validate_request($request); say $results; say ''; # newline say JSON::MaybeXS->new(convert_blessed => 1, canonical => 1, pretty => 1, indent_length => 2)->encode($results);
say 'response:'; my $response = Mojo::Message::Response->new(code => 200, message => 'OK'); $response->headers->content_type('application/json'); $response->headers->header('My-Response-Header', '123'); $response->body('{"status": "ok"}'); $results = $openapi->validate_response($response, { request => $request }); say $results; say ''; # newline say JSON::MaybeXS->new(convert_blessed => 1, canonical => 1, pretty => 1, indent_length => 2)->encode($results);
prints:
request: '/request/body/hello': got integer, not string '/request/body': not all properties are valid
{ "errors" : [ { "absoluteKeywordLocation" : "https://example.com/api#/paths/~1foo~1%7Bfoo_id%7D/post/requestBody/content/application~1json/schema/properties/hello/type", "error" : "got integer, not string", "instanceLocation" : "/request/body/hello", "keywordLocation" : "/paths/~1foo~1{foo_id}/post/requestBody/content/application~1json/schema/properties/hello/type" }, { "absoluteKeywordLocation" : "https://example.com/api#/paths/~1foo~1%7Bfoo_id%7D/post/requestBody/content/application~1json/schema/properties", "error" : "not all properties are valid", "instanceLocation" : "/request/body", "keywordLocation" : "/paths/~1foo~1{foo_id}/post/requestBody/content/application~1json/schema/properties" } ], "valid" : false }
response: valid
{ "valid" : true }
=head1 DESCRIPTION
This module provides various tools for working with an L<OpenAPI Specification v3.1 document|https://spec.openapis.org/oas/v3.1.0#openapi-document> within your application. The JSON Schema evaluator is fully specification-compliant; the OpenAPI evaluator aims to be but some features are not yet available. My belief is that missing features are better than features that seem to work but actually cut corners for simplicity.
=for Pod::Coverage BUILDARGS THAW
=for stopwords schemas jsonSchemaDialect metaschema subschema perlish operationId openapi Mojolicious
=head1 CONSTRUCTOR ARGUMENTS
If construction of the object is not successful, for example the document has a syntax error, the
call to C<new()> will throw an exception. Be careful about examining this exception, for it might be
a L
=head2 openapi_uri
The URI that identifies the OpenAPI document. Ignored if L is provided.
If it is not absolute, it is resolved at runtime against the request's C
=head2 openapi_schema
The data structure describing the OpenAPI v3.1 document (as specified at Lhttps://spec.openapis.org/oas/v3.1.0). Ignored if L is provided.
=head2 openapi_document
The L
=head2 evaluator
The L
=head1 ACCESSORS/METHODS
=head2 openapi_uri
The URI that identifies the OpenAPI document.
=head2 openapi_schema
The data structure describing the OpenAPI document. See L<the specification/https://spec.openapis.org/oas/v3.1.0>.
=head2 openapi_document
The L
=head2 document_get
my $parameter_data = $openapi->document_get('/paths/~1foo~1{foo_id}/get/parameters/0');
Fetches the subschema at the provided JSON pointer. Proxies to L<JSON::Schema::Modern::Document::OpenAPI/get>. This is not recursive (does not follow C<$ref> chains) -- for that, use C<< $openapi->recursive_get(Mojo::URL->new->fragment($json_pointer)) >>, see L.
=head2 evaluator
The L
=head2 validate_request
$result = $openapi->validate_request( $request,
my $options = {
path_template => '/foo/{arg1}/bar/{arg2}',
operation_id => 'my_operation_id',
path_captures => { arg1 => 1, arg2 => 2 },
method => 'get',
},
);
Validates an L
The second argument is an optional hashref that contains extra information about the request, corresponding to the values expected by L below. It is populated with some information about the request: save it and pass it to a later L (corresponding to a response for this request) to improve performance.
=head2 validate_response
$result = $openapi->validate_response( $response, { path_template => '/foo/{arg1}/bar/{arg2}', request => $request, }, );
Validates an L
The second argument is an optional hashref that contains extra information about the request corresponding to the response, as in L.
C
=head2 find_path
$result = $self->find_path($options);
Uses information in the request to determine the relevant parts of the OpenAPI specification.
C
The single argument is a hashref that contains information about the request. Possible values include:
=over 4
=item *
C
=item *
C
=item *
C
=item *
C
=item *
C
=back
All of these values are optional (unless C
When successful, the options hash will be populated with keys C
In addition, these values are populated in the options hash (when available):
=over 4
=item *
C
=item *
C
=back
You can find the associated operation object by using either C
Note that the L<C|https://spec.openapis.org/oas/v3.1.0#server-object> section of the
OpenAPI document is not used for path matching at this time, for either scheme and host matching nor
path prefixes. For now, if you use a path prefix in C/paths
.
=head2 recursive_get
Given a uri or uri-reference, get the definition at that location, following any C<$ref>s along the
way. Include the expected definition type
(one of C
Returns the data in scalar context, or a tuple of the data and the canonical URI of the referenced location in list context.
If the provided location is relative, the main openapi document is used for the base URI. If you have a local json pointer you want to resolve, you can turn it into a uri-reference by prepending C<#>.
my $schema = $openapi->recursive_get('#/components/parameters/Content-Encoding', 'parameter');
my $schema = $js->recursive_get('https:///openapi_doc.yaml#/components/schemas/my_object') my $schema = $js->recursive_get('https://localhost:1234/my_spec#/$defs/my_object')
=head2 canonical_uri
An accessor that delegates to L<JSON::Schema::Modern::Document/canonical_uri>.
=head2 schema
An accessor that delegates to L<JSON::Schema::Modern::Document/schema>.
=head2 get_media_type
An accessor that delegates to L<JSON::Schema::Modern/get_media_type>.
=head2 add_media_type
A setter that delegates to L<JSON::Schema::Modern/add_media_type>.
=head1 CACHING
=for stopwords preforking
Very large OpenAPI documents may take a noticeable time to be loaded and parsed. You can reduce the impact to your preforking application by loading all necessary documents at startup, and impact can be further reduced by saving objects to cache and then reloading them (perhaps by using a timestamp or checksum to determine if a fresh reload is needed).
sub get_openapi (...) { my $serialized_file = Path::Tiny::path($serialized_filename); my $openapi_file = Path::Tiny::path($openapi_filename); my $openapi; if ($serialized_file->stat->mtime < $openapi_file->stat->mtime)) { $openapi = OpenAPI::Modern->new( openapi_uri => '/api', openapi_schema => decode_json($openapi_file->slurp_raw), # your openapi document ); $openapi->evaluator->add_schema(decode_json(...)); # any other needed schemas my $frozen = Sereal::Encoder->new({ freeze_callbacks => 1 })->encode($openapi); $serialized_file->spew_raw($frozen); } else { my $frozen = $serialized_file->slurp_raw; $openapi = Sereal::Decoder->new->decode($frozen); }
# add custom format validations, media types and encodings here
$openapi->evaluator->add_media_type(...);
return $openapi;
}
See also L<JSON::Schema::Modern/CACHING>.
=head1 ON THE USE OF JSON SCHEMAS
Embedded JSON Schemas, through the use of the C
References (with the C<$ref>) keyword may reference any position within the entire OpenAPI document;
as such, json pointers are relative to the B
Values are generally treated as strings for the purpose of schema evaluation. However, if the top
level of the schema contains C<"type": "number"> or C<"type": "integer">, then the value will be
(attempted to be) coerced into a number before being passed to the JSON Schema evaluator.
Type coercion will B
=head1 LIMITATIONS
All message validation is done using L
Only certain permutations of OpenAPI documents are supported at this time:
=over 4
=item *
for path parameters, only C
=item *
for query parameters, only C
=item *
cookie parameters are not checked at all yet
=item *
C<application/x-www-form-urlencoded> and C<multipart/*> messages are not yet supported
=back
=head1 SEE ALSO
=over 4
=item *
L
=item *
L
=item *
L
=item *
L
=item *
=item *
=item *
=item *
Lhttps://spec.openapis.org/oas/v3.1.0
=back
=head1 SUPPORT
Bugs may be submitted through Lhttps://github.com/karenetheridge/OpenAPI-Modern/issues.
I am also usually active on irc, as 'ether' at C
=for stopwords OpenAPI
You can also find me on the L<JSON Schema Slack server|https://json-schema.slack.com> and L<OpenAPI Slack server|https://open-api.slack.com>, which are also great resources for finding help.
=head1 AUTHOR
Karen Etheridge ether@cpan.org
=head1 COPYRIGHT AND LICENCE
This software is copyright (c) 2021 by Karen Etheridge.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.
Some schema files have their own licence, in share/oas/LICENSE.
=cut