File Coverage

lib/Dancer/Plugin/Swagger.pm
Criterion Covered Total %
statement 82 108 75.9
branch 15 28 53.5
condition 9 19 47.3
subroutine 16 21 76.1
pod n/a
total 122 176 69.3


line stmt bran cond sub pod time code
1             # TODO: add responses
2             # TODO: add examples
3             # TODO: then add the template for different responses values
4             # TODO: override send_error ?
5             # TODO: add 'validate_schema'
6             # TODO: add 'strict_schema'
7             # TODO: make /swagger.json configurable
8              
9             package Dancer::Plugin::Swagger;
10             our $AUTHORITY = 'cpan:YANICK';
11             # ABSTRACT: create Swagger documentation of the app REST interface
12             $Dancer::Plugin::Swagger::VERSION = '0.2.1';
13 2     2   983519 use strict;
  2         18  
  2         66  
14 2     2   12 use warnings;
  2         4  
  2         58  
15              
16 2     2   10 use Dancer;
  2         5  
  2         10  
17 2     2   3981 use Dancer::Plugin;
  2         3196  
  2         149  
18 2     2   1221 use Dancer::Plugin::REST;
  2         36991  
  2         533  
19 2     2   1308 use PerlX::Maybe;
  2         5451  
  2         9  
20              
21 2     2   1067 use Dancer::Plugin::Swagger::Path;
  2         6  
  2         101  
22              
23 2     2   21 use Moo;
  2         4  
  2         10  
24              
25             with 'MooX::Singleton';
26 2     2   1498 use MooseX::MungeHas 'is_ro';
  2         24  
  2         15  
27 2     2   2050 use Class::Load qw/ load_class /;
  2         5  
  2         112  
28              
29 2     2   1923 use Path::Tiny;
  2         24310  
  2         137  
30 2     2   1263 use File::ShareDir::Tarball;
  2         126025  
  2         4175  
31              
32             sub import {
33 2   33 2   35 $Dancer::Plugin::Swagger::FIRST_LOADED ||= caller;
34 2         404972 goto &Exporter::import;
35             }
36              
37             has doc => (
38             is => 'ro',
39             lazy => 1,
40             default => sub {
41             my $self = shift;
42              
43             my $doc = {
44             swagger => '2.0',
45             paths => {},
46             };
47              
48             $doc->{info}{$_} = '' for qw/ title description version /;
49              
50             $doc->{info}{title} = $self->main_api_module if $self->main_api_module;
51              
52             if( my( $desc) = $self->main_api_module_content =~ /
53             ^(?:\s* \# \s* ABSTRACT: \s* |=head1 \s+ NAME \s+ (?:\w+) \s+ - \s+ ) ([^\n]+)
54             /xm
55             ) {
56             $doc->{info}{description} = $desc;
57             }
58              
59             $doc->{info}{version} = eval {
60             $self->main_api_module->VERSION
61             } // '0.0.0';
62              
63             $doc;
64            
65             },
66             );
67              
68             has main_api_module => (
69             is => 'ro',
70             lazy => 1,
71             default => sub {
72             plugin_setting->{main_api_module}
73             || $Dancer::Plugin::Swagger::FIRST_LOADED;
74             },
75             );
76              
77             has main_api_module_content => (
78             is => 'ro',
79             lazy => 1,
80             default => sub {
81             my $mod = $_[0]->main_api_module or return '';
82             $mod =~ s#::#/#g;
83             $mod .= '.pm';
84             Path::Tiny::path( $INC{$mod} )->slurp;
85             }
86             );
87              
88             has show_ui => (
89             is => 'ro',
90             lazy => 1,
91             default => sub { plugin_setting->{show_ui} // 1 },
92             );
93              
94             has ui_url => (
95             is => 'ro',
96             lazy => 1,
97             default => sub { plugin_setting->{ui_url} // '/doc' },
98             );
99              
100             has ui_dir => (
101             is => 'ro',
102             lazy => 1,
103             default => sub {
104             Path::Tiny::path(
105             plugin_setting->{ui_dir} ||
106             File::ShareDir::Tarball::dist_dir('Dancer-Plugin-Swagger')
107             )
108             },
109             );
110              
111             has auto_discover_skip => (
112             is => 'ro',
113             lazy => 1,
114             default => sub { [
115             map { /^qr/ ? eval $_ : $_ }
116             @{ plugin_setting->{auto_discover_skip} || [
117             '/swagger.json', ( 'qr!' . $_[0]->ui_url . '!' ) x $_[0]->show_ui
118             ] }
119             ];
120             },
121             );
122              
123 0     0   0 has validate_response => sub { plugin_setting->{validate_response} };
124 0     0   0 has strict_validation => sub { plugin_setting->{strict_validation} };
125              
126             my $plugin = __PACKAGE__->instance;
127              
128             if ( $plugin->show_ui ) {
129             my $base_url = $plugin->ui_url;
130              
131             get $base_url => sub { redirect $base_url . '/?url=/swagger.json' };
132              
133             get $base_url . '/' => sub {
134             my $file = $plugin->ui_dir->child('index.html');
135              
136             send_error "file not found", 404 unless -f $file;
137              
138             return $file->slurp;
139             };
140              
141             get $base_url.'/**' => sub {
142             my $file = $plugin->ui_dir->child( @{ (splat())[0] } );
143              
144             send_error "file not found", 404 unless -f $file;
145              
146             send_file $file, system_path => 1;
147             };
148              
149             }
150              
151             # TODO make the doc url configurable
152              
153             get '/swagger.json' => sub {
154             $plugin->doc
155             };
156              
157             register swagger_auto_discover => sub {
158 0     0   0 my %args = @_;
159              
160 0   0     0 $args{skip} ||= $plugin->auto_discover_skip;
161              
162 0         0 my $routes = Dancer::App->current->registry->routes;
163              
164 0         0 my $doc = $plugin->doc->{paths};
165              
166 0         0 for my $method ( qw/ get post put delete / ) {
167 0         0 for my $r ( @{ $routes->{$method} } ) {
  0         0  
168 0         0 my $pattern = $r->pattern;
169              
170 0 0       0 next if ref $pattern eq 'Regexp';
171              
172 0 0       0 next if grep { ref $_ ? $pattern =~ $_ : $pattern eq $_ } @{ $args{skip} };
  0 0       0  
  0         0  
173              
174 0         0 my $path = Dancer::Plugin::Swagger::Path->new( route => $r );
175              
176 0         0 warn "adding $path";
177              
178 0         0 $path->add_to_doc($plugin->doc);
179              
180             }
181             }
182             };
183              
184             register swagger_path => sub {
185 8     8   125909 my @routes;
186 8         20 push @routes, pop @_ while eval { $_[-1]->isa('Dancer::Route') };
  24         178  
187              
188             # we don't process HEAD
189 8         37 @routes = grep { $_->method ne 'head' } @routes;
  16         92  
190              
191 8         74 my $description;
192 8 100 100     107 if( @_ and not ref $_[0] ) {
193 1         3 $description = shift;
194 1         8 $description =~ s/^\s*\n//;
195            
196 1 50       24 $description =~ s/^$1//mg
197             if $description =~ /^(\s+)/;
198             }
199              
200 8   100     34 my $arg = shift @_ || {};
201              
202 8 100       28 $arg->{description} = $description if $description;
203              
204             # groom the parameters
205 8 100       26 if ( my $p = $arg->{parameters} ) {
206 4 100       16 if( ref $p eq 'HASH' ) {
207 2         10 $_ = { description => $_ } for grep { ! ref } values %$p;
  4         62  
208 2         11 $p = [ map { +{ name => $_, %{$p->{$_}} } } sort keys %$p ];
  4         9  
  4         18  
209             }
210              
211             # deal with named parameters
212 4         9 my @p;
213 4         19 while( my $k = shift @$p ) {
214 9 100       25 unless( ref $k ) {
215 2         5 my $value = shift @$p;
216 2 100       9 $value = { description => $value } unless ref $value;
217 2         4 $value->{name} = $k;
218 2         4 $k = $value;
219             }
220 9 100 66     36 if (!exists($k->{schema}) && !exists($k->{type})) {
221             # set a default type iff it's missing and this doesn't reference a schema
222 3         6 $k->{type} = 'string';
223             }
224 9         26 push @p, $k;
225             }
226 4         11 $p = \@p;
227              
228             # set defaults
229 4         10 $p = [ map { +{ in => 'query', %$_ } } @$p ];
  9         57  
230            
231 4         16 $arg->{parameters} = $p;
232             }
233              
234              
235 8         21 for my $route ( @routes ) {
236 8         218 my $path = Dancer::Plugin::Swagger::Path->new(%$arg, route => $route);
237              
238 8         229 $path->add_to_doc( $plugin->doc );
239              
240 8         29 my $code = $route->code;
241            
242             $route->code(sub {
243 5     5   51392 local $Dancer::Plugin::Swagger::THIS_ACTION = $path;
244 5         18 $code->();
245 8         97 });
246             }
247             };
248              
249             register swagger_template => sub {
250              
251 0     0   0 my $vars = pop;
252 0   0     0 my $status = shift || Dancer::status();
253              
254 0         0 my $template = $Dancer::Plugin::Swagger::THIS_ACTION->{responses}{$status}{template};
255              
256 0 0       0 Dancer::status( $status ) if $status =~ /^\d{3}$/;
257              
258 0 0       0 return swagger_response( $status, $template ? $template->($vars) : $vars );
259             };
260              
261             sub swagger_response {
262 0     0   0 my $data = pop;
263              
264 0         0 my $status = Dancer::status(@_);
265              
266 0 0       0 $Dancer::Plugin::Swagger::THIS_ACTION->validate_response(
267             $status => $data, $plugin->strict_validation
268             ) if $plugin->validate_response;
269              
270 0         0 $data;
271             }
272              
273             register swagger_response => \&swagger_response;
274              
275             register swagger_definition => sub {
276 1     1   35660 my( $name, $def ) = @_;
277              
278 1   50     33 $plugin->doc->{definitions} ||= {};
279              
280 1         19 $plugin->doc->{definitions}{$name} = $def;
281              
282 1         14 return { '$ref', => '#/definitions/'.$name };
283              
284             };
285              
286             register_plugin;
287              
288             1;
289              
290             __END__
291              
292             =pod
293              
294             =encoding UTF-8
295              
296             =head1 NAME
297              
298             Dancer::Plugin::Swagger - create Swagger documentation of the app REST interface
299              
300             =head1 VERSION
301              
302             version 0.2.1
303              
304             =head1 SYNOPSIS
305              
306             package MyApp;
307              
308             use Dancer;
309             use Dancer::Plugin::Swagger;
310              
311             our $VERSION = "0.1";
312              
313             get '/choreograph/:name' => sub { ... };
314              
315             1;
316              
317             =head1 DESCRIPTION
318              
319             This plugin provides tools to create and access a L<Swagger|http://swagger.io/> specification file for a
320             Dancer REST web service.
321              
322             Overview of C<Dancer::Plugin::Swagger>'s features:
323              
324             =over
325              
326             =item Can create a F</swagger.json> REST specification file.
327              
328             =item Can auto-discover routes and add them to the swagger file.
329              
330             =item Can provide a Swagger UI version of the swagger documentation.
331              
332             =back
333              
334             =head1 CONFIGURATION
335              
336             plugins:
337             Swagger:
338             main_api_module: MyApp
339             show_ui: 1
340             ui_url: /doc
341             ui_dir: /path/to/files
342             auto_discover_skip:
343             - /swagger.json
344             - qr#^/doc/#
345              
346             =head2 main_api_module
347              
348             If not provided explicitly, the Swagger document's title and version will be set
349             to the abstract and version of this module.
350              
351             Defaults to the first
352             module to import L<Dancer::Plugin::Swagger>.
353              
354             =head2 show_ui
355              
356             If C<true>, a route will be created for the Swagger UI (see L<http://swagger.io/swagger-ui/>).
357              
358             Defaults to C<true>.
359              
360             =head2 ui_url
361              
362             Path of the swagger ui route. Will also be the prefix for all the CSS/JS dependencies of the page.
363              
364             Defaults to C</doc>.
365              
366             =head2 ui_dir
367              
368             Filesystem path to the directory holding the assets for the Swagger UI page.
369              
370             Defaults to a copy of the Swagger UI code bundled with the L<Dancer::Plugin::Swagger> distribution.
371              
372             =head2 auto_discover_skip
373              
374             List of urls that should not be added to the Swagger document by C<swagger_auto_discover>.
375             If an url begins with C<qr>, it will be compiled as a regular expression.
376              
377             Defauls to C</swagger.json> and, if C<show_ui> is C<true>, all the urls under C<ui_url>.
378              
379             =head2 validate_response
380              
381             If set to C<true>, calls to C<swagger_response> will verify if a schema is defined
382             for the response, and if so validate against it. L<JSON::Schema::AsType> is used for the
383             validation (and this required if this option is used).
384              
385             Defaults to C<false>.
386              
387             =head2 strict_validation
388              
389             If set to C<true>, dies if a call to C<swagger_response> doesn't find a schema for its response.
390              
391             Defaults to C<false>.
392              
393             =head1 PLUGIN KEYWORDS
394              
395             =head2 swagger_path $description, \%args, $route
396              
397             swagger_path {
398             description => 'Returns info about a judge',
399             },
400             get '/judge/:judge_name' => sub {
401             ...;
402             };
403              
404             Registers a route as a swagger path item in the swagger document.
405              
406             C<%args> is optional.
407              
408             The C<$description> is optional as well, and can also be defined as part of the
409             C<%args>.
410              
411             # equivalent to the main example
412             swagger_path 'Returns info about a judge',
413             get '/judge/:judge_name' => sub {
414             ...;
415             };
416              
417             If the C<$description> spans many lines, it will be left-trimmed.
418              
419             swagger_path q{
420             Returns info about a judge.
421              
422             Some more documentation can go here.
423              
424             And this will be seen as a performatted block
425             by swagger.
426             },
427             get '/judge/:judge_name' => sub {
428             ...;
429             };
430              
431             =head3 Supported arguments
432              
433             =over
434              
435             =item method
436              
437             The HTTP method (GET, POST, etc) for the path item.
438              
439             Defaults to the route's method.
440              
441             =item path
442              
443             The url for the path item.
444              
445             Defaults to the route's path.
446              
447             =item description
448              
449             The path item's description.
450              
451             =item tags
452              
453             Optional arrayref of tags assigned to the path.
454              
455             =item parameters
456              
457             List of parameters for the path item. Must be an arrayref or a hashref.
458              
459             Route parameters are automatically populated. E.g.,
460              
461             swagger_path
462             get '/judge/:judge_name' => { ... };
463              
464             is equivalent to
465              
466             swagger_path {
467             parameters => [
468             { name => 'judge_name', in => 'path', required => 1, type => 'string' },
469             ]
470             },
471             get '/judge/:judge_name' => { ... };
472              
473             If the parameters are passed as a hashref, the keys are the names of the parameters, and they will
474             appear in the swagger document following their alphabetical order.
475              
476             If the parameters are passed as an arrayref, they will appear in the document in the order
477             in which they are passed. Additionally, each parameter can be given as a hashref, or can be a
478             C<< name => arguments >> pair.
479              
480             In both format, for the key/value pairs, a string value is considered to be the
481             C<description> of the parameter.
482              
483             Finally, if not specified explicitly, the C<in> argument of a parameter defaults to C<query>,
484             and its type to C<string>.
485              
486             parameters => [
487             { name => 'bar', in => 'path', required => 1, type => 'string' },
488             { name => 'foo', in => 'query', type => 'string', description => 'yadah' },
489             ],
490              
491             # equivalent arrayref with mixed pairs/non-pairs
492              
493             parameters => [
494             { name => 'bar', in => 'path', required => 1, type => 'string' },
495             foo => { in => 'query', type => 'string', description => 'yadah' },
496             ],
497              
498             # equivalent hashref format
499            
500             parameters => {
501             bar => { in => 'path', required => 1, type => 'string' },
502             foo => { in => 'query', type => 'string', description => 'yadah' },
503             },
504              
505             # equivalent, using defaults
506             parameters => {
507             bar => { in => 'path', required => 1 },
508             foo => 'yadah',
509             },
510              
511             =item responses
512              
513             Possible responses from the path. Must be a hashref.
514              
515             swagger_path {
516             responses => {
517             default => { description => 'The judge information' }
518             },
519             },
520             get '/judge/:judge_name' => { ... };
521              
522             If the key C<example> is given (instead of C<examples> as defined by the Swagger specs),
523             and the serializer used by the application is L<Dancer::Serializer::JSON> or L<Dancer::Serializer::YAML>,
524             the example will be expanded to have the right content-type key.
525              
526             swagger_path {
527             responses => {
528             default => { example => { fullname => 'Mary Ann Murphy' } }
529             },
530             },
531             get '/judge/:judge_name' => { ... };
532              
533             # equivalent to
534              
535             swagger_path {
536             responses => {
537             default => { examples => { 'application/json' => { fullname => 'Mary Ann Murphy' } } }
538             },
539             },
540             get '/judge/:judge_name' => { ... };
541              
542             The special key C<template> will not appear in the Swagger doc, but will be
543             used by the C<swagger_template> plugin keyword.
544              
545             =back
546              
547             =head2 swagger_template $code, $args
548              
549             swagger_path {
550             responses => {
551             404 => { template => sub { +{ error => "judge '$_[0]' not found" } }
552             },
553             },
554             get '/judge/:judge_name' => {
555             my $name = param('judge_name');
556             return swagger_template 404, $name unless in_db($name);
557             ...;
558             };
559              
560             Calls the template for the C<$code> response, passing it C<$args>. If C<$code> is numerical, also set
561             the response's status to that value.
562              
563             =head2 swagger_auto_discover skip => \@list
564              
565             Populates the Swagger document with information of all
566             the routes of the application.
567              
568             Accepts an optional C<skip> parameter that takes an arrayref of
569             routes that shouldn't be added to the Swagger document. The routes
570             can be specified as-is, or via regular expressions. If no skip list is given, defaults to
571             the c<auto_discover_skip> configuration value.
572              
573             swagger_auto_discover skip => [ '/swagger.json', qr#^/doc/# ];
574              
575             The information of a route won't be altered if it's
576             already present in the document.
577              
578             If a route has path parameters, they will be automatically
579             added as such in the C<parameters> section.
580              
581             Routes defined as regexes are skipped, as there is no clean way
582             to automatically make them look nice.
583              
584             # will be picked up
585             get '/user' => ...;
586              
587             # ditto, as '/user/{user_id}'
588             get '/user/:user_id => ...;
589              
590             # won't be picked up
591             get qr#/user/(\d+)# => ...;
592              
593             Note that routes defined after C<swagger_auto_discover> has been called won't
594             be added to the Swagger document. Typically, you'll want C<swagger_auto_discover>
595             to be called at the very end of your module. Alternatively, C<swagger_auto_discover>
596             can be called more than once safely -- which can be useful if an application creates
597             routes dynamically.
598              
599             =head2 swagger_definition $name => $definition, ...
600              
601             Adds a schema (or more) to the definition section of the Swagger document.
602              
603             swagger_definition 'Judge' => {
604             type => 'object',
605             required => [ 'fullname' ],
606             properties => {
607             fullname => { type => 'string' },
608             seasons => { type => 'array', items => { type => 'integer' } },
609             }
610             };
611              
612             The function returns the reference to the definition that can be then used where
613             schemas are used.
614              
615             my $Judge = swagger_definition 'Judge' => { ... };
616             # $Judge is now the hashref '{ '$ref' => '#/definitions/Judge' }'
617            
618             # later on...
619             swagger_path {
620             responses => {
621             default => { schema => $Judge },
622             },
623             },
624             get '/judge/:name' => sub { ... };
625              
626             =head1 EXAMPLES
627              
628             See the F<examples/> directory of the distribution for a working example.
629              
630             =head1 SEE ALSO
631              
632             =over
633              
634             =item L<http://swagger.io/|Swagger>
635              
636             =back
637              
638             =head1 AUTHOR
639              
640             Yanick Champoux <yanick@cpan.org>
641              
642             =head1 COPYRIGHT AND LICENSE
643              
644             This software is copyright (c) 2021, 2016, 2015 by Yanick Champoux.
645              
646             This is free software; you can redistribute it and/or modify it under
647             the same terms as the Perl 5 programming language system itself.
648              
649             =cut