Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/karenetheridge/openapi-modern

Validate HTTP requests and responses against an OpenAPI v3.1 document
https://github.com/karenetheridge/openapi-modern

json-schema openapi

Last synced: about 2 months ago
JSON representation

Validate HTTP requests and responses against an OpenAPI v3.1 document

Awesome Lists containing this project

README

        

=pod

=encoding UTF-8

=head1 NAME

OpenAPI::Modern - Validate HTTP requests and responses against an OpenAPI v3.1 document

=head1 VERSION

version 0.072

=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:
- name: My-Request-Header
in: header
required: true
schema:
pattern: ^[0-9]+$
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
hello:
type: string
pattern: ^[0-9]+$
responses:
200:
description: success
headers:
My-Response-Header:
required: true
schema:
pattern: ^[0-9]+$
content:
application/json:
schema:
type: object
required: [ status ]
properties:
status:
const: ok
YAML

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 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 will throw an exception. Be careful about examining this exception, for it might be
a L object, which has a boolean overload of false when it contains
errors! But you never do C, right?

=head2 openapi_uri

The URI that identifies the OpenAPI document.
Ignored if L is provided.

It is used at runtime as the base for absolute URIs used in L objects,
along with the request's C header and scheme (e.g. C), when available.

=head2 openapi_schema

The data structure describing the OpenAPI v3.1 document (as specified at
L). Ignored if L is provided.

=head2 openapi_document

The L document that holds the OpenAPI information to be
used for validation. If it is not provided to the constructor, then both L and
L B be provided, and L will also be used if provided.

=head2 evaluator

The L object to use for all URI resolution and JSON Schema evaluation.
Ignored if L is provided. Optional.

=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.

=head2 openapi_document

The L document that holds the OpenAPI information to be
used for validation.

=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.
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 object to use for all URI resolution and JSON Schema evaluation.

=head2 validate_request

$result = $openapi->validate_request(
$request,
# optional second argument can contain any combination of:
my $options = {
path_template => '/foo/{arg1}/bar/{arg2}',
operation_id => 'my_operation_id',
path_captures => { arg1 => 1, arg2 => 2 },
method => 'get',
},
);

Validates an L, L, L or L
object against the corresponding OpenAPI v3.1 document, returning a
L object.

Absolute URIs in the result object are constructed by resolving the openapi document path against
the L, as well as the C header of the request if a host component is not
included in the 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, L, L or L
object against the corresponding OpenAPI v3.1 document, returning a
L object.

Absolute URIs in the result object are constructed by resolving the openapi document path against
the L, as well as the C header of the request if the request is provided and if a
host component is not included in the L.

The second argument is an optional hashref that contains extra information about the request
corresponding to the response, as in L.

C is also accepted as a key in the hashref, representing the original request object that
corresponds to this response (as not all HTTP libraries link to the request in the response object).

=head2 find_path

$result = $self->find_path($options);

Uses information in the request to determine the relevant parts of the OpenAPI specification.
C should be provided if available, but additional data can be used instead
(which is populated by earlier L or L calls to the same request).

The single argument is a hashref that contains information about the request. Possible values
include:

=over 4

=item *

C: the object representing the HTTP request. Should be provided when available.

=item *

C: a string representing the request URI, with placeholders in braces (e.g. C); see L.

=item *

C: a string corresponding to the L at a particular path-template and HTTP location under C

=item *

C: a hashref mapping placeholders in the path to their actual values in the request URI

=item *

C: the HTTP method used by the request (used case-insensitively)

=back

All of these values are optional (unless C is omitted), and will be derived from the
request URI as needed (albeit less
efficiently than if they were provided). All passed-in values MUST be consistent with each other and
the request URI.

When successful, the options hash will be populated with keys C, C,
C, and C,
and the return value is true.
When not successful, the options hash will be populated with key C, an arrayref containing
a L object, and the return value is false. Other values may also be
populated if they can be successfully calculated.

In addition, these values are populated in the options hash (when available):

=over 4

=item *

C: a URI indicating the document location of the operation object for the request, after following any references (usually something under C, but may be in another document). Use C<< $openapi->evaluator->get($uri) >> to fetch this content (see L). Note that this is the same as C<< $openapi->recursive_get(Mojo::URL->new->fragment(JSON::Schema::Modern::Utilities::jsonp('/paths', $options->{path_template}{$options->{method}}))) >>. (See the documentation for an operation at L or in the specification at L<§4.8.10 of the specification|https://spec.openapis.org/oas/v3.1.0#operation-object>.)

=item *

C (not necessarily what was passed in: this is always a L)

=back

You can find the associated operation object by using either C,
or by calling C<< $openapi->openapi_document->get_operationId_path($operation_id) >>
(see L) (note that the latter will
be removed in a subsequent release, in order to support operations existing in other documents).

Note that the L|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 entries you will need to add this to
the path templates under `/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, C, C, C, C, C,
C, C, C, or C)
for validation of the entire reference chain.

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');

# starts with a JSON::Schema::Modern object (TODO)
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.

=head2 schema

An accessor that delegates to L.

=head2 get_media_type

An accessor that delegates to L.

=head2 add_media_type

A setter that delegates to L.

=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.

=head1 ON THE USE OF JSON SCHEMAS

Embedded JSON Schemas, through the use of the C keyword, are fully draft2020-12-compliant,
as per the spec, and implemented with L. Unless overridden with the use of the
L keyword, their
metaschema is L, which allows for use of the
OpenAPI-specific keywords (C, C, C, and C), as defined in
L. Format validation is turned
B, and the use of content* keywords is off (see
L).

References (with the C<$ref>) keyword may reference any position within the entire OpenAPI document;
as such, json pointers are relative to the B of the document, not the root of the subschema
itself. References to other documents are also permitted, provided those documents have been loaded
into the evaluator in advance (see L).

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 be done if the C keyword is omitted.
This lets you use numeric keywords such as C and C in your schemas.
It also resolves inconsistencies that can arise when request and response objects are created
manually in a test environment (as opposed to being parsed from incoming network traffic) and can
therefore inadvertently contain perlish numbers rather than strings.

=head1 LIMITATIONS

All message validation is done using L objects (L and
L). If messages of other types are passed, conversion is done on a
best-effort basis, but since different implementations have different levels of adherence to the RFC
specs, some validation errors may occur e.g. if a certain required header is missing on the
original. For best results in validating real messages from the network, parse them directly into
Mojolicious messages (see L).

Only certain permutations of OpenAPI documents are supported at this time:

=over 4

=item *

for path parameters, only C and C is supported

=item *

for query parameters, only C and C is supported, only the first value of each parameter name is considered, and C and C are not checked

=item *

cookie parameters are not checked at all yet

=item *

C and C messages are not yet supported

=item *

C fields in definitions are completely ignored, and not considered when parsing request URIs.

=item *

OpenAPI descriptions must be contained in a single document; C<$ref>erences to other documents are not fully supported at this time.

=item *

The use of C<$ref> within a path-item object is not permitted.

=item *

Security schemes in the OpenAPI description, and the use of any C headers in requests, are not currently supported.

=back

=head1 SEE ALSO

=over 4

=item *

L

=item *

L

=item *

L

=item *

L

=item *

L

=item *

L

=item *

L

=item *

L

=back

=head1 SUPPORT

Bugs may be submitted through L.

I am also usually active on irc, as 'ether' at C and C.

=for stopwords OpenAPI

You can also find me on the L and L, which are also great resources for finding help.

=head1 AUTHOR

Karen Etheridge

=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