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   482876 use Mojo::Base 'Mojolicious::Plugin';
  48         121  
  48         462  
3              
4 48     48   37008 use JSON::Validator;
  48         1754775  
  48         373  
5 48     48   1976 use Mojo::JSON;
  48         135  
  48         1678  
6 48     48   374 use Mojo::Util;
  48         178  
  48         1547  
7 48     48   27051 use Mojolicious::Plugin::OpenAPI::Parameters;
  48         173  
  48         793  
8              
9 48   50 48   2523 use constant DEBUG => $ENV{MOJO_OPENAPI_DEBUG} || 0;
  48         130  
  48         178495  
10              
11             our $VERSION = '5.08';
12              
13             has route => sub {undef};
14             has validator => sub { JSON::Validator::Schema->new; };
15              
16             sub register {
17 61     61 1 72936 my ($self, $app, $config) = @_;
18              
19 61   66     489 $self->validator(JSON::Validator->new->schema($config->{url} || $config->{spec})->schema);
20 61 100       1092090 $self->validator->coerce($config->{coerce}) if defined $config->{coerce};
21              
22 61 50 66     632 if (my $class = $config->{version_from_class} // ref $app) {
23 61 100       947 $self->validator->data->{info}{version} = sprintf '%s', $class->VERSION if $class->VERSION;
24             }
25              
26 61 100       1063 my $errors = $config->{skip_validating_specification} ? [] : $self->validator->errors;
27 61 100       16673029 die @$errors if @$errors;
28              
29 59 100       1022 unless ($app->defaults->{'openapi.base_paths'}) {
30 51         1524 $app->helper('openapi.spec' => \&_helper_get_spec);
31 51         27717 $app->helper('openapi.valid_input' => \&_helper_valid_input);
32 51         18318 $app->helper('openapi.validate' => \&_helper_validate);
33 51         18433 $app->helper('reply.openapi' => \&_helper_reply);
34 51         40165 $app->hook(before_render => \&_before_render);
35 51         1520 $app->renderer->add_handler(openapi => \&_render);
36             }
37              
38 59   50     1786 $self->{log_level} = $ENV{MOJO_OPENAPI_LOG_LEVEL} || $config->{log_level} || 'warn';
39 59         456 $self->_build_route($app, $config);
40              
41             # This plugin is required
42 59         1292 my @plugins = (Mojolicious::Plugin::OpenAPI::Parameters->new->register($app, $config));
43              
44 59 50       33688 for my $plugin (@{$config->{plugins} || [qw(+Cors +SpecRenderer +Security)]}) {
  59         676  
45 177 50       41777 $plugin = "Mojolicious::Plugin::OpenAPI::$plugin" if $plugin =~ s!^\+!!;
46 177 50       19356 eval "require $plugin;1" or Carp::confess("require $plugin: $@");
47 177         2855 push @plugins, $plugin->new->register($app, {%$config, openapi => $self});
48             }
49              
50 59 50       367 my %default_response = %{$config->{default_response} || {}};
  59         595  
51 59   100     681 $default_response{name} ||= $config->{default_response_name} || 'DefaultResponse';
      33        
52 59   100     717 $default_response{status} ||= $config->{default_response_codes} || [400, 401, 404, 500, 501];
      33        
53 59         217 $default_response{location} = 'definitions';
54 59 100       139 $self->validator->add_default_response(\%default_response) if @{$default_response{status}};
  59         430  
55              
56 59         103560 $self->_add_routes($app, $config);
57              
58 57         1079 return $self;
59             }
60              
61             sub _add_routes {
62 59     59   276 my ($self, $app, $config) = @_;
63 59   100     428 my $op_spec_to_route = $config->{op_spec_to_route} || '_op_spec_to_route';
64 59         184 my (@routes, %uniq);
65              
66 59         661 for my $route ($self->validator->routes->each) {
67 129         22130 my $op_spec = $self->validator->get([paths => @$route{qw(path method)}]);
68 129   100     18038 my $name = $op_spec->{'x-mojo-name'} || $op_spec->{operationId};
69 129         266 my $r;
70              
71             die qq([OpenAPI] operationId "$op_spec->{operationId}" is not unique)
72 129 100 100     785 if $op_spec->{operationId} and $uniq{o}{$op_spec->{operationId}}++;
73 128 100 100     814 die qq([OpenAPI] Route name "$name" is not unique.) if $name and $uniq{r}{$name}++;
74              
75 127 100 100     660 if (!$op_spec->{'x-mojo-to'} and $name) {
76 109         390 $r = $self->route->root->find($name);
77 109         16396 warn "[OpenAPI] Found existing route by name '$name'.\n" if DEBUG and $r;
78 109 100       505 $self->route->add_child($r) if $r;
79             }
80 127 100       7749 if (!$r) {
81 26         67 my $http_method = $route->{method};
82 26         122 my $route_path = $self->_openapi_path_to_route_path(@$route{qw(method path)});
83 26   66     155 $name ||= $op_spec->{operationId};
84 26         45 warn "[OpenAPI] Creating new route for '$route_path'.\n" if DEBUG;
85 26         98 $r = $self->route->$http_method($route_path);
86 26 100       8863 $r->name("$self->{route_prefix}$name") if $name;
87             }
88              
89 127         803 $r->to(format => undef, 'openapi.method' => $route->{method}, 'openapi.path' => $route->{path});
90 127         4570 $self->$op_spec_to_route($op_spec, $r, $config);
91 127         332 warn "[OpenAPI] Add route $route->{method} @{[$r->to_string]} (@{[$r->name // '']})\n" if DEBUG;
92              
93 127         435 push @routes, $r;
94             }
95              
96 57         495 $app->plugins->emit_hook(openapi_routes_added => $self, \@routes);
97             }
98              
99             sub _before_render {
100 383     383   569530 my ($c, $args) = @_;
101 383 100       1080 return unless _self($c);
102 379   100     2013 my $handler = $args->{handler} || 'openapi';
103              
104             # Call _render() for response data
105 379 100 66     1803 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       3056 return $args->{handler} = $c->app->renderer->default_handler unless exists $args->{handler};
109              
110             # Call _render() for errors
111 13   100     78 my $status = $args->{status} || $c->stash('status') || '200';
112 13 50 66     159 if ($handler eq 'openapi' and ($status eq '404' or $status eq '500')) {
      66        
113 9         24 $args->{handler} = 'openapi';
114 9         56 $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         51 );
122             }
123             }
124              
125             sub _build_route {
126 59     59   228 my ($self, $app, $config) = @_;
127 59         326 my $validator = $self->validator;
128 59         631 my $base_path = $validator->base_url->path->to_string;
129 59         16577 my $route = $config->{route};
130              
131 59 50 66     393 $route = $route->any($base_path) if $route and !$route->pattern->unparsed;
132 59 100       566 $route = $app->routes->any($base_path) unless $route;
133 59         26397 $base_path = $route->to_string;
134 59         2366 $base_path =~ s!/$!!;
135              
136 59         174 push @{$app->defaults->{'openapi.base_paths'}}, [$base_path, $self];
  59         315  
137 59         1182 $route->to({format => undef, handler => 'openapi', 'openapi.object' => $self});
138 59         2065 $validator->base_url($base_path);
139              
140 59 100 100     30412 if (my $spec_route_name = $config->{spec_route_name} || $validator->get('/x-mojo-name')) {
141 4         315 $self->{route_prefix} = "$spec_route_name.";
142             }
143              
144 59   100     6770 $self->{route_prefix} //= '';
145 59         385 $self->route($route);
146             }
147              
148             sub _helper_get_spec {
149 37     37   80237 my $c = shift;
150 37   100     176 my $path = shift // 'for_current';
151 37         110 my $self = _self($c);
152              
153             # Get spec by valid JSON pointer
154 37 100 66     326 return $self->validator->get($path) if ref $path or $path =~ m!^/! or !length $path;
      66        
155              
156             # Find spec by current request
157 36         88 my ($stash) = grep { $_->{'openapi.path'} } reverse @{$c->match->stack};
  67         433  
  36         91  
158 36 100       136 return undef unless $stash;
159              
160 34         118 my $jp = [paths => $stash->{'openapi.path'}];
161 34 100       127 push @$jp, $stash->{'openapi.method'} if $path ne 'for_path'; # Internal for now
162 34         140 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   1862920 my $c = shift;
190 180 100       727 return undef if $c->res->code;
191 174 100       2805 return $c unless my @errors = _helper_validate($c);
192 32         155 $c->stash(status => 400)
193             ->render(data => $c->openapi->build_response_body({errors => \@errors, status => 400}));
194 32         12547 return undef;
195             }
196              
197             sub _helper_validate {
198 176     176   29055 my $c = shift;
199 176         543 my $self = _self($c);
200 176         676 my @errors = $self->validator->validate_request([@{$c->stash}{qw(openapi.method openapi.path)}],
  176         1083  
201             $c->openapi->build_schema_request);
202             $c->openapi->coerce_request_parameters(
203 176         59945 delete $c->stash->{'openapi.evaluated_request_parameters'});
204 176         1561 return @errors;
205             }
206              
207             sub _log {
208 29     29   89 my ($self, $c, $dir) = (shift, shift, shift);
209 29         73 my $log_level = $self->{log_level};
210              
211 29         96 $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   374 my ($self, $op_spec, $r, $config) = @_;
221 125   100     654 my $op_to = $op_spec->{'x-mojo-to'} // [];
222             my @args
223 125 50       563 = 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     625 $r->to(shift @args) if @args and $args[0] =~ m!#!;
227              
228 125         989 my ($constraints, @to) = ($r->pattern->constraints);
229 125 50 0     1228 $constraints->{format} //= $config->{format} if $config->{format};
230 125         572 while (my $arg = shift @args) {
231 7 100 33     32 if (ref $arg eq 'ARRAY') { %$constraints = (%$constraints, @$arg) }
  1 100       5  
    50          
232 1         10 elsif (ref $arg eq 'HASH') { push @to, %$arg }
233 5         16 elsif (!ref $arg and @args) { push @to, $arg, shift @args }
234             }
235              
236 125 100       498 $r->to(@to) if @to;
237             }
238              
239             sub _render {
240 159     159   19359 my ($renderer, $c, $output, $args) = @_;
241 159         570 my $stash = $c->stash;
242 159 50       1432 return unless exists $stash->{openapi};
243 159 50       413 return unless my $self = _self($c);
244              
245 159   100     1183 my $status = $args->{status} || $stash->{status} || 200;
246 159         637 my $method_path_status = [@$stash{qw(openapi.method openapi.path)}, $status];
247 159   100     794 my $op_spec
248             = $method_path_status->[0] && $self->validator->parameters_for_response($method_path_status);
249 159         33927 my @errors;
250              
251 159         425 delete $args->{encoding};
252 159         398 $args->{status} = $status;
253 159   100     835 $stash->{format} ||= 'json';
254              
255 159 100 66     581 if ($op_spec) {
    100          
256 137         501 @errors = $self->validator->validate_response($method_path_status,
257             $c->openapi->build_schema_response);
258             $c->openapi->coerce_response_parameters(
259 137         61657 delete $stash->{'openapi.evaluated_response_parameters'});
260 137 100       708 $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     86 $args->{status} ||= $stash->{openapi}{status};
264 20         46 @errors = @{$stash->{openapi}{errors}};
  20         64  
265             }
266             else {
267 2         11 $args->{status} = 501;
268 2         12 @errors = ({message => qq(No response rule for "$status".)});
269             }
270              
271 159 100       643 $self->_log($c, '>>>', \@errors) if @errors;
272 159         7157 $stash->{status} = $args->{status};
273             $$output = $c->openapi->build_response_body(
274 159 100       557 @errors ? {errors => \@errors, status => $args->{status}} : $stash->{openapi});
275             }
276              
277             sub _openapi_path_to_route_path {
278 26     26   96 my ($self, $http_method, $path) = @_;
279 4         25 my %params = map { ($_->{name}, $_) }
280 26         62 grep { $_->{in} eq 'path' } @{$self->validator->parameters_for_request([$http_method, $path])};
  23         10105  
  26         79  
281              
282 26         9644 $path =~ s/{([^}]+)}/{
283 4         11 my $name = $1;
  4         14  
284 4   100     28 my $type = $params{$name}{'x-mojo-placeholder'} || ':';
285 4         27 "<$type$name>";
286             }/ge;
287              
288 26         91 return $path;
289             }
290              
291             sub _self {
292 796     796   1484 my $c = shift;
293 796         2120 my $self = $c->stash('openapi.object');
294 796 100       9668 return $self if $self;
295 15         64 my $path = $c->req->url->path->to_string;
296 15         1165 return +(map { $_->[1] } grep { $path =~ /^$_->[0]/ } @{$c->stash('openapi.base_paths')})[0];
  11         63  
  24         462  
  15         52  
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