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   475479 use Mojo::Base 'Mojolicious::Plugin';
  48         127  
  48         425  
3              
4 48     48   36917 use JSON::Validator;
  48         1736020  
  48         406  
5 48     48   2066 use Mojo::JSON;
  48         158  
  48         1690  
6 48     48   396 use Mojo::Util;
  48         137  
  48         1509  
7 48     48   27351 use Mojolicious::Plugin::OpenAPI::Parameters;
  48         153  
  48         746  
8              
9 48   50 48   2398 use constant DEBUG => $ENV{MOJO_OPENAPI_DEBUG} || 0;
  48         158  
  48         176635  
10              
11             our $VERSION = '5.09';
12              
13             has route => sub {undef};
14             has validator => sub { JSON::Validator::Schema->new; };
15              
16             sub register {
17 61     61 1 74617 my ($self, $app, $config) = @_;
18              
19 61   66     478 $self->validator(JSON::Validator->new->schema($config->{url} || $config->{spec})->schema);
20 61 100       1091690 $self->validator->coerce($config->{coerce}) if defined $config->{coerce};
21              
22 61 50 66     706 if (my $class = $config->{version_from_class} // ref $app) {
23 61 100       1052 $self->validator->data->{info}{version} = sprintf '%s', $class->VERSION if $class->VERSION;
24             }
25              
26 61 100       1129 my $errors = $config->{skip_validating_specification} ? [] : $self->validator->errors;
27 61 100       16621652 die @$errors if @$errors;
28              
29 59 100       987 unless ($app->defaults->{'openapi.base_paths'}) {
30 51         1582 $app->helper('openapi.spec' => \&_helper_get_spec);
31 51         28260 $app->helper('openapi.valid_input' => \&_helper_valid_input);
32 51         17562 $app->helper('openapi.validate' => \&_helper_validate);
33 51         18318 $app->helper('reply.openapi' => \&_helper_reply);
34 51         40666 $app->hook(before_render => \&_before_render);
35 51         1644 $app->renderer->add_handler(openapi => \&_render);
36             }
37              
38 59   50     1798 $self->{log_level} = $ENV{MOJO_OPENAPI_LOG_LEVEL} || $config->{log_level} || 'warn';
39 59         451 $self->_build_route($app, $config);
40              
41             # This plugin is required
42 59         1324 my @plugins = (Mojolicious::Plugin::OpenAPI::Parameters->new->register($app, $config));
43              
44 59 50       33359 for my $plugin (@{$config->{plugins} || [qw(+Cors +SpecRenderer +Security)]}) {
  59         736  
45 177 50       41265 $plugin = "Mojolicious::Plugin::OpenAPI::$plugin" if $plugin =~ s!^\+!!;
46 177 50       19636 eval "require $plugin;1" or Carp::confess("require $plugin: $@");
47 177         2921 push @plugins, $plugin->new->register($app, {%$config, openapi => $self});
48             }
49              
50 59 50       397 my %default_response = %{$config->{default_response} || {}};
  59         610  
51 59   100     720 $default_response{name} ||= $config->{default_response_name} || 'DefaultResponse';
      33        
52 59   100     784 $default_response{status} ||= $config->{default_response_codes} || [400, 401, 404, 500, 501];
      33        
53 59         228 $default_response{location} = 'definitions';
54 59 100       160 $self->validator->add_default_response(\%default_response) if @{$default_response{status}};
  59         477  
55              
56 59         104457 $self->_add_routes($app, $config);
57              
58 57         1140 return $self;
59             }
60              
61             sub _add_routes {
62 59     59   285 my ($self, $app, $config) = @_;
63 59   100     425 my $op_spec_to_route = $config->{op_spec_to_route} || '_op_spec_to_route';
64 59         182 my (@routes, %uniq);
65              
66 59         292 for my $route ($self->validator->routes->each) {
67 129         21919 my $op_spec = $self->validator->get([paths => @$route{qw(path method)}]);
68 129   100     18140 my $name = $op_spec->{'x-mojo-name'} || $op_spec->{operationId};
69 129         267 my $r;
70              
71             die qq([OpenAPI] operationId "$op_spec->{operationId}" is not unique)
72 129 100 100     835 if $op_spec->{operationId} and $uniq{o}{$op_spec->{operationId}}++;
73 128 100 100     842 die qq([OpenAPI] Route name "$name" is not unique.) if $name and $uniq{r}{$name}++;
74              
75 127 100 100     634 if (!$op_spec->{'x-mojo-to'} and $name) {
76 109         421 $r = $self->route->root->find($name);
77 109         16826 warn "[OpenAPI] Found existing route by name '$name'.\n" if DEBUG and $r;
78 109 100       511 $self->route->add_child($r) if $r;
79             }
80 127 100       8058 if (!$r) {
81 26         85 my $http_method = $route->{method};
82 26         527 my $route_path = $self->_openapi_path_to_route_path(@$route{qw(method path)});
83 26   66     175 $name ||= $op_spec->{operationId};
84 26         47 warn "[OpenAPI] Creating new route for '$route_path'.\n" if DEBUG;
85 26         112 $r = $self->route->$http_method($route_path);
86 26 100       8975 $r->name("$self->{route_prefix}$name") if $name;
87             }
88              
89 127         753 $r->to(format => undef, 'openapi.method' => $route->{method}, 'openapi.path' => $route->{path});
90 127         4787 $self->$op_spec_to_route($op_spec, $r, $config);
91 127         326 warn "[OpenAPI] Add route $route->{method} @{[$r->to_string]} (@{[$r->name // '']})\n" if DEBUG;
92              
93 127         454 push @routes, $r;
94             }
95              
96 57         545 $app->plugins->emit_hook(openapi_routes_added => $self, \@routes);
97             }
98              
99             sub _before_render {
100 383     383   573902 my ($c, $args) = @_;
101 383 100       1079 return unless _self($c);
102 379   100     2047 my $handler = $args->{handler} || 'openapi';
103              
104             # Call _render() for response data
105 379 100 66     1726 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       3151 return $args->{handler} = $c->app->renderer->default_handler unless exists $args->{handler};
109              
110             # Call _render() for errors
111 13   100     109 my $status = $args->{status} || $c->stash('status') || '200';
112 13 50 66     184 if ($handler eq 'openapi' and ($status eq '404' or $status eq '500')) {
      66        
113 9         48 $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         46 );
122             }
123             }
124              
125             sub _build_route {
126 59     59   236 my ($self, $app, $config) = @_;
127 59         339 my $validator = $self->validator;
128 59         675 my $base_path = $validator->base_url->path->to_string;
129 59         16645 my $route = $config->{route};
130              
131 59 50 66     459 $route = $route->any($base_path) if $route and !$route->pattern->unparsed;
132 59 100       642 $route = $app->routes->any($base_path) unless $route;
133 59         27601 $base_path = $route->to_string;
134 59         2497 $base_path =~ s!/$!!;
135              
136 59         206 push @{$app->defaults->{'openapi.base_paths'}}, [$base_path, $self];
  59         399  
137 59         1242 $route->to({format => undef, handler => 'openapi', 'openapi.object' => $self});
138 59         2127 $validator->base_url($base_path);
139              
140 59 100 100     30623 if (my $spec_route_name = $config->{spec_route_name} || $validator->get('/x-mojo-name')) {
141 4         381 $self->{route_prefix} = "$spec_route_name.";
142             }
143              
144 59   100     6981 $self->{route_prefix} //= '';
145 59         386 $self->route($route);
146             }
147              
148             sub _helper_get_spec {
149 37     37   74917 my $c = shift;
150 37   100     181 my $path = shift // 'for_current';
151 37         104 my $self = _self($c);
152              
153             # Get spec by valid JSON pointer
154 37 100 66     335 return $self->validator->get($path) if ref $path or $path =~ m!^/! or !length $path;
      66        
155              
156             # Find spec by current request
157 36         77 my ($stash) = grep { $_->{'openapi.path'} } reverse @{$c->match->stack};
  67         396  
  36         106  
158 36 100       111 return undef unless $stash;
159              
160 34         116 my $jp = [paths => $stash->{'openapi.path'}];
161 34 100       143 push @$jp, $stash->{'openapi.method'} if $path ne 'for_path'; # Internal for now
162 34         107 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   1856523 my $c = shift;
190 180 100       729 return undef if $c->res->code;
191 174 100       2842 return $c unless my @errors = _helper_validate($c);
192 32         193 $c->stash(status => 400)
193             ->render(data => $c->openapi->build_response_body({errors => \@errors, status => 400}));
194 32         12206 return undef;
195             }
196              
197             sub _helper_validate {
198 176     176   29649 my $c = shift;
199 176         544 my $self = _self($c);
200 176         706 my @errors = $self->validator->validate_request([@{$c->stash}{qw(openapi.method openapi.path)}],
  176         1024  
201             $c->openapi->build_schema_request);
202             $c->openapi->coerce_request_parameters(
203 176         59755 delete $c->stash->{'openapi.evaluated_request_parameters'});
204 176         1662 return @errors;
205             }
206              
207             sub _log {
208 29     29   108 my ($self, $c, $dir) = (shift, shift, shift);
209 29         81 my $log_level = $self->{log_level};
210              
211 29         100 $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   429 my ($self, $op_spec, $r, $config) = @_;
221 125   100     699 my $op_to = $op_spec->{'x-mojo-to'} // [];
222             my @args
223 125 50       633 = 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     586 $r->to(shift @args) if @args and $args[0] =~ m!#!;
227              
228 125         1003 my ($constraints, @to) = ($r->pattern->constraints);
229 125 50 0     1279 $constraints->{format} //= $config->{format} if $config->{format};
230 125         553 while (my $arg = shift @args) {
231 7 100 33     33 if (ref $arg eq 'ARRAY') { %$constraints = (%$constraints, @$arg) }
  1 100       7  
    50          
232 1         8 elsif (ref $arg eq 'HASH') { push @to, %$arg }
233 5         20 elsif (!ref $arg and @args) { push @to, $arg, shift @args }
234             }
235              
236 125 100       552 $r->to(@to) if @to;
237             }
238              
239             sub _render {
240 159     159   19185 my ($renderer, $c, $output, $args) = @_;
241 159         471 my $stash = $c->stash;
242 159 50       1350 return unless exists $stash->{openapi};
243 159 50       434 return unless my $self = _self($c);
244              
245 159   100     1150 my $status = $args->{status} || $stash->{status} || 200;
246 159         659 my $method_path_status = [@$stash{qw(openapi.method openapi.path)}, $status];
247 159   100     803 my $op_spec
248             = $method_path_status->[0] && $self->validator->parameters_for_response($method_path_status);
249 159         34324 my @errors;
250              
251 159         372 delete $args->{encoding};
252 159         401 $args->{status} = $status;
253 159   100     884 $stash->{format} ||= 'json';
254              
255 159 100 66     573 if ($op_spec) {
    100          
256 137         441 @errors = $self->validator->validate_response($method_path_status,
257             $c->openapi->build_schema_response);
258             $c->openapi->coerce_response_parameters(
259 137         60633 delete $stash->{'openapi.evaluated_response_parameters'});
260 137 100       674 $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     102 $args->{status} ||= $stash->{openapi}{status};
264 20         41 @errors = @{$stash->{openapi}{errors}};
  20         64  
265             }
266             else {
267 2         6 $args->{status} = 501;
268 2         11 @errors = ({message => qq(No response rule for "$status".)});
269             }
270              
271 159 100       640 $self->_log($c, '>>>', \@errors) if @errors;
272 159         7172 $stash->{status} = $args->{status};
273             $$output = $c->openapi->build_response_body(
274 159 100       549 @errors ? {errors => \@errors, status => $args->{status}} : $stash->{openapi});
275             }
276              
277             sub _openapi_path_to_route_path {
278 26     26   94 my ($self, $http_method, $path) = @_;
279 4         37 my %params = map { ($_->{name}, $_) }
280 26         65 grep { $_->{in} eq 'path' } @{$self->validator->parameters_for_request([$http_method, $path])};
  23         10019  
  26         101  
281              
282 26         9575 $path =~ s/{([^}]+)}/{
283 4         9 my $name = $1;
  4         13  
284 4   100     32 my $type = $params{$name}{'x-mojo-placeholder'} || ':';
285 4         26 "<$type$name>";
286             }/ge;
287              
288 26         90 return $path;
289             }
290              
291             sub _self {
292 796     796   1484 my $c = shift;
293 796         2202 my $self = $c->stash('openapi.object');
294 796 100       9533 return $self if $self;
295 15         56 my $path = $c->req->url->path->to_string;
296 15         1144 return +(map { $_->[1] } grep { $path =~ /^$_->[0]/ } @{$c->stash('openapi.base_paths')})[0];
  11         69  
  24         464  
  15         58  
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