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__ |