view release on metacpan or search on metacpan
- fix test incompatibilities with OpenAPI::Modern 0.040
0.004 2022-12-11 01:22:59Z
- bump dependency on OpenAPI::Modern for new features and
for compatibility in test results
0.003 2022-06-08 02:25:56Z
- test fixes for error messages that changed
0.002 2022-03-25 23:47:55Z
- new helper methods validate_request and validate_response
0.001 2022-02-16 06:14:12Z
- First version, unleashed upon an unsuspecting world.
META.json
META.yml
README
dist.ini
lib/Mojolicious/Plugin/OpenAPI/Modern.pm
t/00-report-prereqs.dd
t/00-report-prereqs.t
t/initialization.t
t/lib/BasicApp.pm
t/lib/Helper.pm
t/validate_request_response.t
weaver.ini
xt/author/00-compile.t
xt/author/clean-namespaces.t
xt/author/distmeta.t
xt/author/eol.t
xt/author/kwalitee.t
xt/author/minimum-version.t
xt/author/mojibake.t
xt/author/no-tabs.t
xt/author/pod-coverage.t
lib/Mojolicious/Plugin/OpenAPI/Modern.pm view on Meta::CPAN
# leave room for other keys in our localized stash
$stash->{openapi} = $openapi;
}
catch ($e) {
die 'Cannot load OpenAPI document: ', $e;
}
$app->helper(openapi => sub ($) { $stash->{openapi} });
$app->helper(validate_request => \&_validate_request);
$app->helper(validate_response => \&_validate_response);
$app->hook(after_dispatch => sub ($c) {
$c->res->on(finish => sub ($res) { $config->{after_response}->($c) });
}) if $config->{after_response};
}
# converts a config hash into values suitable for constructing an OpenAPI::Modern object
sub _process_configs ($config) {
my $schema;
if (exists $config->{schema}) {
lib/Mojolicious/Plugin/OpenAPI/Modern.pm view on Meta::CPAN
my %config_copy = %$config;
delete @config_copy{qw(schema document_filename document_uri after_response)};
warn 'unrecognized config option(s): ', join(', ', keys %config_copy) if keys %config_copy;
return {
openapi_uri => $config->{document_uri} // $config->{document_filename} // '',
openapi_schema => $schema,
};
}
sub _validate_request ($c) {
my $options = $c->stash->{openapi} //= {};
return $c->openapi->validate_request($c->req, $options);
}
sub _validate_response ($c) {
my $options = $c->stash->{openapi} //= {};
local $options->{request} = $c->req;
return $c->openapi->validate_response($c->res, $options);
}
1;
__END__
=pod
=encoding UTF-8
lib/Mojolicious/Plugin/OpenAPI/Modern.pm view on Meta::CPAN
},
...
},
},
...
});
$app->plugin('OpenAPI::Modern', $app->config->{openapi});
# in a controller...
my $result = $c->openapi->validate_request($c->req);
=head1 DESCRIPTION
This L<Mojolicious> plugin makes an L<OpenAPI::Modern> object available to the application.
There are many features to come.
=for stopwords openapi operationId subref
=head1 CONFIGURATION OPTIONS
lib/Mojolicious/Plugin/OpenAPI/Modern.pm view on Meta::CPAN
An L<OpenAPI::Modern> object to use. If provided, all other options above are ignored.
=head2 after_response
A subref which runs after the response has been finalized, to allow you to perform validation on it.
You B<must not> mutate the response here, nor swap it out for a different response, so use this only
for telemetry and logging.
my $after_response = sub ($c) {
my $result = $c->validate_response;
if ($result->valid) {
$c->log->debug('response is valid');
}
else {
# see JSON::Schema::Modern::Result for different output formats
$c->log->error("response is invalid:\n", $result);
}
};
=head1 METHODS
lib/Mojolicious/Plugin/OpenAPI/Modern.pm view on Meta::CPAN
=head1 HELPERS
These methods are made available on the C<$c> object (the invocant of all controller methods,
and therefore other helpers).
=head2 openapi
The L<OpenAPI::Modern> object; it holds your OpenAPI specification and is reused between requests.
=head2 validate_request
my $result = $c->openapi->validate_request;
Passes C<< $c->req >> to L<OpenAPI::Modern/validate_request> and returns a
L<JSON::Schema::Modern::Result> object.
Note that the matching L<Mojo::Routes::Route> object for this request is I<not> used to find the
OpenAPI path-item that corresponds to this request: only information in the request URI itself is
used (although some information in the route may be used in future features).
You might want to define an C<under> route action that calls C<validate_request> and short-circuits
with an HTTP 400 response on validation failure.
=head2 validate_response
my $result = $c->openapi->validate_response;
Passes C<< $c->res >> and C<< $c->req >> to L<OpenAPI::Modern/validate_response> and returns a
L<JSON::Schema::Modern::Result> object.
As this can only be called in the parts of the dispatch flow where the response has already been
rendered and finalized, a hook has been set up for you; you can access it by providing a subref to the
L</after_response> configuration value:
$app->config->{openapi}{after_response} //= sub ($c) {
my $result = $c->validate_response;
# ... do something with the validation result
};
Note that the matching L<Mojo::Routes::Route> object for this request is I<not> used to find the
OpenAPI path-item that corresponds to this request and response: only information in the request URI
itself is used (although some information in the route may be used in future features).
=head1 STASH VALUES
This plugin stores all its data under the C<openapi> hashref for use by controllers and templates,
t/lib/BasicApp.pm view on Meta::CPAN
Class::Method::Modifiers::before('Test::Mojo::_request_ok' => sub {
undef $LAST_VALIDATE_REQUEST_STASH;
undef $LAST_VALIDATE_RESPONSE_RESULT;
undef $LAST_VALIDATE_RESPONSE_STASH;
});
sub startup ($self) {
my $config = $self->config->{openapi};
$config->{after_response} //= sub ($c) {
$LAST_VALIDATE_RESPONSE_RESULT = $c->validate_response;
$LAST_VALIDATE_RESPONSE_STASH = $c->stash('openapi');
};
$self->plugin('OpenAPI::Modern', $config);
my $routes = $self->routes;
$routes->any('/skip_validate_request' => sub ($c) { $c->render(text => 'ok', format => 'txt') });
$routes->any('/foo*catchall' => sub ($c) {
my $result = $c->validate_request;
$LAST_VALIDATE_REQUEST_STASH = $c->stash('openapi');
$c->render(
status => $c->req->query_params->param('status') // ($result->valid ? 200 : 400),
json => {
result => $result,
},
);
});
}
t/validate_request_response.t view on Meta::CPAN
my $openapi_preamble = {
openapi => $::OAD_VERSION,
info => {
title => 'Test API with raw schema',
version => '1.2.3',
},
};
my $doc_uri_rel = Mojo::URL->new('/api');
subtest 'validate_request helper' => sub {
my $t = Test::Mojo->new(
'BasicApp',
{
openapi => {
document_uri => $doc_uri_rel,
schema => YAML::PP->new(boolean => 'JSON::PP')->load_string("openapi: $::OAD_VERSION\n".<<'YAML')} });
info:
title: Test API with raw schema
version: 1.2.3
components:
t/validate_request_response.t view on Meta::CPAN
responses:
200:
$ref: '#/components/responses/validation_response'
400:
$ref: '#/components/responses/validation_response'
500:
description: this response code is produced via ?status=500 in the request
content:
application/json:
schema: false
/skip_validate_request:
get:
operationId: operation_skip_validate_request
responses:
200:
description: request not validated; response body not permitted
content:
text/plain:
schema:
false
YAML
$t->post_ok('/foo/hi/there')
->status_is(400, 'path_template cannot be found')
->json_is({
result => my $expected_result = {
t/validate_request_response.t view on Meta::CPAN
memory_cycle_ok($t->app);
cmp_result(
$BasicApp::LAST_VALIDATE_REQUEST_STASH,
my $expected_stash = superhashof({
method => 'POST',
request => isa('Mojo::Message::Request'),
}),
'stash is set in validate_request',
);
cmp_result(
$BasicApp::LAST_VALIDATE_RESPONSE_STASH,
$expected_stash,
'stash is set in validate_response',
);
cmp_result(
$BasicApp::LAST_VALIDATE_RESPONSE_RESULT->TO_JSON,
$expected_result,
'validate_response attempts to parse the request URI again, producing the same result',
);
$t->get_ok('/foo/hi')
->status_is(400, 'wrong HTTP method')
->json_is({
result => $expected_result = {
valid => false,
errors => [
{
t/validate_request_response.t view on Meta::CPAN
});
memory_cycle_ok($t->app);
cmp_result(
$BasicApp::LAST_VALIDATE_REQUEST_STASH,
$expected_stash = superhashof({
method => 'GET',
request => isa('Mojo::Message::Request'),
}),
'stash is set in validate_request',
);
cmp_result(
$BasicApp::LAST_VALIDATE_RESPONSE_STASH,
$expected_stash,
'stash is set in validate_response',
);
cmp_result(
$BasicApp::LAST_VALIDATE_RESPONSE_RESULT->TO_JSON,
$expected_result,
'validate_response attempts to parse the request URI again, producing the same result',
);
$t->post_ok('/foo/123')
->status_is(400, 'path parameter will fail validation')
->json_is({
result => {
valid => false,
errors => [
{
t/validate_request_response.t view on Meta::CPAN
memory_cycle_ok($t->app);
cmp_result(
$BasicApp::LAST_VALIDATE_REQUEST_STASH,
$expected_stash = superhashof({
method => 'POST',
operation_id => 'operation_foo',
path_template => '/foo/{foo_id}',
request => isa('Mojo::Message::Request'),
}),
'stash is set in validate_request',
);
cmp_result(
$BasicApp::LAST_VALIDATE_RESPONSE_STASH,
$expected_stash,
'stash is set in validate_response',
);
cmp_result(
$BasicApp::LAST_VALIDATE_RESPONSE_RESULT->TO_JSON,
{ valid => true },
'validate_response ran successfully',
);
$t->post_ok('/foo/hi', { 'Content-Type' => 'text/plain' }, '123')
->status_is(400, 'valid path; body does not match')
->json_is({
result => {
valid => false,
errors => [
{
t/validate_request_response.t view on Meta::CPAN
cmp_result(
$BasicApp::LAST_VALIDATE_REQUEST_STASH,
superhashof({
method => 'POST',
operation_id => 'operation_foo',
path_template => '/foo/{foo_id}',
path_captures => { foo_id => 'hi' },
request => isa('Mojo::Message::Request'),
}),
'stash is set in validate_request',
);
$t->post_ok('/foo/hi?status=500', { 'Content-Type' => 'text/plain' }, 'hi')
->status_is(500, 'custom status code')
->json_is({
result => { valid => true },
});
memory_cycle_ok($t->app);
cmp_result(
$BasicApp::LAST_VALIDATE_REQUEST_STASH,
$expected_stash = superhashof({
method => 'POST',
operation_id => 'operation_foo',
path_template => '/foo/{foo_id}',
path_captures => { foo_id => 'hi' },
request => isa('Mojo::Message::Request'),
}),
'stash is set in validate_request',
);
cmp_result(
$BasicApp::LAST_VALIDATE_RESPONSE_STASH,
$expected_stash,
'stash is set in validate_response',
);
cmp_result(
$BasicApp::LAST_VALIDATE_RESPONSE_RESULT->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/response/body/content',
keywordLocation => jsonp(qw(/paths /foo/{foo_id} post responses 500 content application/json schema)),
absoluteKeywordLocation => $doc_uri_rel->clone->fragment(jsonp(qw(/paths /foo/{foo_id} post responses 500 content application/json schema)))->to_string,
error => 'response body not permitted',
},
],
},
'validate_response does not like error responses',
);
$t->get_ok('/skip_validate_request')
->status_is(200)
->content_is('ok');
memory_cycle_ok($t->app);
cmp_result(
$BasicApp::LAST_VALIDATE_REQUEST_STASH,
undef,
'stash was not set in validate_request',
);
cmp_result(
$BasicApp::LAST_VALIDATE_RESPONSE_STASH,
superhashof({
method => 'GET',
operation_id => 'operation_skip_validate_request',
path_template => '/skip_validate_request',
path_captures => {},
}),
'stash is set in validate_response, even though validate_request never ran',
);
cmp_result(
$BasicApp::LAST_VALIDATE_RESPONSE_RESULT->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/response/body/content',
keywordLocation => jsonp(qw(/paths /skip_validate_request get responses 200 content text/plain schema)),
absoluteKeywordLocation => $doc_uri_rel->clone->fragment(jsonp(qw(/paths /skip_validate_request get responses 200 content text/plain schema)))->to_string,
error => 'response body not permitted',
},
],
},
'response from this endpoint never passes the specification',
);
};
had_no_warnings() if $ENV{AUTHOR_TESTING};
done_testing;
xt/author/eol.t view on Meta::CPAN
use Test::More 0.88;
use Test::EOL;
my @files = (
'lib/Mojolicious/Plugin/OpenAPI/Modern.pm',
't/00-report-prereqs.dd',
't/00-report-prereqs.t',
't/initialization.t',
't/lib/BasicApp.pm',
't/lib/Helper.pm',
't/validate_request_response.t',
'xt/author/00-compile.t',
'xt/author/clean-namespaces.t',
'xt/author/distmeta.t',
'xt/author/eol.t',
'xt/author/kwalitee.t',
'xt/author/minimum-version.t',
'xt/author/mojibake.t',
'xt/author/no-tabs.t',
'xt/author/pod-coverage.t',
'xt/author/pod-spell.t',
xt/author/no-tabs.t view on Meta::CPAN
use Test::More 0.88;
use Test::NoTabs;
my @files = (
'lib/Mojolicious/Plugin/OpenAPI/Modern.pm',
't/00-report-prereqs.dd',
't/00-report-prereqs.t',
't/initialization.t',
't/lib/BasicApp.pm',
't/lib/Helper.pm',
't/validate_request_response.t',
'xt/author/00-compile.t',
'xt/author/clean-namespaces.t',
'xt/author/distmeta.t',
'xt/author/eol.t',
'xt/author/kwalitee.t',
'xt/author/minimum-version.t',
'xt/author/mojibake.t',
'xt/author/no-tabs.t',
'xt/author/pod-coverage.t',
'xt/author/pod-spell.t',