File Coverage

lib/Dancer2/Plugin/OpenAPIRoutes.pm
Criterion Covered Total %
statement 35 223 15.7
branch 1 96 1.0
condition 0 42 0.0
subroutine 12 22 54.5
pod 0 1 0.0
total 48 384 12.5


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::OpenAPIRoutes;
2              
3 1     1   526 use strict;
  1         2  
  1         23  
4 1     1   4 use warnings;
  1         2  
  1         32  
5              
6             # ABSTRACT: A Dancer2 plugin for creating routes from a Swagger2 spec
7             our $VERSION = '0.03'; # VERSION
8 1     1   4 use File::Spec;
  1         2  
  1         19  
9 1     1   319 use Dancer2::Plugin;
  1         10532  
  1         7  
10 1     1   2673 use Module::Load;
  1         2  
  1         8  
11 1     1   54 use Carp;
  1         3  
  1         40  
12 1     1   427 use JSON ();
  1         5537  
  1         25  
13 1     1   296 use JSON::Pointer;
  1         5556  
  1         29  
14 1     1   234 use YAML::XS;
  1         1819  
  1         48  
15 1     1   263 use Data::Walk;
  1         822  
  1         75  
16              
17             our $ValidationCompiler; ## no critic (Variables::ProhibitPackageVars)
18              
19             BEGIN {
20 1     1   6 no strict 'refs';
  1         3  
  1         40  
21 1 50   1   5 if (%{"JSV::Compiler::"}) {
  1         1997  
22 0           $ValidationCompiler = JSV::Compiler->new;
23             }
24             }
25              
26             sub _path2mod {
27             ## no critic (BuiltinFunctions::ProhibitComplexMappings)
28 0     0     map {s/[\W_]([[:lower:]])/\u$1/g; ucfirst} @_;
  0            
  0            
29             }
30              
31             # this complex function makes routes to module::function mapping
32             sub _build_path_map {
33 0     0     my $schema = $_[0];
34 0           my $paths = $schema->{paths};
35             #<<<
36             my @paths =
37             map {
38 0           my $p = $_;
39 0           my $ps = $_;
40 0           $p =~ s!/\{[^{}]+\}!!g;
41             (
42             $p,
43             [
44 0           map { +{ method => $_, pspec => $ps } }
45 0           grep { !/^x-/ }
46 0           keys %{ $paths->{$_} }
  0            
47             ]
48             )
49             }
50             sort { ## no critic (BuiltinFunctions::RequireSimpleSortBlock)
51 0           my @a = split m{/}, $a;
52 0           my @b = split m{/}, $b;
53 0           @b <=> @a;
54             }
55 0 0         grep { !/^x-/ && 'HASH' eq ref $paths->{$_} }
56 0           keys %{$paths};
  0            
57             #>>>
58 0           my %paths;
59             ## no critic (ControlStructures::ProhibitCStyleForLoops)
60 0           for (my $i = 0; $i < @paths; $i += 2) {
61 0           my $p = $paths[$i];
62 0           my $ma = $paths[$i + 1];
63 0           my $m;
64 0           my $mn = @$ma;
65 0 0 0       if ($mn == 1 && !exists $paths{$p}) {
66 0           my @p = split m{/}, $p;
67 0 0         if (@p > 2) {
68 0           $m = pop @p;
69             }
70 0           $p = join "/", @p;
71             }
72 0 0         if ($m) {
73 0           push @{$paths{$p}}, $m;
  0            
74 0           my $ps = $ma->[0]{pspec};
75 0           my $method = $ma->[0]{method};
76 0           $paths->{$ps}{$method}{'x-path-map'} = {
77             module_path => $p,
78             func => $m
79             };
80             } else {
81 0           for (@$ma) {
82 0           my $ps = $_->{pspec};
83 0           my $method = $_->{method};
84 0           push @{$paths{$p}}, $method;
  0            
85 0           $paths->{$ps}{$method}{'x-path-map'} = {
86             module_path => $p,
87             func => $method
88             };
89              
90             }
91             }
92             }
93 0           return \%paths;
94             }
95              
96             my %http_methods_func_map_orig = (
97             get => 'fetch',
98             post => 'create',
99             patch => 'update',
100             put => 'replace',
101             delete => 'remove',
102             options => 'choices',
103             head => 'check'
104             );
105              
106             my %http_methods_func_map;
107              
108             sub _path_to_fqfn {
109 0     0     my ($config, $schema, $path_spec, $method) = @_;
110 0           my $paths = $schema->{paths};
111 0           my $module_name;
112 0           my $func = $paths->{$path_spec}{$method}{'x-path-map'}{func};
113 0           my @pwsr = split m{/}, $paths->{$path_spec}{$method}{'x-path-map'}{module_path};
114 0           $module_name = join "::", map {_path2mod $_ } @pwsr;
  0            
115 0 0         if ($http_methods_func_map{"$method:$path_spec"}) {
116 0           my ($mf, $mm) = split /:/, $http_methods_func_map{"$method:$path_spec"}, 2;
117 0 0         $func = $mf if $mf;
118 0 0         $module_name = $mm if $mm;
119             }
120 0 0         if ($module_name eq '') {
121 0   0       $module_name = $config->{default_module} || $config->{appname};
122             } else {
123 0           $module_name = $config->{namespace} . $module_name;
124             }
125 0 0         my $rfunc = $http_methods_func_map{$func} ? $http_methods_func_map{$func} : $func;
126 0 0 0       if ($rfunc eq 'create' && $func eq 'post' && $path_spec =~ m{/\{[^/{}]*\}$}) {
      0        
127 0           $rfunc = 'update';
128             }
129 0           $rfunc =~ s/\W+/_/g;
130 0           return ($module_name, $rfunc);
131             }
132              
133             sub load_schema {
134 0     0 0   my $config = shift;
135 0 0         croak "Need schema file" if not $config->{schema};
136 0           my $schema;
137 0           my $file = File::Spec->catfile($config->{app}->location, $config->{schema});
138 0 0         if ($config->{schema} =~ /\.json/i) {
    0          
139 0           require Path::Tiny;
140 0           $schema = JSON::from_json(path($file)->slurp_utf8);
141             } elsif ($config->{schema} =~ /\.yaml/i) {
142 0           $schema = YAML::XS::LoadFile $file;
143             }
144 0 0 0       if ($schema && 'HASH' eq ref $schema) {
145             walkdepth + {
146             wanted => sub {
147 0 0 0 0     if ( "HASH" eq ref $_
      0        
      0        
148             && exists $_->{'$ref'}
149             && !ref $_->{'$ref'}
150             && keys %$_ == 1)
151             {
152 0           (my $r = $_->{'$ref'}) =~ s/^#//;
153 0           my $rp = JSON::Pointer->get($schema, $r);
154 0 0         if ('HASH' eq ref $rp) {
155 0           %$_ = %$rp;
156             } else {
157 0           croak "Can't load schema part: " . YAML::XS::Dump($_);
158             }
159             }
160             }
161 0           }, $schema;
162             }
163 0           return $schema;
164             }
165              
166             sub _make_handler_params { ## no critic (Subroutines::ProhibitExcessComplexity)
167 0     0     my ($mpath, $parameters) = @_;
168 0           my $param_eval = '';
169 0           my %schema;
170 0           for my $parameter_spec (@$parameters) {
171 0 0         next if $parameter_spec =~ /^x-/;
172 0           my $in = $parameter_spec->{in};
173 0           my $name = $parameter_spec->{name};
174 0           my $required = $parameter_spec->{required};
175 0           my $req_code = "push \@\$errors, \"required parameter '$name' is absent\" if not exists \$input->{\"$name\"};\n ";
176 0           my $src;
177             ## no critic (ControlStructures::ProhibitCascadingIfElse)
178 0 0         if ($in eq 'body') {
    0          
    0          
    0          
    0          
179 0 0 0       $req_code
180             = $required && !$ValidationCompiler
181             ? "push \@\$errors, \"required parameter '$name' is absent\""
182             . " if not ref \$input->{\"$name\"} or not keys %{\$input->{\"$name\"}};"
183             : '';
184             #<<<
185 0           $param_eval .=
186             "{ my \$value;\n"
187             . " if (\$app->request->header(\"Content-Type\")\n"
188             . " && \$app->request->header(\"Content-Type\") =~ m{application/json}) {\n"
189             . " \$value = JSON::decode_json (\$app->request->body)\n } else {\n"
190             . " \$value = \$app->request->body }\n"
191             . " \$input->{\"$name\"} = \$value if defined \$value; $req_code"
192             . "}\n";
193             #>>>
194 0           $req_code = '';
195             } elsif ($in eq 'header') {
196 0           $param_eval .= "\$input->{\"$name\"} = \$app->request->header(\"$name\");\n";
197             } elsif ($in eq 'query') {
198 0           $src = "\$app->request->params('query')";
199             } elsif ($in eq 'path') {
200 0 0 0       if ($parameter_spec->{type} && $parameter_spec->{type} eq 'integer') {
201 0           $mpath =~ s/:$name\b/\\E(?<$name>\\d+)\\Q/;
202 0           $src = "\$app->request->captures";
203             } else {
204 0           $src = "\$app->request->params('route')";
205             }
206             } elsif ($in eq 'formData') {
207 0 0 0       if ($parameter_spec->{type} && $parameter_spec->{type} eq 'file') {
208 0           $param_eval .= "\$input->{\"$name\"} = \$app->request->upload(\"$name\");\n";
209             } else {
210 0           $src = "\$app->request->params('body')";
211             }
212             }
213 0 0         if ($src) {
214 0           $param_eval .= "{ my \$src = $src; \$input->{\"$name\"} = \$src->{\"$name\"} if 'HASH' eq ref \$src; }\n";
215             }
216 0 0         if ($ValidationCompiler) {
217 0           $schema{properties}{$name} = $parameter_spec;
218 0 0 0       if ($schema{properties}{$name}{type} && $schema{properties}{$name}{type} eq 'file') {
219 0           $schema{properties}{$name}{type} = 'string';
220 0           $schema{properties}{$name}{format} = 'binary';
221             }
222 0 0         push @{$schema{required}}, $name if $required;
  0            
223             } else {
224 0 0         $param_eval .= $req_code if $required;
225             }
226             }
227 0 0         if ($ValidationCompiler) {
228 0           $ValidationCompiler->load_schema(\%schema);
229 0           $param_eval .= $ValidationCompiler->compile(input_symbole => '$input', is_required => 0);
230             }
231             $param_eval
232 0           .= "if(\@\$errors) { \$dsl->status('unprocessable_entity'); \$res = { error => join \"; \", \@\$errors }; }\n";
233 0 0         if ($mpath =~ /\(\?
234 0           $mpath = "\\Q$mpath\\E";
235 0           $mpath =~ s/\\Q(.*?)\\E/quotemeta($1)/eg;
  0            
236 0           $mpath = qr|$mpath|;
237             }
238 0           return ($mpath, $param_eval);
239             }
240              
241             sub _path_compare {
242             my $ssc = sub {
243 0 0   0     length($_[1]) >= length($_[0])
244             && substr($_[1], 0, 1 + length $_[0]) eq "$_[0]/";
245 0     0     };
246 0 0         return 0 if $a eq $b;
247 0 0         if ($ssc->($a, $b)) {
248 0           return 1;
249             }
250 0 0         if ($ssc->($b, $a)) {
251 0           return -1;
252             }
253 0           return $a cmp $b;
254             }
255              
256             register OpenAPIRoutes => sub {
257 0     0     my ($dsl, $debug, $custom_map) = @_;
258 0           my $json = JSON->new->utf8->allow_blessed->convert_blessed;
259 0           my $app = $dsl->app;
260 0     0     local $SIG{__DIE__} = sub {Carp::confess(@_)};
  0            
261 0           my $config = plugin_setting;
262 0           $config->{app} = $app;
263 0           $config->{appname} = $dsl->config->{appname};
264 0           my $schema = load_schema($config);
265 0           my $paths = $schema->{paths};
266 0           _build_path_map($schema);
267 0           %http_methods_func_map = %http_methods_func_map_orig;
268              
269 0 0 0       if ($custom_map && 'HASH' eq ref $custom_map) {
270 0           my @cmk = keys %$custom_map;
271 0           @http_methods_func_map{@cmk} = @{$custom_map}{@cmk};
  0            
272             }
273 0           for my $path_spec (sort _path_compare keys %$paths) {
274 0 0         next if $path_spec =~ /^x-/;
275 0           my $path = $path_spec;
276 0           $path =~ s/\{([^{}]+?)\}/:$1/g;
277 0           for my $method (sort keys %{$paths->{$path_spec}}) {
  0            
278 0 0         next if $method =~ /^x-/;
279 0           my ($module_name, $module_func) = _path_to_fqfn($config, $schema, $path_spec, $method);
280 0           my @parameters;
281 0 0         if ($paths->{$path_spec}{$method}{parameters}) {
282 0           @parameters = @{$paths->{$path_spec}{$method}{parameters}};
  0            
283             }
284 0           my ($mpath, $param_eval) = _make_handler_params($path, \@parameters);
285 0 0         my $dancer_method = $method eq 'delete' ? 'del' : $method;
286 0           my $get_env = '';
287 0           for (grep {/^x-env-/} keys %{$paths->{$path_spec}{$method}}) {
  0            
  0            
288 0           my $name = $paths->{$path_spec}{$method}{$_};
289 0           my ($env_var) = /^x-env-(.+)/;
290 0           $env_var = uc $env_var;
291 0           $env_var =~ s/\W/_/;
292 0           $get_env .= "\$input->{'$name'} = \$app->request->env->{'$env_var'} // '';\n";
293             }
294 0           my $prolog_code_src = <<"EOS";
295             sub {
296             my \$input = {};
297             my \$errors = [];
298             my \$res;
299             my \$status;
300             my \$callback;
301             $param_eval;
302             $get_env;
303             (\$res, \$status, \$callback) = eval {${module_name}::$module_func( \$input, \$dsl )} if not \$res;
304             if(\$callback && 'CODE' eq ref \$callback) {
305             \$callback->();
306             }
307             my \$exception = \$\@;
308             if( \$app->request->header(\"Accept\")
309             && \$app->request->header(\"Accept\") =~ m{application/json}
310             && (\$exception || ref \$res)) {
311             \$dsl->content_type("application/json");
312             if (not defined \$res) {
313             \$dsl->error(\$exception) if \$exception;
314             \$res = { error => \$exception };
315             \$res->{error} =~ s/ at .*? line \\d+\.\\n?//;
316             \$dsl->status('bad_request');
317             } else {
318             \$dsl->status(\$status) if \$status;
319             }
320             return \$json->encode(\$res);
321             } else {
322             die \$exception if \$exception and not defined \$res;
323             \$dsl->status(\$status) if \$status;
324             if(!\$status && \$res && ref(\$res) && "\$res" =~ /^(HASH|ARRAY|SCALAR|CODE)\\(/ ) {
325             \$dsl->status('not_acceptable');
326             return;
327             }
328             return \$res;
329             }
330             }
331             EOS
332             ## no critic (BuiltinFunctions::ProhibitStringyEval)
333 0 0         if ($debug > 1) {
334 0           print STDERR "$method $mpath: $prolog_code_src\n";
335             }
336 0           my $prolog_code = eval $prolog_code_src;
337 0 0         if ($@) {
338 0           my $error = $@;
339 0           $dsl->error("$method $mpath ($error): $prolog_code_src");
340 0           croak "Route $method $mpath cant be compiled: $error";
341             }
342 0           my $route = Dancer2::Core::Route->new(
343             method => $method,
344             regexp => $mpath,
345             code => $prolog_code,
346             prefix => $app->prefix
347             );
348 0 0         if ($app->route_exists($route)) {
349 0           croak "Route $method $mpath is already exists";
350             }
351 0 0         $debug && $dsl->debug("$dancer_method $path_spec -> $module_func in $module_name\n");
352 0           my $success_load = eval {load $module_name; 1};
  0            
  0            
353 0 0 0       croak "Can't load module $module_name for path $path_spec: $@"
354             if not $success_load or $@;
355 0           my $cref = "$module_name"->can($module_func);
356 0 0         croak "Can't find function $module_func in module $module_name for path $path_spec"
357             if not $cref;
358 0           $dsl->$dancer_method($mpath => $prolog_code);
359             }
360             }
361             };
362              
363             register_plugin;
364              
365             1;
366              
367             ## no critic (Documentation::RequirePodLinksIncludeText)
368              
369             __END__