Sponsoring The Perl Toolchain Summit 2025: Help make this important event another success Learn more

use constant DEBUG => $ENV{MOJO_OPENAPI_DEBUG} || 0;
our $VERSION = '5.11';
has route => sub {undef};
has validator => sub { JSON::Validator::Schema->new; };
sub register {
my ($self, $app, $config) = @_;
$self->validator(JSON::Validator->new->schema($config->{url} || $config->{spec})->schema);
$self->validator->coerce($config->{coerce}) if defined $config->{coerce};
if (my $class = $config->{version_from_class} // ref $app) {
$self->validator->data->{info}{version} = sprintf '%s', $class->VERSION if $class->VERSION;
}
my $errors = $config->{skip_validating_specification} ? [] : $self->validator->errors;
die @$errors if @$errors;
unless ($app->defaults->{'openapi.base_paths'}) {
$app->helper('openapi.spec' => \&_helper_get_spec);
$app->helper('openapi.valid_input' => \&_helper_valid_input);
$app->helper('openapi.validate' => \&_helper_validate);
$app->helper('reply.openapi' => \&_helper_reply);
$app->hook(before_render => \&_before_render);
$app->renderer->add_handler(openapi => \&_render);
}
$self->{log_level} = $ENV{MOJO_OPENAPI_LOG_LEVEL} || $config->{log_level} || 'warn';
$self->_build_route($app, $config);
# This plugin is required
my @plugins = (Mojolicious::Plugin::OpenAPI::Parameters->new->register($app, $config));
for my $plugin (@{$config->{plugins} || [qw(+Cors +SpecRenderer +Security)]}) {
$plugin = "Mojolicious::Plugin::OpenAPI::$plugin" if $plugin =~ s!^\+!!;
eval "require $plugin;1" or Carp::confess("require $plugin: $@");
push @plugins, $plugin->new->register($app, {%$config, openapi => $self});
}
my %default_response = %{$config->{default_response} || {}};
$default_response{name} ||= $config->{default_response_name} || 'DefaultResponse';
$default_response{status} ||= $config->{default_response_codes} || [400, 401, 404, 500, 501];
$default_response{location} = 'definitions';
$self->validator->add_default_response(\%default_response) if @{$default_response{status}};
$self->_add_routes($app, $config);
return $self;
}
sub _add_routes {
my ($self, $app, $config) = @_;
my $op_spec_to_route = $config->{op_spec_to_route} || '_op_spec_to_route';
my (@routes, %uniq);
for my $route ($self->validator->routes->each) {
my $op_spec = $self->validator->get([paths => @$route{qw(path method)}]);
my $name = $op_spec->{'x-mojo-name'} || $op_spec->{operationId};
my $r;
die qq([OpenAPI] operationId "$op_spec->{operationId}" is not unique)
if $op_spec->{operationId} and $uniq{o}{$op_spec->{operationId}}++;
die qq([OpenAPI] Route name "$name" is not unique.) if $name and $uniq{r}{$name}++;
if (!$op_spec->{'x-mojo-to'} and $name) {
$r = $self->route->root->find($name);
warn "[OpenAPI] Found existing route by name '$name'.\n" if DEBUG and $r;
$self->route->add_child($r) if $r;
}
if (!$r) {
my $http_method = $route->{method};
my $route_path = $self->_openapi_path_to_route_path(@$route{qw(method path)});
$name ||= $op_spec->{operationId};
warn "[OpenAPI] Creating new route for '$route_path'.\n" if DEBUG;
$r = $self->route->$http_method($route_path);
$r->name("$self->{route_prefix}$name") if $name;
}
$r->to(format => undef, 'openapi.method' => $route->{method}, 'openapi.path' => $route->{path});
$self->$op_spec_to_route($op_spec, $r, $config);
warn "[OpenAPI] Add route $route->{method} @{[$r->to_string]} (@{[$r->name // '']})\n" if DEBUG;
push @routes, $r;
}
$app->plugins->emit_hook(openapi_routes_added => $self, \@routes);
}
sub _before_render {
my ($c, $args) = @_;
return unless _self($c);
my $handler = $args->{handler} || 'openapi';
# Call _render() for response data
return if $handler eq 'openapi' and exists $c->stash->{openapi} or exists $args->{openapi};
# Fallback to default handler for things like render_to_string()
return $args->{handler} = $c->app->renderer->default_handler unless exists $args->{handler};
# Call _render() for errors
my $status = $args->{status} || $c->stash('status') || '200';
if ($handler eq 'openapi' and ($status eq '404' or $status eq '500')) {
$args->{handler} = 'openapi';
$args->{status} = $status;
$c->stash(
status => $args->{status},
openapi => {
errors => [{message => $c->res->default_message($args->{status}) . '.', path => '/'}],
status => $args->{status},
}
);
}
}
sub _build_route {
my ($self, $app, $config) = @_;
my $validator = $self->validator;
my $base_path = $validator->base_url->path->to_string;
my $route = $config->{route};
$route = $route->any($base_path) if $route and !$route->pattern->unparsed;
$route = $app->routes->any($base_path) unless $route;
$base_path = $route->to_string;
$base_path =~ s!/$!!;
push @{$app->defaults->{'openapi.base_paths'}}, [$base_path, $self];
$route->to({format => undef, handler => 'openapi', 'openapi.object' => $self});
$validator->base_url($base_path);
if (my $spec_route_name = $config->{spec_route_name} || $validator->get('/x-mojo-name')) {
$self->{route_prefix} = "$spec_route_name.";
}
$self->{route_prefix} //= '';
$self->route($route);
}
sub _helper_get_spec {
my $c = shift;
my $path = shift // 'for_current';
my $self = _self($c);
# Get spec by valid JSON pointer
return $self->validator->get($path) if ref $path or $path =~ m!^/! or !length $path;
# Find spec by current request
my ($stash) = grep { $_->{'openapi.path'} } reverse @{$c->match->stack};
return undef unless $stash;
my $jp = [paths => $stash->{'openapi.path'}];
push @$jp, $stash->{'openapi.method'} if $path ne 'for_path'; # Internal for now
return $self->validator->get($jp);
}
sub _helper_reply {
my $c = shift;
my $status = ref $_[0] ? 200 : shift;
my $output = shift;
my @args = @_;
Mojo::Util::deprecated(
'$c->reply->openapi() is DEPRECATED in favor of $c->render(openapi => ...)');
if (UNIVERSAL::isa($output, 'Mojo::Asset')) {
my $h = $c->res->headers;
if (!$h->content_type and $output->isa('Mojo::Asset::File')) {
my $types = $c->app->types;
my $type = $output->path =~ /\.(\w+)$/ ? $types->type($1) : undef;
$h->content_type($type || $types->type('bin'));
}
return $c->reply->asset($output);
}
push @args, status => $status if $status;
return $c->render(@args, openapi => $output);
}
sub _helper_valid_input {
my $c = shift;
return undef if $c->res->code;
return $c unless my @errors = _helper_validate($c);
$c->stash(status => 400)
->render(data => $c->openapi->build_response_body({errors => \@errors, status => 400}));
return undef;
}
sub _helper_validate {
my $c = shift;
my $self = _self($c);
my @errors = $self->validator->validate_request([@{$c->stash}{qw(openapi.method openapi.path)}],
$c->openapi->build_schema_request);
$c->openapi->coerce_request_parameters(
delete $c->stash->{'openapi.evaluated_request_parameters'});
return @errors;
}
sub _log {
my ($self, $c, $dir) = (shift, shift, shift);
my $log_level = $self->{log_level};
$c->app->log->$log_level(
sprintf 'OpenAPI %s %s %s %s',
$dir, $c->req->method,
$c->req->url->path,
Mojo::JSON::encode_json(@_)
);
}
sub _op_spec_to_route {
my ($self, $op_spec, $r, $config) = @_;
my $op_to = $op_spec->{'x-mojo-to'} // [];
my @args
= ref $op_to eq 'ARRAY' ? @$op_to : ref $op_to eq 'HASH' ? %$op_to : $op_to ? ($op_to) : ();
# x-mojo-to: controller#action
$r->to(shift @args) if @args and $args[0] =~ m!#!;
my ($constraints, @to) = ($r->pattern->constraints);
$constraints->{format} //= $config->{format} if $config->{format};
while (my $arg = shift @args) {
if (ref $arg eq 'ARRAY') { %$constraints = (%$constraints, @$arg) }
elsif (ref $arg eq 'HASH') { push @to, %$arg }
elsif (!ref $arg and @args) { push @to, $arg, shift @args }
}
$r->to(@to) if @to;
}
sub _render {
my ($renderer, $c, $output, $args) = @_;
my $stash = $c->stash;
return unless exists $stash->{openapi};
return unless my $self = _self($c);
my $status = $args->{status} || $stash->{status} || 200;
my $method_path_status = [@$stash{qw(openapi.method openapi.path)}, $status];
my $op_spec
= $method_path_status->[0] && $self->validator->parameters_for_response($method_path_status);
my @errors;
delete $args->{encoding};
$args->{status} = $status;
$stash->{format} ||= 'json';
if ($op_spec) {
@errors = $self->validator->validate_response($method_path_status,
$c->openapi->build_schema_response);
$c->openapi->coerce_response_parameters(
delete $stash->{'openapi.evaluated_response_parameters'});
$args->{status} = $errors[0]->path eq '/header/Accept' ? 400 : 500 if @errors;
}
elsif (ref $stash->{openapi} eq 'HASH' and ref $stash->{openapi}{errors} eq 'ARRAY') {
$args->{status} ||= $stash->{openapi}{status};
@errors = @{$stash->{openapi}{errors}};
}
else {
$args->{status} = 501;
@errors = ({message => qq(No response rule for "$status".)});
}
$self->_log($c, '>>>', \@errors) if @errors;
$stash->{status} = $args->{status};
$$output = $c->openapi->build_response_body(
@errors ? {errors => \@errors, status => $args->{status}} : $stash->{openapi});
}
sub _openapi_path_to_route_path {
my ($self, $http_method, $path) = @_;
my %params = map { ($_->{name}, $_) }
grep { $_->{in} eq 'path' } @{$self->validator->parameters_for_request([$http_method, $path])};
$path =~ s/{([^}]+)}/{
my $name = $1;
my $type = $params{$name}{'x-mojo-placeholder'} || ':';
"<$type$name>";
}/ge;
return $path;
}
sub _self {
my $c = shift;
my $self = $c->stash('openapi.object');
return $self if $self;
my $path = $c->req->url->path->to_string;
return +(map { $_->[1] } grep { $path =~ /^$_->[0]/ } @{$c->stash('openapi.base_paths')})[0];
}
1;
=encoding utf8
=head1 NAME
Mojolicious::Plugin::OpenAPI - OpenAPI / Swagger plugin for Mojolicious
=head1 SYNOPSIS
# It is recommended to use Mojolicious::Plugin::OpenAPI with a "full app".
# See the links after this example for more information.
use Mojolicious::Lite;
# Because the route name "echo" matches the "x-mojo-name", this route
# will be moved under "basePath", resulting in "POST /api/echo"
post "/echo" => sub {
# Validate input request or return an error document
my $c = shift->openapi->valid_input or return;
# Generate some data
my $data = {body => $c->req->json};
# Validate the output response and render it to the user agent
# using a custom "openapi" handler.
$c->render(openapi => $data);
}, "echo";
# Load specification and start web server
plugin OpenAPI => {url => "data:///swagger.yaml"};
app->start;
__DATA__
@@ swagger.yaml
swagger: "2.0"
info: { version: "0.8", title: "Echo Service" }
schemes: ["https"]
basePath: "/api"
paths:
/echo:
post:
x-mojo-name: "echo"
parameters:
- { in: "body", name: "body", schema: { type: "object" } }
responses:
200:
description: "Echo response"
schema: { type: "object" }
See L<Mojolicious::Plugin::OpenAPI::Guides::OpenAPIv2> or
L<Mojolicious::Plugin::OpenAPI::Guides::OpenAPIv3> for more in depth
information about how to use L<Mojolicious::Plugin::OpenAPI> with a "full app".
Even with a "lite app" it can be very useful to read those guides.
Looking at the documentation for
L<Mojolicious::Plugin::OpenAPI::Guides::OpenAPIv2/x-mojo-to> can be especially
useful. (The logic is the same for OpenAPIv2 and OpenAPIv3)
=head1 DESCRIPTION
L<Mojolicious::Plugin::OpenAPI> is L<Mojolicious::Plugin> that add routes and
input/output validation to your L<Mojolicious> application based on a OpenAPI
(Swagger) specification. This plugin supports both version L<2.0|/schema> and
L<3.x|/schema>, though 3.x I<might> have some missing features.
Have a look at the L</SEE ALSO> for references to plugins and other useful
documentation.
or open pull requests to enhance the 3.0 support.
=head1 HELPERS
=head2 openapi.spec
$hash = $c->openapi->spec($json_pointer)
$hash = $c->openapi->spec("/info/title")
$hash = $c->openapi->spec;
Returns the OpenAPI specification. A JSON Pointer can be used to extract a
given section of the specification. The default value of C<$json_pointer> will
be relative to the current operation. Example:
{
"paths": {
"/pets": {
"get": {
// This datastructure is returned by default
}
}
}
}
=head2 openapi.validate
@errors = $c->openapi->validate;
Used to validate a request. C<@errors> holds a list of
L<JSON::Validator::Error> objects or empty list on valid input.
Note that this helper is only for customization. You probably want
L</openapi.valid_input> in most cases.
=head2 openapi.valid_input
$c = $c->openapi->valid_input;
Returns the L<Mojolicious::Controller> object if the input is valid or
automatically render an error document if not and return false. See
L</SYNOPSIS> for example usage.
=head1 HOOKS
L<Mojolicious::Plugin::OpenAPI> will emit the following hooks on the
L<application|Mojolicious> object.
=head2 openapi_routes_added
Emitted after all routes have been added by this plugin.
$app->hook(openapi_routes_added => sub {
my ($openapi, $routes) = @_;
for my $route (@$routes) {
...
}
});
This hook is EXPERIMENTAL and subject for change.
=head1 RENDERER
This plugin register a new handler called C<openapi>. The special thing about
this handler is that it will validate the data before sending it back to the
user agent. Examples:
$c->render(json => {foo => 123}); # without validation
$c->render(openapi => {foo => 123}); # with validation
This handler will also use L</renderer> to format the output data. The code
below shows the default L</renderer> which generates JSON data:
$app->plugin(
OpenAPI => {
renderer => sub {
my ($c, $data) = @_;
return Mojo::JSON::encode_json($data);
}
}
);
=head1 ATTRIBUTES
=head2 route
$route = $openapi->route;
The parent L<Mojolicious::Routes::Route> object for all the OpenAPI endpoints.
=head2 validator
$jv = $openapi->validator;
Holds either a L<JSON::Validator::Schema::OpenAPIv2> or a
L<JSON::Validator::Schema::OpenAPIv3> object.
=head1 METHODS
=head2 register
$openapi = $openapi->register($app, \%config);
$openapi = $app->plugin(OpenAPI => \%config);
Loads the OpenAPI specification, validates it and add routes to
L<$app|Mojolicious>. It will also set up L</HELPERS> and adds a
L<before_render|Mojolicious/before_render> hook for auto-rendering of error
documents. The return value is the object instance, which allow you to access
the L</ATTRIBUTES> after you load the plugin.
C<%config> can have:
=head3 coerce
See L<JSON::Validator/coerce> for possible values that C<coerce> can take.
Default: booleans,numbers,strings
The default value will include "defaults" in the future, once that is stable enough.
=head3 default_response
Instructions for
L<JSON::Validator::Schema::OpenAPIv2/add_default_response_schema>. (Also used
for OpenAPIv3)
=head3 format
Set this to a default list of file extensions that your API accepts. This value
can be overwritten by
L<Mojolicious::Plugin::OpenAPI::Guides::OpenAPIv2/x-mojo-to>.
This config parameter is EXPERIMENTAL and subject for change.
=head3 log_level
C<log_level> is used when logging invalid request/response error messages.
Default: "warn".
=head3 op_spec_to_route
C<op_spec_to_route> can be provided if you want to add route definitions
without using "x-mojo-to". Example:
$app->plugin(OpenAPI => {op_spec_to_route => sub {
my ($plugin, $op_spec, $route) = @_;
# Here are two ways to customize where to dispatch the request
$route->to(cb => sub { shift->render(openapi => ...) });
$route->to(ucfirst "$op_spec->{operationId}#handle_request");
}});
This feature is EXPERIMENTAL and might be altered and/or removed.
=head3 plugins
A list of OpenAPI classes to extend the functionality. Default is:
L<Mojolicious::Plugin::OpenAPI::Cors>,
L<Mojolicious::Plugin::OpenAPI::SpecRenderer> and
L<Mojolicious::Plugin::OpenAPI::Security>.
$app->plugin(OpenAPI => {plugins => [qw(+Cors +SpecRenderer +Security)]});
You can load your own plugins by doing:
$app->plugin(OpenAPI => {plugins => [qw(+SpecRenderer My::Cool::OpenAPI::Plugin)]});
=head3 renderer
See L</RENDERER>.
=head3 route
C<route> can be specified in case you want to have a protected API. Example:
$app->plugin(OpenAPI => {
route => $app->routes->under("/api")->to("user#auth"),
url => $app->home->rel_file("cool.api"),
});
=head3 skip_validating_specification
Used to prevent calling L<JSON::Validator::Schema::OpenAPIv2/errors> for the
specification.
=head3 spec_route_name
Name of the route that handles the "basePath" part of the specification and
serves the specification. Defaults to "x-mojo-name" in the specification at
the top level.
=head3 spec, url
See L<JSON::Validator/schema> for the different C<url> formats that is
accepted.
C<spec> is an alias for "url", which might make more sense if your
specification is written in perl, instead of JSON or YAML.
Here are some common uses:
$app->plugin(OpenAPI => {url => $app->home->rel_file('openapi.yaml'));
$app->plugin(OpenAPI => {url => 'https://example.com/swagger.json'});
$app->plugin(OpenAPI => {spec => JSON::Validator::Schema::OpenAPIv3->new(...)});
$app->plugin(OpenAPI => {spec => {swagger => "2.0", paths => {...}, ...}});
=head3 version_from_class
Can be used to overridden C</info/version> in the API specification, from the
return value from the C<VERSION()> method in C<version_from_class>.
Defaults to the current C<$app>. This can be disabled by setting the
"version_from_class" to zero (0).
=head1 AUTHORS
=head2 Project Founder
Jan Henning Thorsen - C<jhthorsen@cpan.org>
=head2 Contributors
=over 2
=item * Bernhard Graf <augensalat@gmail.com>
=item * Doug Bell <doug@preaction.me>
=item * Ed J <mohawk2@users.noreply.github.com>
=item * Henrik Andersen <hem@fibia.dk>
=item * Henrik Andersen <hem@hamster.dk>
=item * Ilya Rassadin <elcamlost@gmail.com>
=item * Jan Henning Thorsen <jan.henning@thorsen.pm>
=item * Jan Henning Thorsen <jhthorsen@cpan.org>
=item * Ji-Hyeon Gim <potatogim@gluesys.com>
=item * Joel Berger <joel.a.berger@gmail.com>
=item * Krasimir Berov <k.berov@gmail.com>
=item * Lars Thegler <lth@fibia.dk>
=item * Lee Johnson <lee@givengain.ch>
=item * Linn-Hege Kristensen <linn-hege@stix.no>
=item * Manuel <manuel@mausz.at>
=item * Martin Renvoize <martin.renvoize@ptfs-europe.com>
=item * Mohammad S Anwar <mohammad.anwar@yahoo.com>
=item * Nick Morrott <knowledgejunkie@gmail.com>
=item * Renee <reb@perl-services.de>
=item * Roy Storey <kiwiroy@users.noreply.github.com>
=item * SebMourlhou <35918953+SebMourlhou@users.noreply.github.com>
=item * SebMourlhou <sebastien.mourlhou@justice.ge.ch>
=item * SebMourlhou <sebmourlhou@yahoo.fr>
=item * Søren Lund <sl@keycore.dk>
=item * Stephan Hradek <github@hradek.net>
=item * Stephan Hradek <stephan.hradek@eco.de>
=back
=head1 COPYRIGHT AND LICENSE
Copyright (C) Jan Henning Thorsen
This program is free software, you can redistribute it and/or modify it under
the terms of the Artistic License version 2.0.
=head1 SEE ALSO
=over 2
=item * L<Mojolicious::Plugin::OpenAPI::Guides::OpenAPIv2>
Guide for how to use this plugin with OpenAPI version 2.0 spec.
=item * L<Mojolicious::Plugin::OpenAPI::Guides::OpenAPIv3>
Guide for how to use this plugin with OpenAPI version 3.0 spec.
=item * L<Mojolicious::Plugin::OpenAPI::Cors>
Plugin to add Cross-Origin Resource Sharing (CORS).
=item * L<Mojolicious::Plugin::OpenAPI::Security>
Plugin for handling security definitions in your schema.
=item * L<Mojolicious::Plugin::OpenAPI::SpecRenderer>
Plugin for exposing your spec in human readable or JSON format.
Official OpenAPI website.
=back
=cut