File Coverage

blib/lib/Mojolicious/Plugin/Swagger2.pm
Criterion Covered Total %
statement 204 210 97.1
branch 78 90 86.6
condition 36 56 64.2
subroutine 26 27 96.3
pod 3 3 100.0
total 347 386 89.9


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Swagger2;
2 28     28   35691 use Mojo::Base 'Mojolicious::Plugin';
  28         33  
  28         138  
3 28     28   4195 use Mojo::JSON;
  28         32  
  28         903  
4 28     28   94 use Mojo::Loader;
  28         24  
  28         829  
5 28     28   85 use Mojo::Util qw(decamelize deprecated);
  28         33  
  28         1197  
6 28     28   8477 use Swagger2;
  28         51  
  28         147  
7 28     28   12231 use JSON::Validator::OpenAPI::Mojolicious;
  28         13546  
  28         1471  
8 28     28   130 use constant DEBUG => $ENV{SWAGGER2_DEBUG};
  28         34  
  28         1787  
9 28     28   106 use constant IO_LOGGING => $ENV{SWAGGER2_IO_LOGGING}; # EXPERIMENTAL
  28         30  
  28         69449  
10              
11             deprecated "Mojolicious::Plugin::Swagger2 is deprecated in favor of Mojolicious::Plugin::OpenAPI";
12              
13             my $SKIP_OP_RE = qr(By|From|For|In|Of|To|With);
14             my $LAYOUT = Mojo::Loader::data_section(__PACKAGE__, 'layouts/mojolicious_plugin_swagger.html.ep');
15              
16             has url => '';
17             has _validator => sub { JSON::Validator::OpenAPI::Mojolicious->new; };
18              
19             sub dispatch_to_swagger {
20 5 50 33 5 1 24852 return undef unless $_[1]->{op} and $_[1]->{id} and ref $_[1]->{params} eq 'HASH';
      33        
21              
22 5         6 my ($c, $data) = @_;
23 5         10 my $self = $c->stash('swagger.plugin');
24             my $reply
25 5   50 5   42 = sub { $_[0]->send({json => {code => $_[2] || 200, id => $data->{id}, body => $_[1]}}) };
  5         30  
26             my $defaults = $self->{route_defaults}{$data->{op}}
27 5 100       19 or return $c->$reply(_error('Unknown operationId.'), 400);
28 4         5 my ($e, $input, @errors);
29              
30 4 50       20 return $c->$reply(_error($e), 501) if $e = _find_action($c, $defaults);
31              
32 4 100       2 for my $p (@{$defaults->{swagger_operation_spec}{parameters} || []}) {
  4         17  
33 2         3 my $name = $p->{name};
34 2   33     7 my $value = $data->{params}{$name} // $p->{default};
35 2         5 my @e = $self->_validator->_validate_request_value($p, $name => $value);
36 2 100       625 $input->{$name} = $value unless @e;
37 2         4 push @errors, @e;
38             }
39              
40 4 100       11 return $c->$reply({errors => \@errors}, 400) if @errors;
41             return Mojo::IOLoop->delay(
42             sub {
43 3     3   581 my $delay = shift;
44 3         6 my $action = $defaults->{action};
45 3         11 my $sc = $delay->data->{sc} = $defaults->{controller}->new(%$c);
46 3         42 $sc->stash(swagger_operation_spec => $defaults->{swagger_operation_spec});
47 3         33 $sc->$action($input, $delay->begin);
48             },
49             sub {
50 3     3   251 my $delay = shift;
51 3         3 my $data = shift;
52 3   50     8 my $status = shift || 200;
53             my @errors = $self->_validator->validate_response($c, $defaults->{swagger_operation_spec},
54 3         8 $status, $data);
55              
56 3 100       799 return $c->$reply($data, $status) unless @errors;
57 1         1 warn "[Swagger2] Invalid response: @errors\n" if DEBUG;
58 1         16 $c->$reply({errors => \@errors}, 500);
59             },
60 3         20 );
61             }
62              
63             sub render_swagger {
64 114     114 1 2843 my ($c, $err, $data, $status) = @_;
65              
66 114 100       399 return $c->render(json => $err, status => $status) if %$err;
67 79 100       404 return $c->render(ref $data ? (json => $data) : (text => $data), status => $status);
68             }
69              
70             sub register {
71 40     40 1 64414 my ($self, $app, $config) = @_;
72 40         46 my ($base_path, $paths, $r, $swagger);
73              
74 40   33     321 $swagger = $config->{swagger} || Swagger2->new->load($config->{url} || '"url" is missing');
75 40         126 $swagger = $swagger->expand;
76 40   50     396 $paths = $swagger->api_spec->get('/paths') || {};
77              
78 40 50       1084 $app->plugin(PODRenderer => {no_perldoc => 1}) unless $app->renderer->helpers->{pod_to_html};
79              
80 40 50 50     400722 if ($config->{validate} // 1) {
81 40         168 my @errors = $swagger->validate;
82 40 50       7883636 die join "\n", "Swagger2: Invalid spec:", @errors if @errors;
83             }
84              
85 40 50       270 if ($app->plugins->has_subscribers('swagger_route_added')) {
86 0         0 warn
87             "swagger_route_added hook will be deprecated. https://github.com/jhthorsen/swagger2/issues/65";
88             }
89             else {
90 40         1139 $app->hook(swagger_route_added => \&_on_route_added);
91             }
92              
93 40   33     746 local $config->{coerce} = $config->{coerce} || $ENV{SWAGGER_COERCE_VALUES};
94 40 50       140 $self->_validator->coerce($config->{coerce}) if $config->{coerce};
95 40         227 $self->_validator->schema($swagger->api_spec->data);
96 40         29094 $self->url($swagger->url);
97 40 50       316 $app->helper(dispatch_to_swagger => \&dispatch_to_swagger)
98             unless $app->renderer->get_helper('dispatch_to_swagger');
99 40 50       6697 $app->helper(render_swagger => \&render_swagger)
100             unless $app->renderer->get_helper('render_swagger');
101              
102 40         4622 $r = $config->{route};
103              
104 40 50 66     166 if ($r and !$r->pattern->unparsed) {
105 0         0 $r->to(swagger => $swagger);
106 0         0 $r = $r->any($swagger->base_url->path->to_string);
107             }
108 40 100       151 if (!$r) {
109 39         185 $r = $app->routes->any($swagger->base_url->path->to_string);
110 39         10027 $r->to(swagger => $swagger);
111             }
112 40 100       809 if (my $ws = $config->{ws}) {
113 1         2 $ws->to('swagger.plugin' => $self);
114             }
115              
116 40         161 $base_path = $swagger->api_spec->data->{basePath} = $r->to_string;
117 40         1307 $base_path =~ s!/$!!;
118              
119 40         201 for my $path (sort { length $a <=> length $b } keys %$paths) {
  37         124  
120             my $around_action
121 72   100     482 = $paths->{$path}{'x-mojo-around-action'} || $swagger->api_spec->get('/x-mojo-around-action');
122             my $controller
123 72   100     1612 = $paths->{$path}{'x-mojo-controller'} || $swagger->api_spec->get('/x-mojo-controller');
124              
125 72         1053 for my $http_method (grep { !/^x-/ } keys %{$paths->{$path}}) {
  88         279  
  72         196  
126 80         132 my $op_spec = $paths->{$path}{$http_method};
127 80         114 my $route_path = $path;
128 80 100       96 my %parameters = map { ($_->{name}, $_) } @{$op_spec->{parameters} || []};
  61         217  
  80         303  
129              
130 80         281 $route_path =~ s/{([^}]+)}/{
131 24         37 my $name = $1;
  24         72  
132 24   100     128 my $type = $parameters{$name}{'x-mojo-placeholder'} || ':';
133 24         122 "($type$name)";
134             }/ge;
135              
136             $op_spec->{'x-mojo-around-action'} = $around_action
137 80 100 100     379 if !$op_spec->{'x-mojo-around-action'} and $around_action;
138             $op_spec->{'x-mojo-controller'} = $controller
139 80 100 100     228 if !$op_spec->{'x-mojo-controller'} and $controller;
140 80         220 $app->plugins->emit(swagger_route_added =>
141             $r->$http_method($route_path => $self->_generate_request_handler($op_spec)));
142 80         751 warn "[Swagger2] Add route $http_method $base_path$route_path\n" if DEBUG;
143             }
144             }
145              
146 40 50 50     300 if (my $spec_path = $config->{spec_path} // '/') {
147 40         212 my $title = $swagger->api_spec->get('/info/title');
148 40         975 $title =~ s!\W!_!g;
149 40     4   128 $r->get($spec_path)->to(cb => sub { _render_spec(shift, $swagger) })->name(lc $title);
  4         59878  
150             }
151 40 100       4145 if ($config->{ensure_swagger_response}) {
152 2         12 $self->_ensure_swagger_response($app, $config->{ensure_swagger_response}, $swagger);
153             }
154             }
155              
156             sub _ensure_swagger_response {
157 2     2   4 my ($self, $app, $responses, $swagger) = @_;
158 2         5 my $base_path = $swagger->api_spec->data->{basePath};
159              
160 2   50     21 $responses->{exception} ||= 'Internal server error.';
161 2   50     9 $responses->{not_found} ||= 'Not found.';
162 2         27 $base_path = qr{^$base_path};
163              
164             $app->hook(
165             before_render => sub {
166 16     16   39898 my ($c, $args) = @_;
167              
168 16 100       43 return unless my $template = $args->{template};
169 8 100       22 return unless my $msg = $responses->{$template};
170 3 100       10 return unless $c->req->url->path->to_string =~ $base_path;
171              
172 2         84 $args->{json} = _error($msg);
173             }
174 2         15 );
175             }
176              
177             sub _generate_request_handler {
178 80     80   404 my ($self, $op_spec) = @_;
179 80         177 my $defaults = {swagger_operation_spec => $op_spec};
180              
181             my $handler = sub {
182 115     115   857161 my $c = shift;
183 115         145 my ($e, @errors, %input);
184              
185 115 100       275 return $c->render_swagger(_error($e), {}, 501) if $e = _find_action($c, $defaults);
186 113         489 $c = $defaults->{controller}->new(%$c);
187 113         970 @errors = $self->_validator->validate_request($c, $op_spec, \%input);
188              
189 113         34437 _io_error($c, Input => \@errors) if IO_LOGGING and @errors;
190 113 100       425 return $c->render_swagger({errors => \@errors}, {}, 400) if @errors;
191             return $c->delay(
192             sub {
193 94         28362 my $action = $defaults->{action};
194 94         301 $c->app->log->debug(
195             qq(Swagger2 routing to controller "$defaults->{controller}" and action "$action"));
196 94         2200 $c->$action(\%input, shift->begin);
197             },
198             sub {
199 93         8565 my $delay = shift;
200 93         131 my $data = shift;
201 93   100     250 my $status = shift || 200;
202 93         322 my @errors = $self->_validator->validate_response($c, $op_spec, $status, $data);
203              
204 93 100       10590 return $c->render_swagger({}, $data, $status) unless @errors;
205 14         18 _io_error($c, Output => \@errors) if IO_LOGGING and @errors;
206 14         96 $c->render_swagger({errors => \@errors}, $data, 500);
207             },
208 94         1085 );
209 80         497 };
210              
211 80 100       90 for my $p (@{$op_spec->{parameters} || []}) {
  80         311  
212 61 100 100     293 $defaults->{$p->{name}} = $p->{default} if $p->{in} eq 'path' and defined $p->{default};
213             }
214              
215 80 100       194 if (my $around_action = $op_spec->{'x-mojo-around-action'}) {
216 3         5 my $next = $handler;
217             $handler = sub {
218 6     6   41967 my $c = shift;
219 6   33     39 my $around = $c->can($around_action) || $around_action;
220 6         18 $around->($next, $c, $op_spec);
221 3         12 };
222             }
223              
224 80         241 $self->{route_defaults}{$op_spec->{operationId}} = $defaults;
225 80         345 return $defaults, $handler;
226             }
227              
228             sub _on_route_added {
229 80     80   14224 my ($self, $r) = @_;
230 80         182 my $op_spec = $r->pattern->defaults->{swagger_operation_spec};
231 80         407 my $controller = $op_spec->{'x-mojo-controller'};
232 80         88 my $route_name;
233              
234             $route_name = $controller
235 152         497 ? decamelize join '::', map { ucfirst $_ } $controller, $op_spec->{operationId}
236 80 100       267 : decamelize $op_spec->{operationId};
237              
238 80         2273 $route_name =~ s/\W+/_/g;
239 80         254 $r->name($route_name);
240             }
241              
242             sub _render_spec {
243 4     4   6 my ($c, $swagger) = @_;
244 4   100     11 my $format = $c->stash('format') || 'json';
245 4         47 my $spec = $swagger->api_spec->data;
246 4         27 my $url = $c->req->url->to_abs;
247              
248 4         516 local $spec->{id};
249 4         6 delete $spec->{id};
250 4         10 local $spec->{host} = $url->host_port;
251 4         69 $swagger->base_url->host($url->host)->port($url->port);
252              
253 4 100       60 if ($format eq 'text') {
    100          
254 1         4 $c->render(text => $swagger->pod->to_string);
255             }
256             elsif ($format eq 'html') {
257 1         7 $c->render(
258             handler => 'ep',
259             inline => $LAYOUT,
260             pod => $c->pod_to_html($swagger->pod->to_string)
261             );
262             }
263             else {
264 2         7 $c->render(json => $spec);
265             }
266             }
267              
268             sub _error {
269 5     5   38 return {errors => [{message => $_[0], path => '/'}]};
270             }
271              
272             sub _find_action {
273 130 100   130   9609 return if $_[1]->{controller}; # cached
274 76         98 my ($c, $defaults) = @_;
275 76 50       269 my $op = $defaults->{swagger_operation_spec}{operationId} or return 'operationId is missing.';
276             my $can = sub {
277             $defaults->{controller}->can($defaults->{action})
278 75 100   75   678 ? ''
279             : qq(Method "$defaults->{action}" not implemented.);
280 76         257 };
281              
282             # specify controller manually
283             @$defaults{qw(action controller)}
284 76         233 = _load($c, $op, $defaults->{swagger_operation_spec}{'x-mojo-controller'});
285 76 100       269 return $can->() if $defaults->{controller};
286              
287             # "createFileInFileSystem" = ("createFile", "FileSystem")
288 16         118 @$defaults{qw(action controller)} = _load($c, split $SKIP_OP_RE, $op);
289 16 100       43 return $can->() if $defaults->{controller};
290              
291             # "showPetById" = "showPet"
292 6         143 $op =~ s!$SKIP_OP_RE.*$!!;
293              
294             # "show_fooPet" = ("show_foo", "Pet")
295 6         32 @$defaults{qw(action controller)} = _load($c, $op =~ /^([a-z_]+)([A-Z]\w+)$/);
296 6 100       25 return $can->() if $defaults->{controller};
297              
298             return
299 1         13 qq(Controller from operationId "$defaults->{swagger_operation_spec}{operationId}" could not be loaded.);
300             }
301              
302             sub _io_error {
303 0     0   0 my $c = shift;
304 0         0 my $level = IO_LOGGING;
305 0         0 $c->app->log->$level(sprintf '%s error: %s', shift, Mojo::JSON::encode_json(shift));
306             }
307              
308             sub _load {
309 98     98   147 my ($c, $action, $controller) = @_;
310 98         89 my (@classes, %uniq);
311              
312 98 100       202 return unless $controller;
313 80         288 $action = decamelize ucfirst $action;
314              
315 80 100       1651 if ($controller =~ /::/) {
316 61         121 push @classes, $controller;
317             }
318             else {
319 19         60 my $singular = $controller;
320 19         32 $singular =~ s!s$!!; # "showPets" = "showPet"
321             push @classes,
322 36         81 grep { !$uniq{$_}++ }
323 19         18 map { ("${_}::$controller", "${_}::$singular") } @{$c->app->routes->namespaces};
  18         188  
  19         45  
324             }
325              
326 80         230 while ($controller = shift @classes) {
327 82         305 my $e = Mojo::Loader::load_class($controller);
328 82         15121 warn
329             qq([Swagger2] Load "$controller": @{[ref $e ? $e : $e ? "Can't locate class" : "Success"]}.\n)
330             if DEBUG;
331 82 100       693 return ($action, $controller) if $controller->can('new');
332             }
333              
334 5         16 return;
335             }
336              
337             1;
338              
339             =encoding utf8
340              
341             =head1 NAME
342              
343             Mojolicious::Plugin::Swagger2 - Mojolicious plugin for Swagger2
344              
345             =head1 DEPRECATION WARNING
346              
347             See L.
348              
349             =head1 SYNOPSIS
350              
351             package MyApp;
352             use Mojo::Base "Mojolicious";
353              
354             sub startup {
355             my $app = shift;
356             $app->plugin(Swagger2 => {url => "data://MyApp/petstore.json"});
357             }
358              
359             __DATA__
360             @@ petstore.json
361             {
362             "swagger": "2.0",
363             "info": {...},
364             "host": "petstore.swagger.wordnik.com",
365             "basePath": "/api",
366             "paths": {
367             "/pets": {
368             "get": {...}
369             }
370             }
371             }
372              
373             =head1 DESCRIPTION
374              
375             L is L that add routes and
376             input/output validation to your L application.
377              
378             =head1 HOOKS
379              
380             =head2 swagger_route_added
381              
382             This hook will be DEPRECATED! See L.
383              
384             =head1 STASH VARIABLES
385              
386             =head2 swagger
387              
388             The L object used to generate the routes is available
389             as C from L. Example code:
390              
391             sub documentation {
392             my ($c, $args, $cb);
393             $c->$cb($c->stash('swagger')->pod->to_string, 200);
394             }
395              
396             =head2 swagger_operation_spec
397              
398             The Swagger specification for the current
399             L
400             is stored in the "swagger_operation_spec" stash variable.
401              
402             sub list_pets {
403             my ($c, $args, $cb);
404             $c->app->log->info($c->stash("swagger_operation_spec")->{operationId});
405             ...
406             }
407              
408             =head1 ATTRIBUTES
409              
410             =head2 url
411              
412             Holds the URL to the swagger specification file.
413              
414             =head1 HELPERS
415              
416             =head2 dispatch_to_swagger
417              
418             $bool = $c->dispatch_to_swagger(\%data);
419              
420             This helper can be used to handle WebSocket requests with swagger.
421              
422             This helper is EXPERIMENTAL.
423              
424             =head2 render_swagger
425              
426             $c->render_swagger(\%err, \%data, $status);
427              
428             This method is used to render C<%data> from the controller method. The C<%err>
429             hash will be empty on success, but can contain input/output validation errors.
430             C<$status> is used to set a proper HTTP status code such as 200, 400 or 500.
431              
432             =head1 METHODS
433              
434             =head2 register
435              
436             $self->register($app, \%config);
437              
438             This method is called when this plugin is registered in the L
439             application.
440              
441             C<%config> can contain these parameters:
442              
443             =over 4
444              
445             =item * coerce
446              
447             This argument will be passed on to L.
448              
449             =item * ensure_swagger_response
450              
451             $app->plugin(swagger2 => {
452             ensure_swagger_response => {
453             exception => "Internal server error.",
454             not_found => "Not found.",
455             }
456             });
457              
458             Adds a L hook which will make sure
459             "exception" and "not_found" responses are in JSON format. There is no need to
460             specify "exception" and "not_found" if you are happy with the defaults.
461              
462             =item * route
463              
464             Need to hold a Mojolicious route object. See L for an example.
465              
466             This parameter is optional.
467              
468             =item * spec_path
469              
470             Holds the location for where the specifiation can be served from. The default
471             value is "/", relative to "basePath" in the specification. Can be disabled
472             by setting this value to empty string.
473              
474             =item * validate
475              
476             A boolean value (default is true) that will cause your schema to be validated.
477             This plugin will abort loading if the schema include errors
478              
479             =item * swagger
480              
481             A C object. This can be useful if you want to keep use the
482             specification to other things in your application. Example:
483              
484             use Swagger2;
485             my $swagger = Swagger2->new->load($url);
486             plugin Swagger2 => {swagger => $swagger2};
487             app->defaults(swagger_spec => $swagger->api_spec);
488              
489             Either this parameter or C need to be present.
490              
491             =item * url
492              
493             This will be used to construct a new L object. The C
494             can be anything that L can handle.
495              
496             Either this parameter or C need to be present.
497              
498             =back
499              
500             =head1 COPYRIGHT AND LICENSE
501              
502             Copyright (C) 2014-2015, Jan Henning Thorsen
503              
504             This program is free software, you can redistribute it and/or modify it under
505             the terms of the Artistic License version 2.0.
506              
507             =head1 AUTHOR
508              
509             Jan Henning Thorsen - C
510              
511             =cut
512              
513             __DATA__