File Coverage

lib/Dancer/Plugin/Swagger.pm
Criterion Covered Total %
statement 80 106 75.4
branch 13 26 50.0
condition 7 16 43.7
subroutine 16 21 76.1
pod n/a
total 116 169 68.6


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