File Coverage

lib/Dancer/Plugin/Swagger.pm
Criterion Covered Total %
statement 85 111 76.5
branch 15 28 53.5
condition 9 19 47.3
subroutine 17 22 77.2
pod n/a
total 126 180 70.0


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