File Coverage

blib/lib/Mojolicious/Plugin/OpenAPI.pm
Criterion Covered Total %
statement 179 193 92.7
branch 81 104 77.8
condition 71 101 70.3
subroutine 18 19 94.7
pod 1 1 100.0
total 350 418 83.7


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::OpenAPI;
2 48     48   56414362 use Mojo::Base 'Mojolicious::Plugin';
  48         128  
  48         466  
3              
4 48     48   44474 use JSON::Validator;
  48         2338124  
  48         447  
5 48     48   2722 use Mojo::JSON;
  48         126  
  48         5793  
6 48     48   373 use Mojo::Util;
  48         140  
  48         2076  
7 48     48   34808 use Mojolicious::Plugin::OpenAPI::Parameters;
  48         193  
  48         658  
8              
9 48   50 48   3100 use constant DEBUG => $ENV{MOJO_OPENAPI_DEBUG} || 0;
  48         105  
  48         245144  
10              
11             our $VERSION = '5.11';
12              
13             has route => sub {undef};
14             has validator => sub { JSON::Validator::Schema->new; };
15              
16             sub register {
17 61     61 1 87130 my ($self, $app, $config) = @_;
18              
19 61   66     583 $self->validator(JSON::Validator->new->schema($config->{url} || $config->{spec})->schema);
20 61 100       1300133 $self->validator->coerce($config->{coerce}) if defined $config->{coerce};
21              
22 61 50 66     658 if (my $class = $config->{version_from_class} // ref $app) {
23 61 100       1249 $self->validator->data->{info}{version} = sprintf '%s', $class->VERSION if $class->VERSION;
24             }
25              
26 61 100       499 my $errors = $config->{skip_validating_specification} ? [] : $self->validator->errors;
27 61 100       16828489 die @$errors if @$errors;
28              
29 59 100       1168 unless ($app->defaults->{'openapi.base_paths'}) {
30 51         1483 $app->helper('openapi.spec' => \&_helper_get_spec);
31 51         30038 $app->helper('openapi.valid_input' => \&_helper_valid_input);
32 51         22544 $app->helper('openapi.validate' => \&_helper_validate);
33 51         22294 $app->helper('reply.openapi' => \&_helper_reply);
34 51         40140 $app->hook(before_render => \&_before_render);
35 51         1667 $app->renderer->add_handler(openapi => \&_render);
36             }
37              
38 59   50     1820 $self->{log_level} = $ENV{MOJO_OPENAPI_LOG_LEVEL} || $config->{log_level} || 'warn';
39 59         457 $self->_build_route($app, $config);
40              
41             # This plugin is required
42 59         1284 my @plugins = (Mojolicious::Plugin::OpenAPI::Parameters->new->register($app, $config));
43              
44 59 50       36505 for my $plugin (@{$config->{plugins} || [qw(+Cors +SpecRenderer +Security)]}) {
  59         667  
45 177 50       49133 $plugin = "Mojolicious::Plugin::OpenAPI::$plugin" if $plugin =~ s!^\+!!;
46 177 50       27624 eval "require $plugin;1" or Carp::confess("require $plugin: $@");
47 177         4170 push @plugins, $plugin->new->register($app, {%$config, openapi => $self});
48             }
49              
50 59 50       406 my %default_response = %{$config->{default_response} || {}};
  59         566  
51 59   100     765 $default_response{name} ||= $config->{default_response_name} || 'DefaultResponse';
      33        
52 59   100     731 $default_response{status} ||= $config->{default_response_codes} || [400, 401, 404, 500, 501];
      33        
53 59         187 $default_response{location} = 'definitions';
54 59 100       132 $self->validator->add_default_response(\%default_response) if @{$default_response{status}};
  59         465  
55              
56 59         112546 $self->_add_routes($app, $config);
57              
58 57         2499 return $self;
59             }
60              
61             sub _add_routes {
62 59     59   294 my ($self, $app, $config) = @_;
63 59   100     483 my $op_spec_to_route = $config->{op_spec_to_route} || '_op_spec_to_route';
64 59         169 my (@routes, %uniq);
65              
66 59         307 for my $route ($self->validator->routes->each) {
67 129         24826 my $op_spec = $self->validator->get([paths => @$route{qw(path method)}]);
68 129   100     20711 my $name = $op_spec->{'x-mojo-name'} || $op_spec->{operationId};
69 129         399 my $r;
70              
71             die qq([OpenAPI] operationId "$op_spec->{operationId}" is not unique)
72 129 100 100     929 if $op_spec->{operationId} and $uniq{o}{$op_spec->{operationId}}++;
73 128 100 100     1138 die qq([OpenAPI] Route name "$name" is not unique.) if $name and $uniq{r}{$name}++;
74              
75 127 100 100     716 if (!$op_spec->{'x-mojo-to'} and $name) {
76 109         420 $r = $self->route->root->find($name);
77 109         18259 warn "[OpenAPI] Found existing route by name '$name'.\n" if DEBUG and $r;
78 109 100       538 $self->route->add_child($r) if $r;
79             }
80 127 100       8793 if (!$r) {
81 26         66 my $http_method = $route->{method};
82 26         129 my $route_path = $self->_openapi_path_to_route_path(@$route{qw(method path)});
83 26   66     142 $name ||= $op_spec->{operationId};
84 26         46 warn "[OpenAPI] Creating new route for '$route_path'.\n" if DEBUG;
85 26         99 $r = $self->route->$http_method($route_path);
86 26 100       24145 $r->name("$self->{route_prefix}$name") if $name;
87             }
88              
89 127         899 $r->to(format => undef, 'openapi.method' => $route->{method}, 'openapi.path' => $route->{path});
90 127         4820 $self->$op_spec_to_route($op_spec, $r, $config);
91 127         272 warn "[OpenAPI] Add route $route->{method} @{[$r->to_string]} (@{[$r->name // '']})\n" if DEBUG;
92              
93 127         404 push @routes, $r;
94             }
95              
96 57         660 $app->plugins->emit_hook(openapi_routes_added => $self, \@routes);
97             }
98              
99             sub _before_render {
100 383     383   708581 my ($c, $args) = @_;
101 383 100       1436 return unless _self($c);
102 379   100     2423 my $handler = $args->{handler} || 'openapi';
103              
104             # Call _render() for response data
105 379 100 66     1954 return if $handler eq 'openapi' and exists $c->stash->{openapi} or exists $args->{openapi};
      66        
106              
107             # Fallback to default handler for things like render_to_string()
108 229 100       3368 return $args->{handler} = $c->app->renderer->default_handler unless exists $args->{handler};
109              
110             # Call _render() for errors
111 13   100     71 my $status = $args->{status} || $c->stash('status') || '200';
112 13 50 66     167 if ($handler eq 'openapi' and ($status eq '404' or $status eq '500')) {
      66        
113 9         27 $args->{handler} = 'openapi';
114 9         27 $args->{status} = $status;
115             $c->stash(
116             status => $args->{status},
117             openapi => {
118             errors => [{message => $c->res->default_message($args->{status}) . '.', path => '/'}],
119             status => $args->{status},
120             }
121 9         45 );
122             }
123             }
124              
125             sub _build_route {
126 59     59   232 my ($self, $app, $config) = @_;
127 59         378 my $validator = $self->validator;
128 59         690 my $base_path = $validator->base_url->path->to_string;
129 59         22788 my $route = $config->{route};
130              
131 59 50 66     473 $route = $route->any($base_path) if $route and !$route->pattern->unparsed;
132 59 100       576 $route = $app->routes->any($base_path) unless $route;
133 59         25940 $base_path = $route->to_string;
134 59         2298 $base_path =~ s!/$!!;
135              
136 59         191 push @{$app->defaults->{'openapi.base_paths'}}, [$base_path, $self];
  59         371  
137 59         1200 $route->to({format => undef, handler => 'openapi', 'openapi.object' => $self});
138 59         1977 $validator->base_url($base_path);
139              
140 59 100 100     34096 if (my $spec_route_name = $config->{spec_route_name} || $validator->get('/x-mojo-name')) {
141 4         344 $self->{route_prefix} = "$spec_route_name.";
142             }
143              
144 59   100     6816 $self->{route_prefix} //= '';
145 59         300 $self->route($route);
146             }
147              
148             sub _helper_get_spec {
149 37     37   105588 my $c = shift;
150 37   100     233 my $path = shift // 'for_current';
151 37         184 my $self = _self($c);
152              
153             # Get spec by valid JSON pointer
154 37 100 66     407 return $self->validator->get($path) if ref $path or $path =~ m!^/! or !length $path;
      66        
155              
156             # Find spec by current request
157 36         145 my ($stash) = grep { $_->{'openapi.path'} } reverse @{$c->match->stack};
  67         500  
  36         173  
158 36 100       178 return undef unless $stash;
159              
160 34         154 my $jp = [paths => $stash->{'openapi.path'}];
161 34 100       184 push @$jp, $stash->{'openapi.method'} if $path ne 'for_path'; # Internal for now
162 34         159 return $self->validator->get($jp);
163             }
164              
165             sub _helper_reply {
166 0     0   0 my $c = shift;
167 0 0       0 my $status = ref $_[0] ? 200 : shift;
168 0         0 my $output = shift;
169 0         0 my @args = @_;
170              
171 0         0 Mojo::Util::deprecated(
172             '$c->reply->openapi() is DEPRECATED in favor of $c->render(openapi => ...)');
173              
174 0 0       0 if (UNIVERSAL::isa($output, 'Mojo::Asset')) {
175 0         0 my $h = $c->res->headers;
176 0 0 0     0 if (!$h->content_type and $output->isa('Mojo::Asset::File')) {
177 0         0 my $types = $c->app->types;
178 0 0       0 my $type = $output->path =~ /\.(\w+)$/ ? $types->type($1) : undef;
179 0   0     0 $h->content_type($type || $types->type('bin'));
180             }
181 0         0 return $c->reply->asset($output);
182             }
183              
184 0 0       0 push @args, status => $status if $status;
185 0         0 return $c->render(@args, openapi => $output);
186             }
187              
188             sub _helper_valid_input {
189 180     180   2408733 my $c = shift;
190 180 100       1683 return undef if $c->res->code;
191 174 100       3744 return $c unless my @errors = _helper_validate($c);
192 32         220 $c->stash(status => 400)
193             ->render(data => $c->openapi->build_response_body({errors => \@errors, status => 400}));
194 32         15431 return undef;
195             }
196              
197             sub _helper_validate {
198 176     176   40933 my $c = shift;
199 176         922 my $self = _self($c);
200 176         1040 my @errors = $self->validator->validate_request([@{$c->stash}{qw(openapi.method openapi.path)}],
  176         1383  
201             $c->openapi->build_schema_request);
202             $c->openapi->coerce_request_parameters(
203 176         77205 delete $c->stash->{'openapi.evaluated_request_parameters'});
204 176         2056 return @errors;
205             }
206              
207             sub _log {
208 29     29   99 my ($self, $c, $dir) = (shift, shift, shift);
209 29         137 my $log_level = $self->{log_level};
210              
211 29         177 $c->app->log->$log_level(
212             sprintf 'OpenAPI %s %s %s %s',
213             $dir, $c->req->method,
214             $c->req->url->path,
215             Mojo::JSON::encode_json(@_)
216             );
217             }
218              
219             sub _op_spec_to_route {
220 125     125   405 my ($self, $op_spec, $r, $config) = @_;
221 125   100     898 my $op_to = $op_spec->{'x-mojo-to'} // [];
222             my @args
223 125 50       724 = ref $op_to eq 'ARRAY' ? @$op_to : ref $op_to eq 'HASH' ? %$op_to : $op_to ? ($op_to) : ();
    50          
    100          
224              
225             # x-mojo-to: controller#action
226 125 100 100     608 $r->to(shift @args) if @args and $args[0] =~ m!#!;
227              
228 125         974 my ($constraints, @to) = ($r->pattern->constraints);
229 125 50 0     1213 $constraints->{format} //= $config->{format} if $config->{format};
230 125         445 while (my $arg = shift @args) {
231 7 100 33     24 if (ref $arg eq 'ARRAY') { %$constraints = (%$constraints, @$arg) }
  1 100       18  
    50          
232 1         4 elsif (ref $arg eq 'HASH') { push @to, %$arg }
233 5         12 elsif (!ref $arg and @args) { push @to, $arg, shift @args }
234             }
235              
236 125 100       491 $r->to(@to) if @to;
237             }
238              
239             sub _render {
240 159     159   24923 my ($renderer, $c, $output, $args) = @_;
241 159         573 my $stash = $c->stash;
242 159 50       1648 return unless exists $stash->{openapi};
243 159 50       640 return unless my $self = _self($c);
244              
245 159   100     1613 my $status = $args->{status} || $stash->{status} || 200;
246 159         821 my $method_path_status = [@$stash{qw(openapi.method openapi.path)}, $status];
247 159   100     929 my $op_spec
248             = $method_path_status->[0] && $self->validator->parameters_for_response($method_path_status);
249 159         44141 my @errors;
250              
251 159         491 delete $args->{encoding};
252 159         599 $args->{status} = $status;
253 159   100     1231 $stash->{format} ||= 'json';
254              
255 159 100 66     678 if ($op_spec) {
    100          
256 137         660 @errors = $self->validator->validate_response($method_path_status,
257             $c->openapi->build_schema_response);
258             $c->openapi->coerce_response_parameters(
259 137         75212 delete $stash->{'openapi.evaluated_response_parameters'});
260 137 100       1125 $args->{status} = $errors[0]->path eq '/header/Accept' ? 400 : 500 if @errors;
    100          
261             }
262             elsif (ref $stash->{openapi} eq 'HASH' and ref $stash->{openapi}{errors} eq 'ARRAY') {
263 20   33     91 $args->{status} ||= $stash->{openapi}{status};
264 20         75 @errors = @{$stash->{openapi}{errors}};
  20         131  
265             }
266             else {
267 2         4 $args->{status} = 501;
268 2         8 @errors = ({message => qq(No response rule for "$status".)});
269             }
270              
271 159 100       846 $self->_log($c, '>>>', \@errors) if @errors;
272 159         5704 $stash->{status} = $args->{status};
273             $$output = $c->openapi->build_response_body(
274 159 100       669 @errors ? {errors => \@errors, status => $args->{status}} : $stash->{openapi});
275             }
276              
277             sub _openapi_path_to_route_path {
278 26     26   79 my ($self, $http_method, $path) = @_;
279 4         20 my %params = map { ($_->{name}, $_) }
280 26         55 grep { $_->{in} eq 'path' } @{$self->validator->parameters_for_request([$http_method, $path])};
  23         9566  
  26         98  
281              
282 26         10811 $path =~ s/{([^}]+)}/{
283 4         9 my $name = $1;
  4         14  
284 4   100     24 my $type = $params{$name}{'x-mojo-placeholder'} || ':';
285 4         17 "<$type$name>";
286             }/ge;
287              
288 26         88 return $path;
289             }
290              
291             sub _self {
292 796     796   1668 my $c = shift;
293 796         2619 my $self = $c->stash('openapi.object');
294 796 100       11212 return $self if $self;
295 15         57 my $path = $c->req->url->path->to_string;
296 15         1619 return +(map { $_->[1] } grep { $path =~ /^$_->[0]/ } @{$c->stash('openapi.base_paths')})[0];
  11         58  
  24         532  
  15         71  
297             }
298              
299             1;
300              
301             =encoding utf8
302              
303             =head1 NAME
304              
305             Mojolicious::Plugin::OpenAPI - OpenAPI / Swagger plugin for Mojolicious
306              
307             =head1 SYNOPSIS
308              
309             # It is recommended to use Mojolicious::Plugin::OpenAPI with a "full app".
310             # See the links after this example for more information.
311             use Mojolicious::Lite;
312              
313             # Because the route name "echo" matches the "x-mojo-name", this route
314             # will be moved under "basePath", resulting in "POST /api/echo"
315             post "/echo" => sub {
316              
317             # Validate input request or return an error document
318             my $c = shift->openapi->valid_input or return;
319              
320             # Generate some data
321             my $data = {body => $c->req->json};
322              
323             # Validate the output response and render it to the user agent
324             # using a custom "openapi" handler.
325             $c->render(openapi => $data);
326             }, "echo";
327              
328             # Load specification and start web server
329             plugin OpenAPI => {url => "data:///swagger.yaml"};
330             app->start;
331              
332             __DATA__
333             @@ swagger.yaml
334             swagger: "2.0"
335             info: { version: "0.8", title: "Echo Service" }
336             schemes: ["https"]
337             basePath: "/api"
338             paths:
339             /echo:
340             post:
341             x-mojo-name: "echo"
342             parameters:
343             - { in: "body", name: "body", schema: { type: "object" } }
344             responses:
345             200:
346             description: "Echo response"
347             schema: { type: "object" }
348              
349             See L or
350             L for more in depth
351             information about how to use L with a "full app".
352             Even with a "lite app" it can be very useful to read those guides.
353              
354             Looking at the documentation for
355             L can be especially
356             useful. (The logic is the same for OpenAPIv2 and OpenAPIv3)
357              
358             =head1 DESCRIPTION
359              
360             L is L that add routes and
361             input/output validation to your L application based on a OpenAPI
362             (Swagger) specification. This plugin supports both version L<2.0|/schema> and
363             L<3.x|/schema>, though 3.x I have some missing features.
364              
365             Have a look at the L for references to plugins and other useful
366             documentation.
367              
368             Please report in L
369             or open pull requests to enhance the 3.0 support.
370              
371             =head1 HELPERS
372              
373             =head2 openapi.spec
374              
375             $hash = $c->openapi->spec($json_pointer)
376             $hash = $c->openapi->spec("/info/title")
377             $hash = $c->openapi->spec;
378              
379             Returns the OpenAPI specification. A JSON Pointer can be used to extract a
380             given section of the specification. The default value of C<$json_pointer> will
381             be relative to the current operation. Example:
382              
383             {
384             "paths": {
385             "/pets": {
386             "get": {
387             // This datastructure is returned by default
388             }
389             }
390             }
391             }
392              
393             =head2 openapi.validate
394              
395             @errors = $c->openapi->validate;
396              
397             Used to validate a request. C<@errors> holds a list of
398             L objects or empty list on valid input.
399              
400             Note that this helper is only for customization. You probably want
401             L in most cases.
402              
403             =head2 openapi.valid_input
404              
405             $c = $c->openapi->valid_input;
406              
407             Returns the L object if the input is valid or
408             automatically render an error document if not and return false. See
409             L for example usage.
410              
411             =head1 HOOKS
412              
413             L will emit the following hooks on the
414             L object.
415              
416             =head2 openapi_routes_added
417              
418             Emitted after all routes have been added by this plugin.
419              
420             $app->hook(openapi_routes_added => sub {
421             my ($openapi, $routes) = @_;
422              
423             for my $route (@$routes) {
424             ...
425             }
426             });
427              
428             This hook is EXPERIMENTAL and subject for change.
429              
430             =head1 RENDERER
431              
432             This plugin register a new handler called C. The special thing about
433             this handler is that it will validate the data before sending it back to the
434             user agent. Examples:
435              
436             $c->render(json => {foo => 123}); # without validation
437             $c->render(openapi => {foo => 123}); # with validation
438              
439             This handler will also use L to format the output data. The code
440             below shows the default L which generates JSON data:
441              
442             $app->plugin(
443             OpenAPI => {
444             renderer => sub {
445             my ($c, $data) = @_;
446             return Mojo::JSON::encode_json($data);
447             }
448             }
449             );
450              
451             =head1 ATTRIBUTES
452              
453             =head2 route
454              
455             $route = $openapi->route;
456              
457             The parent L object for all the OpenAPI endpoints.
458              
459             =head2 validator
460              
461             $jv = $openapi->validator;
462              
463             Holds either a L or a
464             L object.
465              
466             =head1 METHODS
467              
468             =head2 register
469              
470             $openapi = $openapi->register($app, \%config);
471             $openapi = $app->plugin(OpenAPI => \%config);
472              
473             Loads the OpenAPI specification, validates it and add routes to
474             L<$app|Mojolicious>. It will also set up L and adds a
475             L hook for auto-rendering of error
476             documents. The return value is the object instance, which allow you to access
477             the L after you load the plugin.
478              
479             C<%config> can have:
480              
481             =head3 coerce
482              
483             See L for possible values that C can take.
484              
485             Default: booleans,numbers,strings
486              
487             The default value will include "defaults" in the future, once that is stable enough.
488              
489             =head3 default_response
490              
491             Instructions for
492             L. (Also used
493             for OpenAPIv3)
494              
495             =head3 format
496              
497             Set this to a default list of file extensions that your API accepts. This value
498             can be overwritten by
499             L.
500              
501             This config parameter is EXPERIMENTAL and subject for change.
502              
503             =head3 log_level
504              
505             C is used when logging invalid request/response error messages.
506              
507             Default: "warn".
508              
509             =head3 op_spec_to_route
510              
511             C can be provided if you want to add route definitions
512             without using "x-mojo-to". Example:
513              
514             $app->plugin(OpenAPI => {op_spec_to_route => sub {
515             my ($plugin, $op_spec, $route) = @_;
516              
517             # Here are two ways to customize where to dispatch the request
518             $route->to(cb => sub { shift->render(openapi => ...) });
519             $route->to(ucfirst "$op_spec->{operationId}#handle_request");
520             }});
521              
522             This feature is EXPERIMENTAL and might be altered and/or removed.
523              
524             =head3 plugins
525              
526             A list of OpenAPI classes to extend the functionality. Default is:
527             L,
528             L and
529             L.
530              
531             $app->plugin(OpenAPI => {plugins => [qw(+Cors +SpecRenderer +Security)]});
532              
533             You can load your own plugins by doing:
534              
535             $app->plugin(OpenAPI => {plugins => [qw(+SpecRenderer My::Cool::OpenAPI::Plugin)]});
536              
537             =head3 renderer
538              
539             See L.
540              
541             =head3 route
542              
543             C can be specified in case you want to have a protected API. Example:
544              
545             $app->plugin(OpenAPI => {
546             route => $app->routes->under("/api")->to("user#auth"),
547             url => $app->home->rel_file("cool.api"),
548             });
549              
550             =head3 skip_validating_specification
551              
552             Used to prevent calling L for the
553             specification.
554              
555             =head3 spec_route_name
556              
557             Name of the route that handles the "basePath" part of the specification and
558             serves the specification. Defaults to "x-mojo-name" in the specification at
559             the top level.
560              
561             =head3 spec, url
562              
563             See L for the different C formats that is
564             accepted.
565              
566             C is an alias for "url", which might make more sense if your
567             specification is written in perl, instead of JSON or YAML.
568              
569             Here are some common uses:
570              
571             $app->plugin(OpenAPI => {url => $app->home->rel_file('openapi.yaml'));
572             $app->plugin(OpenAPI => {url => 'https://example.com/swagger.json'});
573             $app->plugin(OpenAPI => {spec => JSON::Validator::Schema::OpenAPIv3->new(...)});
574             $app->plugin(OpenAPI => {spec => {swagger => "2.0", paths => {...}, ...}});
575              
576             =head3 version_from_class
577              
578             Can be used to overridden C in the API specification, from the
579             return value from the C method in C.
580              
581             Defaults to the current C<$app>. This can be disabled by setting the
582             "version_from_class" to zero (0).
583              
584             =head1 AUTHORS
585              
586             =head2 Project Founder
587              
588             Jan Henning Thorsen - C
589              
590             =head2 Contributors
591              
592             =over 2
593              
594              
595             =item * Bernhard Graf
596              
597             =item * Doug Bell
598              
599             =item * Ed J
600              
601             =item * Henrik Andersen
602              
603             =item * Henrik Andersen
604              
605             =item * Ilya Rassadin
606              
607             =item * Jan Henning Thorsen
608              
609             =item * Jan Henning Thorsen
610              
611             =item * Ji-Hyeon Gim
612              
613             =item * Joel Berger
614              
615             =item * Krasimir Berov
616              
617             =item * Lars Thegler
618              
619             =item * Lee Johnson
620              
621             =item * Linn-Hege Kristensen
622              
623             =item * Manuel
624              
625             =item * Martin Renvoize
626              
627             =item * Mohammad S Anwar
628              
629             =item * Nick Morrott
630              
631             =item * Renee
632              
633             =item * Roy Storey
634              
635             =item * SebMourlhou <35918953+SebMourlhou@users.noreply.github.com>
636              
637             =item * SebMourlhou
638              
639             =item * SebMourlhou
640              
641             =item * Søren Lund
642              
643             =item * Stephan Hradek
644              
645             =item * Stephan Hradek
646              
647             =back
648              
649             =head1 COPYRIGHT AND LICENSE
650              
651             Copyright (C) Jan Henning Thorsen
652              
653             This program is free software, you can redistribute it and/or modify it under
654             the terms of the Artistic License version 2.0.
655              
656             =head1 SEE ALSO
657              
658             =over 2
659              
660             =item * L
661              
662             Guide for how to use this plugin with OpenAPI version 2.0 spec.
663              
664             =item * L
665              
666             Guide for how to use this plugin with OpenAPI version 3.0 spec.
667              
668             =item * L
669              
670             Plugin to add Cross-Origin Resource Sharing (CORS).
671              
672             =item * L
673              
674             Plugin for handling security definitions in your schema.
675              
676             =item * L
677              
678             Plugin for exposing your spec in human readable or JSON format.
679              
680             =item * L
681              
682             Official OpenAPI website.
683              
684             =back
685              
686             =cut