File Coverage

blib/lib/Dancer2/Plugin/Swagger2.pm
Criterion Covered Total %
statement 59 140 42.1
branch 15 86 17.4
condition 6 20 30.0
subroutine 10 14 71.4
pod 0 1 0.0
total 90 261 34.4


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