File Coverage

blib/lib/Dancer2/Plugin/Swagger2.pm
Criterion Covered Total %
statement 59 99 59.6
branch 15 46 32.6
condition 6 12 50.0
subroutine 10 12 83.3
pod 0 1 0.0
total 90 170 52.9


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