File Coverage

blib/lib/Raisin/Plugin/Swagger.pm
Criterion Covered Total %
statement 217 257 84.4
branch 98 146 67.1
condition 20 39 51.2
subroutine 31 33 93.9
pod 2 2 100.0
total 368 477 77.1


line stmt bran cond sub pod time code
1             #!perl
2             #PODNAME: Raisin::Plugin::Swagger
3             #ABSTRACT: Generates API description in Swagger 2/OpenAPI compatible format
4             # vim:ts=4:shiftwidth=4:expandtab:syntax=perl
5              
6 5     5   3735 use strict;
  5         9  
  5         128  
7 5     5   24 use warnings;
  5         6  
  5         208  
8              
9             package Raisin::Plugin::Swagger;
10             $Raisin::Plugin::Swagger::VERSION = '0.94';
11 5     5   29 use parent 'Raisin::Plugin';
  5         9  
  5         25  
12              
13 5     5   232 use Carp 'croak';
  5         9  
  5         193  
14 5     5   2046 use Data::Dumper;
  5         20735  
  5         239  
15 5     5   29 use Digest::MD5 qw/md5_hex/;
  5         7  
  5         207  
16 5     5   25 use JSON::MaybeXS qw/encode_json/;
  5         8  
  5         179  
17 5     5   25 use List::Util qw/pairmap/;
  5         7  
  5         482  
18 5     5   30 use Scalar::Util qw(blessed);
  5         10  
  5         14458  
19              
20             my %DEFAULTS;
21             my %SETTINGS;
22              
23             my $HTTP_OK = 200;
24              
25             sub build {
26 3     3 1 13 my $self = shift;
27              
28             $self->register(
29 19     19   41 swagger_build_spec => sub { $self->_spec_20 },
30 3     3   1042 swagger_setup => sub { %SETTINGS = @_ },
31 3         32 swagger_security => \&swagger_security,
32             );
33              
34 3         10 1;
35             }
36              
37             sub swagger_security {
38 0     0 1 0 my %p = @_;
39              
40 0 0       0 croak 'Invalid `type`' unless grep { $p{type} eq $_ } qw/basic api_key oauth2/;
  0         0  
41              
42 0         0 my %security;
43              
44 0 0       0 if ($p{type} eq 'basic') {
    0          
    0          
45             $security{ $p{name} } = {
46 0         0 type => 'basic',
47             };
48             }
49             elsif ($p{type} eq 'api_key') {
50 0 0       0 croak 'Invalid `in`' unless grep { $p{in} eq $_ } qw/query header/;
  0         0  
51              
52             $security{ $p{name} } = {
53             type => 'apiKey',
54             name => $p{name},
55             in => $p{in},
56 0         0 };
57             }
58             elsif ($p{type} eq 'oauth2') {
59 0 0       0 croak 'Invalid `flow`' unless grep { $p{flow} eq $_ } qw/implicit password application accessCode/;
  0         0  
60              
61             $security{ $p{name} } = {
62             type => 'oauth2',
63             flow => $p{flow},
64             scopes => $p{scopes},
65 0         0 };
66              
67 0 0       0 if (grep { $p{flow} eq $_ } qw/implicit accessCode/) {
  0         0  
68 0         0 $security{ $p{name} }{authorizationUrl} = $p{authorization_url};
69             }
70              
71 0 0       0 if (grep { $p{flow} eq $_ } qw/password application accessCode/) {
  0         0  
72 0         0 $security{ $p{name} }{tokenUrl} = $p{token_url};
73             }
74             }
75              
76             $SETTINGS{security} = {
77 0 0       0 %{ $SETTINGS{security} || {} },
  0         0  
78             %security,
79             };
80             }
81              
82             sub _spec_20 {
83 20     20   32 my $self = shift;
84 20 100       55 return 1 if $self->{built};
85              
86 2         9 my $app = $self->app;
87 2         7 my $req = $app->req;
88 2         7 my $routes = $app->routes->routes;
89              
90 2 50       13 my @content_types = $app->format
91             ? $app->format
92             : qw(application/x-yaml application/json);
93              
94 2         20 my $base_path = $req->base->as_string;
95             ### Respect proxied requests
96             # A proxy map is used to fill the "basePath" attribute.
97 2   50     461 my $_base = $req->env->{HTTP_X_FORWARDED_SCRIPT_NAME} || q(/);
98 2         32 $base_path =~ s#http(?:s?)://[^/]+/#$_base#msix;
99              
100 2         7 $DEFAULTS{consumes} = \@content_types;
101 2         4 $DEFAULTS{produces} = \@content_types;
102              
103             my %spec = (
104             swagger => '2.0',
105             info => _info_object($app),
106             ### Respect proxied requests
107             # The frontend hostname is used if set.
108             host => $req->env->{HTTP_X_FORWARDED_HOST}
109             || $req->env->{SERVER_NAME}
110             || $req->env->{HTTP_HOST},
111 2   33     8 basePath => $base_path,
112             schemes => [$req->scheme],
113             consumes => \@content_types,
114             produces => \@content_types,
115             paths => _paths_object($routes),
116             definitions => _definitions_object($routes),
117             #parameters => undef,
118             #responses => undef,
119             securityDefinitions => _security_definitions_object(),
120             security => _security_object(),
121             #tags => undef,
122             #externalDocs => undef,
123             );
124              
125 2         10 my $tags = _tags_object($self->app);
126 2 50       7 if (scalar @$tags) {
127 0         0 $spec{tags} = $tags;
128             }
129              
130             # routes
131             $self->app->add_route(
132             method => 'GET',
133             path => '/swagger',
134 0     0   0 code => sub { \%spec }
135 2         6 );
136              
137             # mark as built
138 2         13 $self->{built} = 1;
139              
140 2         9 \%spec;
141             }
142              
143             sub _contact_object {
144 2     2   4 my $contact = shift;
145 2         4 my %obj;
146 2         3 for (qw(name url email)) {
147 6 50       19 $obj{$_} = $contact->{$_} if $contact->{$_};
148             }
149 2         5 \%obj;
150             }
151              
152             sub _license_object {
153 2     2   3 my $license = shift;
154             my %obj = (
155             name => $license->{name},
156 2         6 );
157 2 50       14 $obj{url} = $license->{url} if $license->{url};
158 2         4 \%obj;
159             }
160              
161             sub _info_object {
162 4     4   7 my $app = shift;
163              
164             my %obj = (
165 4   100     33 title => $SETTINGS{title} || 'API',
      100        
166             version => $app->api_version || '0.0.1',
167             );
168              
169 4 100       14 $obj{description} = $SETTINGS{description} if $SETTINGS{description};
170 4 100       16 $obj{termsOfService} = $SETTINGS{terms_of_service} if $SETTINGS{terms_of_service};
171              
172 4 100       15 $obj{contact} = _contact_object($SETTINGS{contact}) if keys %{ $SETTINGS{contact} };
  4         28  
173 4 100       11 $obj{license} = _license_object($SETTINGS{license}) if keys %{ $SETTINGS{license} };
  4         22  
174              
175 4         20 \%obj;
176             }
177              
178             sub _security_object {
179 2   0 2   5 my @obj = map { { $_->{name} => $_->{scopes} || [] } } values %{ $SETTINGS{security} };
  0         0  
  2         8  
180 2         57 \@obj;
181             }
182              
183 2 50   2   26 sub _security_definitions_object { $SETTINGS{security} || {} }
184              
185             sub _paths_object {
186 2     2   51 my $routes = shift;
187              
188 2         3 my %obj;
189 2         8 for my $r (sort { $a->path cmp $b->path } @$routes) {
  72         302  
190 26 100       143 next if lc($r->method) eq 'options';
191              
192 20         87 my $path = $r->path;
193 20         136 $path =~ s#:([^/]+)#{$1}#msixg;
194              
195 20         45 $obj{ $path }{ lc($r->method) } = _operation_object($r);
196             }
197              
198 2         17 \%obj;
199             }
200              
201             sub _operation_object {
202 20     20   25 my $r = shift;
203              
204 20         31 my $path = $r->path;
205 20         69 $path =~ tr#/:#_#;
206 20         31 my $operation_id = lc($r->method) . $path;
207              
208             my %obj = (
209             consumes => $DEFAULTS{consumes},
210             #deprecated => 'false',
211             description => $r->desc || '',
212             #externalDocs => '',
213             operationId => $operation_id,
214             produces => $r->produces || $DEFAULTS{produces},
215             responses => {
216             default => {
217             description => 'Unexpected error',
218             #examples => '',
219             #headers => '',
220             #schema => '',
221             },
222             # Adds a response object from route's entity if it exists
223 20   50     89 %{ _response_object($r) },
  20   33     185  
      100        
224             },
225             #schemes => [],
226             #security => {}, # TODO per operation permissions
227             summary => $r->summary || '',
228             tags => $r->tags,
229             );
230              
231 20         52 my $params = _parameters_object($r->method, $r->params);
232 20 100       52 $obj{parameters} = $params if scalar @$params;
233              
234 20         68 \%obj;
235             }
236              
237             sub _response_object {
238 21     21   32 my $r = shift;
239 21 100       53 return {} unless $r->entity;
240              
241 1         6 my $name = $r->entity;
242              
243 1   50     6 my %obj = (
244             $HTTP_OK => {
245             description => $r->desc || $r->summary || '',
246             schema => {
247             '$ref' => sprintf('#/definitions/%s', _name_for_object($name)),
248             }
249             },
250             );
251              
252 1         5 \%obj;
253             }
254              
255             sub _parameters_object {
256 28     28   8714 my ($method, $pp) = @_;
257              
258 28         34 my @obj;
259 28         49 for my $p (@$pp) {
260 43         84 my ($type) = _param_type($p->type);
261              
262             # Available: query, header, path, formData or body
263 43         68 my $location = do {
264 43 100       80 if ($p->in) { $p->in }
  2 100       4  
    100          
    100          
265 14         57 elsif ($p->named) { 'path' }
266 1         6 elsif ($type eq 'object') { 'body' }
267 17         106 elsif ($method =~ /patch|post|put/i) { 'formData' }
268 9         73 else { 'query' }
269             };
270              
271 43         72 my $ptype = _param_type_object($p);
272 43 100       73 if (_type_name($p->type) =~ /^HashRef$/ ) {
273 1         9 $ptype->{schema}{'$ref'} = delete $ptype->{'$ref'};
274             }
275              
276             # If the type is an Enum, set type to string and give the enum values.
277 43 100       192 if (_type_is_enum($p->type)) {
278 1         18 $ptype->{type} = 'string';
279 1         3 $ptype->{enum} = $p->type->values;
280             }
281              
282 43 100 100     85 my %param = (
283             description => $p->desc || '',
284             in => $location,
285             name => $p->name,
286             required => $p->required ? JSON::MaybeXS::true : JSON::MaybeXS::false,
287             %$ptype,
288             );
289 43 100       608 $param{default} = $p->default if defined $p->default;
290              
291              
292 43         254 push @obj, \%param;
293             }
294              
295 28         55 \@obj;
296             }
297              
298             sub _definitions_object {
299 3     3   24 my $routes = shift;
300 3         7 my @objects;
301              
302 3         7 for my $r (@$routes) {
303 27 100       107 if ($r->entity) {
304 1         7 push @objects, $r->entity;
305             }
306              
307 27         87 my @pp = @{ $r->params };
  27         38  
308 27         103 while (my $p = pop @pp) {
309 41 100       125 next unless _type_name($p->type) =~ /^HashRef$/;
310 2 50       15 push @pp, @{ $p->enclosed || [] };
  2         6  
311 2         12 push @objects, $p;
312             }
313             }
314              
315 3         18 my %definitions = map { %{ _schema_object($_) } }
  4         7  
  4         35  
316             _collect_nested_definitions(@objects);
317 3         14 \%definitions;
318             }
319              
320             sub _collect_nested_definitions {
321 5     5   11 my @objects = @_;
322 5 100       25 return () unless scalar @objects;
323              
324 2         3 my @nested;
325 2         4 for my $obj (@objects) {
326 4 50       20 if( $obj->can('enclosed') ) {
327 4 50       5 for my $expose ( @{ $obj->enclosed || [] } ) {
  4         9  
328 9 100       25 if (exists $expose->{'using'} ){
329 1         3 push @nested, $expose->{using};
330             }
331             }
332             }
333             }
334 2         6 push @objects, _collect_nested_definitions(@nested);
335              
336 2         4 return @objects;
337             }
338              
339              
340             sub _schema_object {
341 6     6   4030 my $p = shift;
342 6 50       18 return unless _type_name($p->type) =~ /^HashRef$/;
343              
344 6         42 my (@required, %properties);
345 6 50       8 for my $pp (@{ $p->enclosed || [] }) {
  6         13  
346 14         57 $properties{ _type_name($pp) } = _param_type_object($pp);
347              
348 14 100       66 push @required, _type_name($pp) if $pp->required;
349             }
350              
351 6         24 my %object = (
352             _name_for_object($p) => {
353             type => 'object',
354             required => \@required,
355             properties => \%properties,
356             }
357             );
358 6         24 \%object;
359             }
360              
361             sub _tags_object {
362 2     2   35 my $app = shift;
363              
364 2         7 my %tags;
365 2         4 for my $r (@{ $app->routes->routes }) {
  2         8  
366 26 50       59 next unless $_;
367 0         0 $tags{ $_ }++ for @{ $r->tags };
  0         0  
368             }
369              
370 2         4 my @tags;
371 2         8 for my $t (keys %tags) {
372 0   0     0 my $tag = {
373             name => $t,
374             description => $app->resource_desc($t) || '',
375             #externalDocs => {
376             # description => '',
377             # url => '', #R
378             #},
379             };
380 0         0 push @tags, $tag;
381             }
382              
383 2         5 \@tags;
384             }
385              
386             # get the name of a type
387             sub _type_name {
388 342     342   831 my $type = shift;
389 342 50 33     637 if ($type && $type->can('display_name')) {
    0 0        
390 342         3117 return $type->display_name;
391             }
392             elsif ($type && $type->can('name')) {
393             # fall back to name() (e.g. Moose types do not have display_name)
394 0         0 return $type->name;
395             }
396             else {
397 0         0 return "$type";
398             }
399             }
400              
401             sub _param_type_object {
402 57     57   69 my $p = shift;
403 57         64 my %item;
404              
405 57         114 my $tt = $p->type;
406              
407              
408 57 100       174 if (_type_name($tt) =~ /^Maybe\[/) {
409 2         12 $item{nullable} = JSON::MaybeXS::true;
410 2         9 $tt = $tt->type_parameter;
411             }
412              
413 57 100       266 if (_type_name($tt) =~ /^HashRef$/ ) {
    50          
    100          
414 3 50       20 $item{'$ref'} = sprintf('#/definitions/%s', _name_for_object($p->can('using')?$p->using:$p));
415             }
416             elsif (_type_name($tt) =~ /^HashRef\[.*\]$/) {
417 0         0 $item{'type'} = 'object';
418 0         0 $item{'additionalProperties'} = {
419             '$ref' => sprintf('#/definitions/%s', _name_for_object($p->using))
420             };
421             }
422             elsif (_type_name($tt) =~ /^ArrayRef/) {
423 3         18 $item{type} = 'array';
424              
425 3         5 my $type;
426             my $format;
427              
428             # Loop down to find type beneath coercion.
429 3         8 while (!defined $type) {
430 4 50       10 if($tt->can('type_parameter')) {
431 4         35 ($type, $format) = _param_type($tt->type_parameter);
432             }
433             else {
434 0         0 ($type, $format) = ('object', '' );
435             }
436 4 100       13 $tt = $tt->parent if !defined $type;
437             }
438              
439 3 100       6 if ($type eq 'object') {
440 2         5 $item{items} = {}; # {} is the "any-type" schema.
441 2 100 66     17 if ($p->can('using') && $p->using) {
    50          
442 1         15 $item{items}{'$ref'} = sprintf('#/definitions/%s', _name_for_object($p->using));
443             }
444             elsif ($tt->can("type_parameter")) {
445 1         11 my ($subscript_type, $subscript_format) = _param_type($tt->type_parameter);
446 1 50       4 if (defined $subscript_type) {
447 1         2 $item{items}->{type} = $subscript_type;
448 1 50       18 $item{items}->{format} = $subscript_format if defined $subscript_format;
449             }
450             }
451             }
452             else {
453 1         3 $item{items}->{type} = $type;
454 1 50       12 $item{items}->{format} = $format if $format;
455 1 50       4 $item{description} = $p->desc if $p->desc;
456             }
457             }
458             else {
459 51         231 my ($type, $format) = _param_type($tt);
460 51         117 $item{type} = $type;
461 51 100       86 $item{format} = $format if $format;
462 51 100       125 $item{description} = $p->desc if $p->desc;
463             }
464 57         318 \%item;
465             }
466              
467             sub _param_type {
468 99     99   220 my $t = shift;
469 99 100 66     240 if ($t && $t->can('name')) { # allow nested types as Str in ArrayRef[Str]
470 98 100       1054 if ($t->name =~ /int/i) { 'integer', 'int32' }
  34 50       188  
    50          
    50          
    100          
    50          
    50          
    50          
    50          
    50          
    100          
471 0         0 elsif ($t->name =~ /long/i) { 'integer', 'int64' }
472 0         0 elsif ($t->name =~ /num|float|real/i) { 'number', 'float' }
473 0         0 elsif ($t->name =~ /double/i) { 'number', 'double' }
474 54         814 elsif ($t->name =~ /str/i) { 'string', undef }
475 0         0 elsif ($t->name =~ /byte/i) { 'string', 'byte' }
476 0         0 elsif ($t->name =~ /bool/i) { 'boolean', undef }
477 0         0 elsif ($t->name =~ /datetime/i) { 'string', 'date-time' }
478 0         0 elsif ($t->name =~ /date/i) { 'string', 'date' }
479 0         0 elsif ($t->name =~ /password/i) { 'string', 'password' }
480 4         111 elsif ($t->name =~ /hashref/i) { 'object', undef }
481             else {
482 6 100       176 if (_type_name($t) =~ /ArrayRef/) { 'array', undef }
  2         14  
483 4         28 else { 'object', undef } # fallback
484             }
485             }
486             else {
487 1         2 { $t, undef }
  1         3  
488             }
489             }
490              
491             sub _name_for_object {
492 12     12   34 my $obj = shift;
493              
494 12         15 local $Data::Dumper::Deparse = 1;
495 12         68 local $Data::Dumper::Indent = 0;
496 12         16 local $Data::Dumper::Maxdepth = 2;
497 12         42 local $Data::Dumper::Purity = 0;
498 12         16 local $Data::Dumper::Sortkeys = 1;
499 12         14 local $Data::Dumper::Terse = 1;
500              
501 12         46 my $fingerprint = md5_hex(Data::Dumper->Dump([$obj], [qw/obj/]));
502 12         1217 my $objname = ucfirst($obj->name);
503             #--- $ref values must be RFC3986 compliant URIs ---
504 12         55 $objname =~ s/::/-/g;
505 12         86 sprintf '%s-%s', $objname, uc(substr($fingerprint, 0, 10));
506             }
507              
508             sub _type_is_enum {
509 43     43   129 my $type = shift;
510              
511 43 100 66     81 return 1 if $type->isa('Moose::Meta::TypeConstraint::Enum')
512             or $type->isa('Type::Tiny::Enum');
513              
514 42         691 return 0;
515             }
516              
517             1;
518              
519             __END__