File Coverage

lib/Dancer2/Plugin/OpenAPIRoutes.pm
Criterion Covered Total %
statement 30 190 15.7
branch 0 74 0.0
condition 0 30 0.0
subroutine 10 20 50.0
pod 0 1 0.0
total 40 315 12.7


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