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