File Coverage

lib/Dancer2/Plugin/OpenAPIRoutes.pm
Criterion Covered Total %
statement 30 204 14.7
branch 0 84 0.0
condition 0 36 0.0
subroutine 10 20 50.0
pod 0 1 0.0
total 40 345 11.5


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