File Coverage

blib/lib/Dancer2/Plugin/Swagger2.pm
Criterion Covered Total %
statement 123 149 82.5
branch 56 102 54.9
condition 18 38 47.3
subroutine 13 14 92.8
pod 0 1 0.0
total 210 304 69.0


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::Swagger2;
2              
3 4     4   800989 use strict;
  4         5  
  4         104  
4 4     4   15 use warnings;
  4         6  
  4         194  
5              
6             # ABSTRACT: A Dancer2 plugin for creating routes from a Swagger2 spec
7             our $VERSION = '0.003_002'; # TRIAL VERSION
8              
9 4     4   2113 use Dancer2::Plugin;
  4         126698  
  4         29  
10 4     4   31390 use Module::Runtime 'use_module';
  4         7  
  4         23  
11 4     4   1919 use Swagger2;
  4         267423  
  4         28  
12 4     4   1806 use Swagger2::SchemaValidator;
  4         738  
  4         5052  
13              
14              
15 14     14 0 44 sub DEBUG { !!$ENV{SWAGGER2_DEBUG} }
16              
17              
18             register swagger2 => sub {
19 2     2   12495 my ( $dsl, %args ) = @_;
20 2         8 my $conf = plugin_setting;
21              
22             ### get arguments/config values/defaults ###
23              
24             my $controller_factory =
25 2   50     914 $args{controller_factory} || \&_default_controller_factory;
26 2 50       7 my $url = $args{url} or die "argument 'url' missing";
27             my $create_options_route =
28             exists $args{create_options_route} ? !!$args{create_options_route}
29             : exists $conf->{create_options_route} ? !!$conf->{create_options_route}
30 2 50       9 : '';
    50          
31             my $validate_spec =
32             exists $args{validate_spec} ? !!$args{validate_spec}
33             : exists $conf->{validate_spec} ? !!$conf->{validate_spec}
34 2 50       8 : 1;
    50          
35             my $validate_requests =
36             exists $args{validate_requests} ? !!$args{validate_requests}
37             : exists $conf->{validate_requests} ? !!$conf->{validate_requests}
38 2 50       6 : $validate_spec;
    50          
39             my $validate_responses =
40             exists $args{validate_responses} ? !!$args{validate_responses}
41             : exists $conf->{validate_responses} ? !!$conf->{validate_responses}
42 2 50       10 : $validate_spec;
    50          
43              
44             # parse Swagger2 file
45 2         19 my $spec = Swagger2->new($url)->expand;
46              
47 2 0 33     9568 if ( $validate_spec or $validate_requests or $validate_responses ) {
      33        
48 2 50       9 if ( my @errors = $spec->validate ) {
49 0 0       0 if ($validate_spec) {
50 0         0 die join "\n" => "Swagger2: Invalid spec:", @errors;
51             }
52             else {
53 0         0 warn "Spec contains errors but"
54             . " request/response validation is enabled!";
55             }
56             }
57             }
58              
59 2         371311 my $basePath = $spec->api_spec->get('/basePath');
60 2         54 my $paths = $spec->api_spec->get('/paths'); # TODO might be undef?
61              
62 2         36 while ( my ( $path => $path_spec ) = each %$paths ) {
63 4         7053 my $dancer2_path = $path;
64              
65 4 100       12 $basePath and $dancer2_path = $basePath . $dancer2_path;
66              
67             # adapt Swagger2 syntax for URL path arguments to Dancer2 syntax
68             # '/path/{argument}' -> '/path/:argument'
69 4         5 $dancer2_path =~ s/\{([^{}]+?)\}/:$1/g;
70              
71 4         13 my @http_methods = sort keys %$path_spec;
72              
73 4 50       11 if ($create_options_route) {
74 0         0 my $allow_methods = join ', ' => 'OPTIONS', map uc, @http_methods;
75             $dsl->options(
76             $dancer2_path => sub {
77 0     0   0 $dsl->headers(
78             Allow => $allow_methods, # RFC 2616 HTTP/1.1
79             'Access-Control-Allow-Methods' => $allow_methods, # CORS
80             'Access-Control-Max-Age' => 60 * 60 * 24,
81             );
82             },
83 0         0 );
84             }
85              
86 4         6 for my $http_method (@http_methods) {
87 4         6 my $method_spec = $path_spec->{ $http_method };
88 4 50       10 my $coderef = $controller_factory->(
89             $method_spec, $http_method, $path, $dsl, $conf, \%args
90             ) or next;
91              
92 4 50       7 DEBUG and warn "Add route $http_method $dancer2_path";
93              
94 4         6 my $params = $method_spec->{parameters};
95              
96             # Dancer2 DSL keyword is different from HTTP method
97 4 50       7 $http_method eq 'delete' and $http_method = 'del';
98              
99             $dsl->$http_method(
100             $dancer2_path => sub {
101 7     7   171687 my @args = @_;
102              
103 7 50       24 if ($validate_requests) {
104 7         35 my @errors =
105             _validate_request( $method_spec, $dsl->app->request );
106              
107 7 100       21 if (@errors) {
108 1 50       3 DEBUG and warn "Invalid request: @errors\n";
109 1         5 $dsl->status(400);
110 1         90 return { errors => [ map { "$_" } @errors ] };
  1         4  
111             }
112             }
113              
114 6         18 my $result = $coderef->(@args);
115              
116 5 50       20 if ($validate_responses) {
117 5         19 my @errors =
118             _validate_response( $method_spec, $dsl->response,
119             $result );
120              
121 5 100       13 if (@errors) {
122 1 50       3 DEBUG and warn "Invalid response: @errors\n";
123 1         4 $dsl->status(500);
124              
125             # TODO hide details of server-side errors?
126 1         80 return { errors => [ map { "$_" } @errors ] };
  1         3  
127             }
128             }
129              
130 4         12 return $result;
131             }
132 4         32 );
133             }
134             }
135             };
136              
137             register_plugin;
138              
139             sub _validate_request {
140 11     11   3482 my ( $method_spec, $request ) = @_;
141              
142 11         13 my @errors;
143              
144 11         12 for my $parameter_spec ( @{ $method_spec->{parameters} } ) {
  11         29  
145 8         12 my $in = $parameter_spec->{in};
146 8         11 my $name = $parameter_spec->{name};
147 8         7 my $required = $parameter_spec->{required};
148              
149 8 50       15 if ( $in eq 'body' ) { # complex data structure in HTTP body
150 0         0 my $input = $request->data;
151 0         0 my $schema = $parameter_spec->{schema};
152              
153 0         0 push @errors, _validator()->validate_input( $input, $schema );
154             }
155             else { # simple key-value-pair in HTTP header/query/path/form
156 8         7 my $type = $parameter_spec->{type};
157 8         9 my @values;
158              
159 8 50       21 if ( $in eq 'header' ) {
    50          
    0          
    0          
160 0         0 @values = $request->header($name);
161             }
162             elsif ( $in eq 'query' ) {
163 8         27 @values = $request->query_parameters->get_all($name);
164             }
165             elsif ( $in eq 'path' ) {
166 0         0 @values = $request->route_parameters->get_all($name);
167             }
168             elsif ( $in eq 'formData' ) {
169 0         0 @values = $request->body_parameters->get_all($name);
170             }
171 0         0 else { die "Unknown value for property 'in' of parameter '$name'" }
172              
173             # TODO align error messages to output style of SchemaValidator
174 8 100 100     540 if ( @values == 0 and $required ) {
    100          
175 1 50       4 $required and push @errors, "No value for parameter '$name'";
176 1         2 next;
177             }
178             elsif ( @values > 1 ) {
179 1         3 push @errors, "Multiple values for parameter '$name'";
180 1         2 next;
181             }
182              
183              
184 6         8 my $value = $values[0];
185              
186             # TODO steal more from Mojolicious::Plugin::Swagger2 ;-)
187 6 100 66     35 if ($type and defined ($value //= $parameter_spec->{default})) {
      100        
188 3 50 33     22 if (($type eq 'integer' or $type eq 'number') and $value =~ /^-?\d/) {
    50 33        
189 0         0 $value += 0;
190             }
191             elsif ($type eq 'boolean') {
192 0 0 0     0 $value = (!$value or $value eq 'false') ? '' : 1;
193             }
194             }
195              
196 6 100       16 my %input = defined $value ? ( $name => $value ) : ();
197 6         12 my %schema = ( properties => { $name => $parameter_spec } );
198              
199 6 50       15 $required and $schema{required} = [$name];
200              
201 6         11 push @errors, _validator()->validate_input( \%input, \%schema );
202             }
203             }
204              
205 11         876 return @errors;
206             }
207              
208             sub _validate_response {
209 8     8   1182 my ( $method_spec, $response, $result ) = @_;
210              
211 8         12 my $responses = $method_spec->{responses};
212 8         75 my $status = $response->status;
213              
214 8         1625 my @errors;
215              
216 8 50 33     32 if ( my $response_spec = $responses->{$status} || $responses->{default} ) {
217              
218 8         8 my $headers = $response_spec->{headers};
219              
220 8         29 while ( my ( $name => $header_spec ) = each %$headers ) {
221 2         9 my @values = $response->header($name);
222              
223 2 50       65 if ( $header_spec->{type} eq 'array' ) {
224 0         0 push @errors,
225             _validator()->validate_input( \@values, $header_spec );
226             }
227             else {
228 2 50       8 if ( @values == 0 ) {
    50          
229 0         0 next; # you can't make a header 'required' in Swagger2
230             }
231             elsif ( @values > 1 ) {
232              
233             # TODO align error message to output style of SchemaValidator
234 0         0 push @errors, "header '$name' has multiple values";
235 0         0 next;
236             }
237              
238 2         4 push @errors,
239             _validator()->validate_input( $values[0], $header_spec );
240             }
241             }
242              
243 8 100       208 if ( my $schema = $response_spec->{schema} ) {
244 5         11 push @errors, _validator()->validate_input( $result, $schema );
245             }
246             }
247             else {
248             # TODO Call validate_input($response, {}) like
249             # in Mojolicious::Plugin::Swagger2?
250             # Swagger2-0.71/lib/Mojolicious/Plugin/Swagger2.pm line L315
251             }
252              
253 8         545 return @errors;
254             }
255              
256              
257             sub _default_controller_factory {
258             # TODO simplify argument list
259 4     4   9 my ( $method_spec, $http_method, $path, $dsl, $conf, $args, ) = @_;
260              
261             # from Dancer2 app
262 4   33     13 my $namespace = $args->{controller} || $conf->{controller};
263 4         28 my $app = $dsl->app->name;
264              
265             # from Swagger2 file
266 4         4 my $module;
267 4         6 my $method = $method_spec->{operationId};
268 4 100       13 if ( $method =~ s/^(.+)::// ) { # looks like Perl module
269 2         6 $module = $1;
270             }
271              
272             # different candidates possibly reflecting operationId
273 4         4 my @controller_candidates = do {
274 4 50       7 if ($namespace) {
275 0 0       0 if ($module) { $namespace . '::' . $module, $module }
  0         0  
276 0         0 else { $namespace }
277             }
278             else {
279 4 100       8 if ($module) {
280             ( # parens for better layout by Perl::Tidy
281 2         8 $app . '::' . $module,
282             $app . '::Controller::' . $module,
283             $module, # maybe a top level module name?
284             );
285             }
286 2         5 else { $app, $app . '::Controller' }
287             }
288             };
289              
290             # check candidates
291 4         6 for my $controller (@controller_candidates) {
292 6         7 local $@;
293 6 50       6 if ( ! eval { use_module( $controller ); 1; } ) {
  6         36  
  0         0  
294 6 50 33     1386 if ( $@ && $@ =~ m/^Can't locate / ) { # module doesn't exist
295 6 50       13 DEBUG and warn "Can't load '$controller'";
296              
297             # don't do `next` here because controller could be
298             # defined in other package ...
299             }
300             else { # module doesn't compile
301 0         0 die $@;
302             }
303             }
304              
305 6 100       68 if ( my $cb = $controller->can($method) ) {
306 4         16 return $cb; # confirmed candidate
307             }
308             else {
309 2 50       3 DEBUG and warn "Controller '$controller' can't '$method'";
310             }
311             }
312              
313             # none found
314 0         0 warn "Can't find any handler for operationId '$method_spec->{operationId}'";
315 0         0 return;
316             }
317              
318             my $validator;
319 13   66 13   100 sub _validator { $validator ||= Swagger2::SchemaValidator->new }
320              
321              
322             1;
323              
324             __END__